/* ============================================================================
   JT CONTENT PAGES — v2.0.0
   Stylesheet for pages produced by build/generate.js from OUTPUT/ markdown.

   Loads AFTER /styles/common.css, which owns the palette, the reset, the base,
   the skip link and the eyebrow. What is left here is the content page: the
   Figma Content container, the page header, and the prose.

   The page shell is derived from Figma "Content page" (87:9989), specifically
   its "Content container" (87:9993). At the 1920px reference width the panel
   is 1440px wide, starts at y=190, has 64/80/72px internal padding, a 56px
   vertical gap, 3px corners, 28px background blur and a 0 24px 60px shadow.
   Those measured desktop values are preserved exactly; fluid values below the
   comp width keep the same hierarchy without forcing desktop padding onto a
   phone.

   NAMING
   The --jt- prefix belongs to frozen modules and must not appear here. This
   file must never style anything inside .jt-menu or .jt-footer.
   ========================================================================== */

:root {
  /* --- overrides of common.css -------------------------------------------
     The container metrics are this page's alone. --content-max is 1440 here
     against the landing page's 1600, both from their own comps.

     --panel is now only the <details> block's fill. The main container stopped
     using it when the comp restyled that panel — see .content-container. */
  --panel:                rgba(8, 13, 20, 0.55);

  --content-max:          1440px;
  /* SIDE SPACING IS STEPPED, NOT FLUID, and it comes from one table.

     Figma "[Foundation] Grid & vertical rhythm" → `grid spec` states a margin
     and a panel padding per range, and they are discrete values, not a curve:

       range              margin   panel            panel padding (h)
       320–509 Compact      12     viewport − 24          16
       510–719 Wide         24     viewport − 48          32
       720–1024 Tablet      32     = container            48
       1025–1440 Desktop    48     1040 centred           80

     These were `clamp()`s, and a clamp cannot express that table: at 320 the
     gutter clamped to its 24px floor where the grid says 12, and at 768 it sat
     at 24 where the grid says 32. Every range was wrong by a different amount,
     which is why nothing looked systematically off — just vaguely inconsistent.

     landing.css already steps its own --gutter through 12/24/32/48 from this
     same frame, so the two now line up on every range instead of only where
     the two curves happened to cross.

     VERTICAL PADDING IS LEFT ALONE. The table gives one vertical number per
     range and this file distinguishes top from bottom; reconciling that needs
     its own read of the frame, and it is not what side spacing is about. */
  --content-page-gutter:  16px;
  --content-pad-inline:   16px;

  --content-pad-top:      clamp(28px, 2.917vw, 56px);
  --content-pad-bottom:   clamp(40px, 3.75vw, 72px);
  --content-gap:          clamp(36px, 2.917vw, 56px);

  /* The gap below the header is the HERO's now — see .page__hero. */

  /* The measure is a length rather than a ch count so it holds steady across
     all three languages — Russian sets wider than English at the same size. */
  --measure:              880px;

  /* --- the disclosure marker ----------------------------------------------
     Figma "btn icon" (65:2671), members Icon=plus (804:7942) and Icon=minus
     (804:8072), as instanced by BOTH accordion components: "Accordion - small"
     (217:4387) and "Accordion - big" (628:7041). 16 × 16 box, an 8 × 8 cross
     inset at 4,4, round caps.

     STROKE WIDTH IS 1, NOT THE SET'S 1.2. Both accordions override it on the
     instance, and the accordions are what these serve. Read off the instances
     (glyph.strokeWeight === 1), not off the main component.

     They are masks rather than <img>: a mask takes its colour from the box it
     paints, so one glyph serves the pale blue at rest, the amber on hover and
     the full-strength blue when open. An <img> would need three files, and the
     marker is a ::before — there is no element to put an <svg> inside, because
     <details>/<summary> are hand-written in the markdown and this stylesheet
     never sees them until they are already HTML.

     Declared once, used by both accordions, so the two cannot drift. If the
     design changes the glyph again, this is the only place it lives.
     ---------------------------------------------------------------------- */
  --icon-plus:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 8H12M8 4V12' stroke='%23000' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 8H12' stroke='%23000' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* The grid's other three ranges. `min-width`, so the compact values in :root
   are the base and each range overrides the one below — the same shape
   landing.css uses for its --gutter, from the same frame. */
@media (min-width:  510px) { :root { --content-page-gutter: 24px; --content-pad-inline: 32px; } }
@media (min-width:  720px) { :root { --content-page-gutter: 32px; --content-pad-inline: 48px; } }
@media (min-width: 1025px) { :root { --content-page-gutter: 48px; --content-pad-inline: 80px; } }

/* The surface primitive sits HERE, before every construct that uses it, so a
   block can override its geometry with a plain class — the tab panel does.
   Defined after them it won on source order and welded edges came back. */
/* ===========================================================================
   THE SURFACE — Figma variable collection "Panel"

   Every boxed thing in this dialect is the same surface in one of four tones.
   It used to be nine classes each restating a fill, a stroke and a radius from
   memory, and they had drifted apart: five different stroke colours, four fill
   values, radii of 2, 3, 4, 6 and 8, for boxes the design draws identically.

   The collection holds SIX variables. They are transcribed here once, under
   the names Figma gives them, and nothing else in this file writes a panel
   fill or a panel stroke literal:

     Fill - neutral        #ffffff  3%
     Fill - highlighted    #40d1ff  5%
     Fill - dark           #080d14 50%
     Stroke - neutral      #89e3f5 10%
     Stroke - highlighted  #40d1ff 36%
     Stroke - important    #d06700 40%

   The four tones are the pairings the file actually uses, counted across all
   130 layers bound to those variables:

     (default)      neutral fill + neutral stroke      a structural box
     --important    neutral fill + important stroke    something to note
     --highlighted  highlighted  + highlighted stroke  selected, or on offer
     --dark         dark fill    + highlighted stroke  a cell inside one of those

   Radius is 4 everywhere the design has been swept. Two layers still say 2, 3
   and 6 — "[panel] First session checklist", "Practice types" and the Uniform
   copy of "[panel] Hit qualification" — against 25 that say 4; they are read
   as copies that missed the sweep, not as intent, and are NOT special-cased.
   The tab panel is the one real exception and says so at its own rule.

   A block class carries LAYOUT — display, gap, padding. The surface arrives in
   the markup as `class="panel surface surface--important"`, the way a Figma
   layer binds the variable rather than inheriting it from its name. A new
   block adds a class; it does not add three more declarations.
   ========================================================================= */
/* ---------------------------------------------------------------------------
   COLOUR IN THIS FILE COMES FROM TOKENS, WITH THREE NAMED EXCEPTIONS.

   Every colour here used to be a hex literal measured off a node. That gave the
   site FIVE greys doing the job of "body copy" (#a7b9cb, #b2c7db, #9fb2c6,
   #9eb2c7, #bdd1e3) and FOUR whites doing "heading" (#ebf2f8, #f0f7ff, #ebf2fa,
   #fff) — differences no one chose, just different days' measurements.

   The comp is not ambiguous about this: of 151 body-copy nodes across the
   eighteen page frames, 148 are #bdcddb bound to `Text/Paragraph`; of 152
   heading nodes, 94 are #cbddea bound to `Text/Heading`. So the greys collapse
   to --body, the whites to --ink, and the brighter tier (--ink-soft) is kept
   for open and selected STATES, which is the one place the comp reliably
   brightens a heading.

   THE THREE THAT STAY LITERAL have no Figma variable behind them and are not
   near-duplicates of one that does:
     #f69638  panel and column labels — a second, lighter orange than
              Text/Orange #eb8b2d. Verified against the comp today; snapping it
              to --amber would be a visible change no comp asks for.
     #7dd3fc  the route number and "more" link — between Blue #00d4ff and
              Blue-pale #40d1ff, matching neither.
     #f0a416  scale and metric values — an amber-yellow used nowhere else.
   If any of the three gains a variable, it should move here with the rest.
   ------------------------------------------------------------------------- */

/* The six panel fills and strokes are NOT declared here any more — they are
   Figma's `Panel` collection, generated into styles/tokens.css, which loads
   before this file. Re-declaring them here would win on source order and
   silently pin the panels to whatever was true when they were pasted in: the
   stroke had already drifted to 0.10 against Figma's 0.11 that way.

   Only the two values with no Figma variable behind them stay. */
:root {
  --panel-radius:             4px;
  --panel-pad:                24px;
}

/* The tone properties are RE-DECLARED here, not read with a var() fallback.
   Custom properties inherit, so a surface nested inside a toned one picked up
   its ancestor's tone: a :::metrics cell inside an important :::panel came out
   with the amber stroke. Declaring the defaults on .surface itself means every
   surface starts neutral and only its own modifier moves it. */
.surface {
  --surface-fill:   var(--panel-fill-neutral);
  --surface-stroke: var(--panel-stroke-neutral);
  --surface-radius: var(--panel-radius);

  border: 1px solid var(--surface-stroke);
  border-radius: var(--surface-radius);
  background: var(--surface-fill);
}

.surface--important   { --surface-stroke: var(--panel-stroke-important); }

.surface--highlighted {
  --surface-fill:   var(--panel-fill-highlighted);
  --surface-stroke: var(--panel-stroke-highlighted);
}

/* FILL ONLY. "equipment requirement" State=default and the weapon-suitability
   cells carry a stroke bound to Stroke - highlighted with the paint SWITCHED
   OFF; the hover and focus variants are what turn it on. Reading a bound-but-
   hidden paint as a visible one put a cyan outline on every gear cell — the
   border stays in the box so the layout does not shift when hover adds it. */
.surface--dark {
  --surface-fill:   var(--panel-fill-dark);
  --surface-stroke: transparent;
}

/* One layer draws the neutral stroke with its fill switched off rather than
   set — "Practice types" on Martial arts landscape. A tone, not an exception. */
.surface--bare { --surface-fill: transparent; }

/* Rows inside one surface are divided, not boxed: the hairline is the surface's
   own stroke, so a divider and a border can never come to disagree. */
.branch + .branch,
.scale__row + .scale__row,
.step + .step { border-top: 1px solid var(--panel-stroke-neutral); }


/* Reading typography. The structural half of <body> is in common.css.
   Figma Body/Paragraph: Exo 2 Light 16/26, +1px. Component copy that uses
   Body/Paragraph Small overrides this locally at 14/24, +1.2px. */
body {
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

/* ---------------------------------------------------------------------------
   Content page and Figma Content container
   ------------------------------------------------------------------------- */
/* Top padding clears the FIXED MENU and nothing else.

   It used to be clamp(140px, 12vw, 300px), from when this page owned its
   header's placement. The hero module owns that now and carries the comp's
   120px above itself, so the two were stacking: 300 + 120 = 420 at a wide
   viewport, against the comp's 180.

   The rebuilt frames say it plainly — the page frame's own top padding is 60,
   which is the menu's height, and the Hero instance's 120 sits inside that. So
   this is the menu clearance, expressed as the menu's own variable rather than
   a number that would silently disagree if the bar ever changed height. */
.content-page {
  width: 100%;
  margin-inline: auto;
  padding: var(--menu-height) var(--content-page-gutter) 0;
}

/* The panel, re-measured from the comp. Every value here changed — the fill was
   near-black at 55%, it is now a desaturated slate at 8%; there is a hairline
   border that did not exist; the corner is 4px not 3; the background blur is 20
   not 28; the drop shadow is an ambient 80px halo rather than a 24px offset
   one; and there is a new inner cyan glow. Replaced wholesale rather than
   nudged, because reading them as adjustments to the old numbers is how a panel
   ends up half in one style and half in another. */
.content-container {
  display: flex;
  flex-direction: column;
  gap: var(--content-gap);
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding: var(--content-pad-top) var(--content-pad-inline)
           var(--content-pad-bottom);
  border: 1px solid rgba(75, 97, 107, 0.20);
  border-radius: 4px;
  background: rgba(63, 78, 82, 0.08);
  /* Ambient halo, then the inner glow that lifts the top edge off the
     background. Figma draws these as two separate effects on one frame. */
  box-shadow: 0 0 80px rgba(0, 9, 14, 0.25),
              inset 0 0 70px rgba(46, 199, 254, 0.05);
  -webkit-backdrop-filter: blur(15px);
  backdrop-filter: blur(15px);
}

/* ---------------------------------------------------------------------------
   The hero — OUTSIDE the panel, above it

   Rendered and styled by modules/hero/. This page says only how wide it is and
   how much room it takes; the eyebrow, the type and the internal gaps are the
   module's. Everything that used to be `.page__header` is in
   modules/hero/hero.css.

   Measured from the rebuilt page frames, where the Hero instance is 1042 wide
   with padding 120 / 0 / 64 / 0:

     width   the container's OWN width, not its padding box — which is what
             puts the title 80px left of the article text below it
     before  120 to the menu, which the frame's own 60px top padding sits above
     after   64 to the top of the panel
   ------------------------------------------------------------------------- */
.page__hero {
  --jt-hero-space-before: clamp(64px, 6.25vw, 120px);
  --jt-hero-space-after:  clamp(36px, 3.33vw, 64px);
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
}

/* ===========================================================================
   TEMPLATE: content-page
   Figma "Content page without sidebar" (196:7638).

   Opted into per page and per language, from frontmatter:  template: content-page
   The generator puts that value on <body data-template>, so everything below is
   scoped to it and no other page moves until it opts in too. When this becomes
   the default the attribute selector comes off and these rules fold into the
   block above.

   Measured at the 1920 reference width:
     container   1040 wide (not 1440), padding 64/80/72/80, gap 56, radius 3
     title       Tektur SemiBold 42 / +0.5px  (the default shell uses 64 Bold)
     rule        1px #7dd3fc at 14%

   The comp's breadcrumb is NOT implemented. Every page here sits at one depth,
   the category crumb had no destination because categories have no index page,
   and the last crumb is the current page — one working link, to Home, which the
   fixed menu already gives. The category is the eyebrow above the title
   instead. See NO BREADCRUMB in build/generate.js.
   ========================================================================= */

:is([data-template="content-page"], [data-template="personal-equipment"]) {
  /* 1040 − 160 of padding = an 880 measure, which is what the comp's article
     column is. --measure already carries 880, so the prose needs no change. */
  --content-max: 1040px;
}

/* Figma "[Website Page] Personal equipment" (459:4466).

   The article starts with the qualification panel; the older standalone
   introduction is hidden in the frame and is therefore not published. The
   four article sections remain 56px apart, while blocks inside a section are
   32px apart. Scope these overrides to this page so the shared panel, columns
   and heading components keep their existing rhythm everywhere else. */
[data-template="personal-equipment"] .prose > h4,
[data-template="personal-equipment"] .prose > p {
  margin-bottom: 32px;
}

[data-template="personal-equipment"] .prose > .panel--important {
  gap: 16px;
  padding-bottom: 32px;
}

[data-template="personal-equipment"] .prose > .columns {
  padding-bottom: 32px;
}

[data-template="personal-equipment"] .column {
  gap: 16px;
}

[data-template="personal-equipment"] .tier__body li + li {
  margin-top: 0;
}

/* ===========================================================================
   TEMPLATE: Nutrition
   Figma "[Website Page] Nutrition" (764:7832).

   This is deliberately a template rather than a collection of page-name
   selectors. The three translations share the same document structure, while
   their headings and paragraph lengths differ. All geometry below therefore
   follows semantic siblings instead of English ids or copy.

   Reference frame at 1920px:
     hero / panel       1042px wide
     panel              64 / 80 / 64 padding, 7.5px background blur
     reading measure    880px
     sections           56px apart
     heading → body     32px (20px in the practical guide)
     body blocks        20px apart
   ========================================================================== */
[data-template="nutrition"] {
  --content-max:        1042px;
  --content-pad-top:    clamp(32px, 3.333vw, 64px);
  --content-pad-bottom: clamp(40px, 3.333vw, 64px);
  --measure:            880px;
  /* NO --content-pad-inline here. It restated the old clamp, which would have
     survived the move to the grid's stepped values and left this one template
     with the fluid side padding every other page had just stopped using. Its
     vertical rhythm is its own; its side spacing is the site's. */
}

[data-template="nutrition"] .content-container {
  -webkit-backdrop-filter: blur(7.5px);
  backdrop-filter: blur(7.5px);
}

/* Figma keeps strokes inside a frame, whereas CSS border-box subtracts the two
   1px borders from its content box. Restore the comp's measured 882px inner
   column and align it to x+80 without changing the panel's 1042px outside. */
[data-template="nutrition"] .prose {
  width: calc(100% + 2px);
  max-width: 882px;
  margin-inline: -1px;
  margin-block: -1px;
}

/* The Nutrition comp uses an explicit stacked rhythm. Reset the generic prose
   margins first so adjacent margins cannot make a measured 20/32/56px gap
   browser-dependent, then state each relationship once.

   THE TAGS ARE h4 AND h5, AND THAT RESET IS WHY IT MATTERS.

   This block was written against `h2` and `h3`, from when markdown depth WAS
   the tag number and `##` produced an h2. Depth was later remapped onto the
   design's ramp, so the page started emitting h4 and h5 — and every selector
   here stopped matching. The `.prose > *` reset above kept working, because it
   matches everything, so the page lost its 56/32/20 rhythm entirely and every
   heading sat flush against the paragraph on either side of it. A section
   heading with no space above it reads as part of the paragraph before it.

   That is the failure mode of a reset plus tag-keyed spacing: the reset never
   goes stale, and the rules that give the space back do. build-test now asserts
   that every tag named here is a tag the built page actually contains.

   The type is NOT restated. h4 and h5 take size, weight and tracking from the
   shared ramp in common.css — this template only says where things sit and
   that its sub-heading is cyan. */
[data-template="nutrition"] .prose > * { margin: 0; }

/* The one type value that is this template's own: the comp's sub-heading is
   cyan, where the ramp's default heading colour is --ink. */
[data-template="nutrition"] .prose > h5 { color: var(--cyan); }

[data-template="nutrition"] .prose > h4 { margin-top: 56px; }

[data-template="nutrition"] .prose > p {
  color: var(--body);
}

[data-template="nutrition"] .prose > h4 + * { margin-top: 32px; }
[data-template="nutrition"] .prose > h5 + p,
[data-template="nutrition"] .prose > p + p,
[data-template="nutrition"] .prose > p + ul,
[data-template="nutrition"] .prose > ul + p,
[data-template="nutrition"] .prose > p + .blockquote,
[data-template="nutrition"] .prose > p + hr,
/* The guide's closing rule follows its four-item list, not a paragraph, so the
   `p + hr` pair above never covered it and the rule sat flush against the last
   bullet. Only visible once the rest of the rhythm was restored. */
[data-template="nutrition"] .prose > ul + hr,
[data-template="nutrition"] .prose > hr + p { margin-top: 20px; }

[data-template="nutrition"] .prose > h4 + h5 { margin-top: 32px; }
[data-template="nutrition"] .prose > p + h5 { margin-top: 32px; }

/* The fifth section is the practical guide in every translation. Its intro is
   ordinary body copy before the four-item Figma `ul` component. */
[data-template="nutrition"] .prose > h4:nth-of-type(5) + p { margin-top: 20px; }

[data-template="nutrition"] .prose > ul {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

[data-template="nutrition"] .prose > ul > li { margin: 0; }

/* Sources is the final h5/list pair, after the guide's closing rule. It is a
   quiet reference list rather than another cyan subheading or bulleted list. */
[data-template="nutrition"] .prose > p + h5:last-of-type { margin-top: 56px; }

[data-template="nutrition"] .prose > h5:last-of-type {
  color: var(--ink);
}

[data-template="nutrition"] .prose > h5:last-of-type + ul {
  margin-top: 20px;
  padding: 0;
  list-style: none;
}

[data-template="nutrition"] .prose > h5:last-of-type + ul > li {
  min-height: 0;
  padding-inline-start: 0;
  color: var(--body);
  font-size: 14px;
  line-height: 24px;
  letter-spacing: 1.2px;
}

[data-template="nutrition"] .prose > h5:last-of-type + ul > li::before {
  content: none;
  background: none;
}

/*[data-template="nutrition"] .prose > h5:last-of-type + ul a {
  color: var(--body);
  text-decoration-color: currentColor;
}*/

@media (max-width: 600px) {
  [data-template="nutrition"] .prose {
    width: 100%;
    margin-inline: 0;
  }
}

/* NO lede colour override here. This briefly carried
   `.page__hero { --jt-hero-body: #9fb2c6 }`, read off Martial arts landscape
   (532:5842) as "content-page ledes are quieter". Training structure
   (551:6021) then drew its lede at #bdd1e3 — the module's own default — so the
   shade tracks the KIND of lede, not the template: an epigraph is quieter than
   a plain one. One page is not a rule, and a blanket override would have made
   the second page wrong to fix the first. The module default stands until
   there is a reason to name that distinction. */

/* The title is the hero module's now, and the same on every template — the
   Hero component draws one title, at Tektur Bold 52. The old shell's 42px
   SemiBold and the 64px default both went with `.page__header`. */

/* ---------------------------------------------------------------------------
   :::route — the Warm-up comp's "Four-stage route"

   Measured from "Four-stage route" in Figma "Warm-up" (218:2413):

     row       880 wide, HORIZONTAL, gap 32; four cards FILL to 196 each
               (880 − 3×32) ÷ 4 = 196
     card      196×206, padding 16, gap 8, radius 4,
               fill #080d14 at 45%, no stroke, background blur 18
     number    Tektur Medium 11 / +1.7 / upper / #7dd3fc
     title     Tektur Regular 13 / +2   / upper / #ebf2f8
     desc      Exo 2 Light 14 / 22 / +1.2       / #a7b9cb
     arrow     16×16, centred in each gap; the glyph is a 10×7 stroked chevron,
               #7dd3fc at 92%

   THE NUMBERS ARE THE CARDS' POSITIONS, not written in the markdown — see
   :::route in build/markdown.js. The arrows are pseudo-elements on every card
   but the first, so they land in the gaps whatever the count and wherever the
   row wraps; the exported glyph is assets/route/connector.svg, hash-verified
   against Figma rather than redrawn.
   ------------------------------------------------------------------------- */
/* NO MARGIN DECLARED HERE, DELIBERATELY. A component that says `margin: 0`
   says it with one class — (0,1,0) — and the article's rhythm rule
   `:where(.prose) > * + *` is (0,0,0), because `*` and `+` count for nothing.
   So every component that zeroed its own margin also cancelled the 32 the
   article wanted before it, and the blocks sat flush against the paragraph
   above. The container already zeroes its top-level children; anything nested
   is zeroed by a `:where()`-wrapped rule that the rhythm can outrank. */
:where(.prose) .route {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 32px;
  padding: 0;
  list-style: none;
  counter-reset: none;
}

/* The comp draws these as "[panel] Stage 01–04" — the neutral surface at 16px
   with a background blur, not the bordered-less dark box this rule used to
   invent. The blur is the stage's own; only the fill, stroke and radius come
   from .surface. */
.route__stage {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
}

/* The arrow sits in the gap to this card's LEFT, so the first card has none and
   a wrapped row never trails one off its end. */
.route__stage + .route__stage::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -24px;                  /* half the 32 gap, minus half the 16 glyph */
  width: 16px;
  height: 16px;
  transform: translateY(-50%);
  background: url("/assets/route/connector.svg") center / contain no-repeat;
  pointer-events: none;
}

.route__number {
  color: #7dd3fc;
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}

.route__title {
  color: var(--ink);
  font-family: var(--display);
  font-size: var(--body-label-size);
  font-weight: var(--body-label-weight);
  letter-spacing: var(--body-label-tracking);
  line-height: 1.31;
  text-transform: uppercase;
}

.route__desc {
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  letter-spacing: 1.2px;
  line-height: 24px;
}

/* Derived, not drawn — the comp gives one width. Two columns keep the cards
   near their designed 196; one column below that, where the arrow would point
   sideways at nothing and is dropped. */
@media (max-width: 720px) {
  .prose .route { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
  .route__stage + .route__stage::before { display: none; }
}
@media (max-width: 460px) {
  .prose .route { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------------------
   The golden balance figure, mid-article

   Figma "Triunity of Dualities" (492:23712) embeds the hero figure between the
   opening paragraph and the first heading. Its wrapper there is `Frame 263`:
   VERTICAL, gap 10, padding 20 / 0 / 20 / 0, children centred, 880 wide with a
   666-wide figure inside it.

   Rendered by modules/balance/ via the `:::balance` block — see BUILD.md. This
   page says only where it sits and how wide it is; the drawing, its colours and
   its motion are the module's. Both captions are hidden on this page, which is
   a module option, not something done here.
   ------------------------------------------------------------------------- */
.prose__balance {
  display: flex;
  justify-content: center;
  padding-block: 20px;
}

.prose__balance .jt-balance { --jt-balance-width: min(666px, 100%); }

/* ---------------------------------------------------------------------------
   Prose
   Everything the markdown converter can emit, and nothing it cannot.
   ------------------------------------------------------------------------- */
:where(.prose) {
  width: 100%;
  max-width: var(--measure);
}

/* --- THE ARTICLE'S VERTICAL RHYTHM, from the comps ------------------------
   One flat 20 used to serve for everything here. The comps say three numbers,
   and the difference between them is the difference between blocks that belong
   to the same thought and blocks that do not:

     56   between SECTIONS.  Every "[Website Page]" frame's Content container
          is `VERTICAL gap 56`, without exception — checked across all sixteen.
          Carried by the h4, which is what opens a section.
     32   between BLOCKS inside a section. The section frames' own itemSpacing:
          32 on the large majority, with a few at 20 or 24 where a note follows
          a short intro. 32 is the value, not an average of a spread.
     18   between PARAGRAPHS of one block. Not a frame gap at all — it is the
          `paragraphSpacing` of a single text node, and it is 18 on every
          Description and Body layer in the file, landing and articles alike.

   The 20 was close enough to 18 to look deliberate and close enough to 32 to
   be missed, which is why it survived. It made every heading sit 12px tighter
   to its first paragraph than drawn, on a dozen pages.

   EXPRESSED AS "SPACE BEFORE", never after. `* + *` cannot fire on the first
   child, so no `:first-child` reset is needed, and with no bottom margins in
   play there is nothing for a `:last-child` rule to clear either — both went.
   A component that wants a different gap before it says so and wins on
   specificity; see `.blockquote + .timeline` further down. */
/* NESTED FIRST, AND THE ORDER IS THE WHOLE POINT. These selectors are
   (0,0,0) — everything is inside :where() — and so are the two adjacency rules
   below them, so the LATER rule wins wherever both match. A blockquote at the
   top level is matched by both and takes the rhythm's 32; one inside a zone or
   a step is matched only by this, and keeps 0 so its own container's gap
   spaces it. Move this below the rhythm and every top-level block goes flush
   against the paragraph above it, which is exactly the bug this replaced. */
:where(.prose :is(.blockquote, .figure, .options, .route, .timeline, ul, ol))
  { margin: 0; }

/* NOT :where()-WRAPPED, AND THAT IS THE ONE EXCEPTION IN THIS FILE.
   Everything else here is a container DEFAULT, written `:where(.prose) p` so a
   component class always beats it — see CLAUDE.md, and the three bugs that
   rule exists to prevent.

   These three are not a default. They are the article's layout: where each
   block sits relative to the one above it, which is the page's job and not the
   component's. At (0,0,0) — `*` and `+` count for nothing — every component
   that says `margin: 0` in common.css outranked them, so `.blockquote` and
   friends sat flush against the paragraph above while `.route` and `.zones`,
   which say nothing, spaced correctly. One class puts these at (0,1,0), above
   a bare component class by source order (content.css loads after common.css)
   and below anything that names an element or two classes — which is how the
   deliberate exceptions below keep winning:

     .prose :is(h4,h5,h6)          (0,1,1)  56 before a section
     .prose h4 + h5                (0,1,2)  headings that touch
     .prose hr / img               (0,1,1)  their own drawn rhythm
     .prose > .blockquote+.timeline(0,2,0)  56, measured
     .zone__items > .blockquote    (0,2,0)  24, the column's own gap
     [data-template=…] .prose > *  (1,1,0)  a template that draws its own */
.prose > * { margin: 0; }
.prose > * + * { margin-top: 32px; }
.prose > p + p { margin-top: 18px; }

/* THE RHYTHM'S EXCEPTIONS, and they are child-scoped for a reason.

   These three elements draw their own vertical rhythm — a rule breathes, an
   image sits in its own space — so they have to outrank the 32 above, which
   means one class: `.prose > hr` is (0,1,1).

   CHILD, not descendant. `.prose img { margin: 1.6em 0 }` also reads (0,1,1),
   and it beat `.card__photo`, `.swing__art` and `.semicircle__ring` — art that
   sets its own height and corners inside a card. That is the bug CLAUDE.md
   opens with, reintroduced by a specificity bump made for an unrelated reason.
   The rhythm only ever applies to a DIRECT child of .prose, so the exception
   only needs to reach that far; nested art keeps the `:where()` default and
   its own class still wins. */
.prose > hr      { margin: 2.5em 0; }
.prose > img     { margin: 1.6em 0; }
.prose > details { margin: 0.5em 0; }
.prose > details + details { margin-top: -1px; }

/* --- skip the work for blocks that are off screen -------------------------
   These pages get long: the sayings page is 223 paragraphs and roughly 14,500
   pixels tall, and without this the browser lays out and paints every one of
   them on load and again on every reflow, even though eighteen screens of it
   are nowhere near the viewport.

   `content-visibility: auto` lets it skip layout, paint and hit-testing for a
   block until that block is close to being scrolled into view.

   `contain-intrinsic-size: auto 120px` is what keeps the scrollbar honest. The
   `auto` keyword makes the browser remember each block's real height once it
   has been rendered, so scrolling back over it is exact; the 120px is only the
   first guess for a block never yet rendered. Without a size here, skipped
   blocks would measure zero and the page would collapse and jump.

   The 120px was checked against the real thing rather than guessed: on the
   sayings page it puts the initial document height within about 5% of the
   measured truth (31,855px against 30,215px), and that error shrinks to nothing
   as blocks render and their real sizes are remembered. Worth re-checking if
   the prose ever becomes image-heavy, where blocks are much taller.

   Applied to the prose children only — never to the container or the header,
   which are always on screen and whose containment would buy nothing.

   Modern Chrome and Safari also keep find-in-page and anchor links working
   through skipped content. Browsers without support ignore both properties and
   render exactly as before, so this needs no fallback.

   ONE TRAP, and it has already been sprung once. `content-visibility: auto`
   implies `contain: layout style paint`. Layout containment makes the element a
   containing block for `position: fixed` descendants, and paint containment
   clips them to its box — which, before first render, is the 120px guess. A
   block that pins something to the viewport must therefore opt out:

     content-visibility: visible;
     contain-intrinsic-size: none;

   styles/ranks.css does exactly that for its scroll-driven gutter.

   ─────────────────────────────────────────────────────────────────────────
   AND IT IS TURNED OFF, because it broke the bottom of every article.

   REPRODUCED, not theorised: load /en/gear/dueling-gloves/ and scroll to the
   end with the wheel. `document.scrollHeight` starts at 3869 and finishes at
   2947 — the page loses 922px WHILE YOU ARE READING IT. Chrome does not pull
   the scroll position back when the document shrinks under it, so the viewport
   ends up parked past the real end and you get 308px of bare starfield below
   the footer. That is the "empty space below the footer" bug.

   The mechanism is the estimate, not the feature. `contain-intrinsic-size:
   auto 120px` guesses 120px for every block it has not yet rendered, and this
   prose is nothing like 120px a block — paragraphs run 100–160, panels, zone
   accordions, tab groups and figure blocks run 300–700. So the initial height
   is inflated by hundreds of blocks' worth of over-estimate, and it deflates
   towards the truth as you scroll. The note above says the guess was checked
   on the SAYINGS page and lands within 5% — and that is exactly why it was
   never caught: sayings is 223 near-identical short paragraphs, the one page
   in the site where 120px is a good average. Everywhere else it is not.

   Tuning the number would move the bug rather than remove it: any single
   guess is wrong for a page whose blocks vary 6× in height, and being wrong
   in the other direction gives a page that GROWS while you read instead.

   So it goes. What it bought was layout and paint work on off-screen blocks;
   what it cost was a visibly broken page end on every article. If long-page
   performance needs revisiting, the honest version is per-construct intrinsic
   sizes (a `.panel` is not a `<p>`) measured from the built pages, plus a
   scroll-anchoring check — not one number for everything.

   Kept as a comment rather than deleted: the trap above is real and the next
   person to reach for `content-visibility` should read it first. */

/* The headings' opt-out went with it. It read "headings are cheap, and skipping
   them costs more than it saves: they are the things a reader scrubs the
   scrollbar looking for" — true, and moot now that nothing is skipped. Two of
   the three tags it named (h2, h3) had also stopped existing in prose when
   markdown depth moved onto the design's ramp, so it was half dead anyway.

   styles/ranks.css still carries its own opt-out with the same reasoning; it
   is now belt-and-braces rather than load-bearing, and harmless. */

/* ---------------------------------------------------------------------------
   ARTICLE HEADINGS — the tag IS the design's heading level

   A Figma layer called "H4" carries the "Heading/H4" style, so it is an <h4>
   wearing the H4 ramp. One name, one tag, one style; reading any of the three
   tells you the other two.

   This REPLACES an indirection that had grown here: the markup used h2/h3/h4
   and this file dressed them in the H4/H5/H6 ramps, so `.prose h4` was
   Heading/H6 and no rule said so except a comment. Every conversation about a
   heading had to carry the translation table with it, and the table is exactly
   the sort of thing that goes stale — it already had, describing "reassigned
   Figma type roles" that the frames no longer reassign.

   Across every page frame the design draws three levels and only three:

     h1   Heading/H1   48 Bold      the page title (the Hero renders it)
     h4   Heading/H4   23 SemiBold  a section of the article        ##
     h5   Heading/H5   17 Medium    a sub-section within it         ###

   h2 and h3 are therefore absent from prose ON PURPOSE. They are not styled
   below because nothing renders them; a rule for them would be a guess at a
   design that does not exist. build/markdown.js maps the hashes.
   ------------------------------------------------------------------------- */
/* THE SIZES ARE NOT HERE ANY MORE — they are the shared ramp in common.css.

   `h1 … h6` there give family, size, weight, tracking and colour to every
   heading on every page, so this file says only what is TRUE OF PROSE AND
   NOTHING ELSE: how far a heading sits from the paragraph above and below it,
   and how its lines wrap. Restating the ramp here made it a second definition,
   and the landing then made a third — see the note in common.css. */
/* SPLIT IN TWO, BY SPECIFICITY, AND THE SPLIT IS THE POINT.

   `text-wrap` is a container DEFAULT: it describes how a heading looks, so it
   stays :where()-wrapped at (0,0,1) and any component that wants to wrap
   differently simply says so and wins.

   The 56 is not a default. It is the article's rhythm — how far a section
   heading sits from the block above it — so it has to outrank a component's
   bare `margin: 0`, which means one class. And because it outranks, it must
   reach ONLY the headings the rhythm actually governs: the direct children of
   .prose.

   Written as a descendant, `.prose h5` is (0,1,1) and beat `.timeline__title`,
   `.panel__title`, `.tier__title` and every other block title — all of which
   say `margin: 0` at (0,1,0) and are spaced by their own component's layout.
   Every step of every timeline opened a 56px hole under its number. That is
   the bug CLAUDE.md's cascade section is about, reintroduced by a specificity
   bump made for an unrelated reason — the same way it happened to
   `.card__photo` an hour earlier. An exception to the rhythm is child-scoped;
   the rhythm never reaches further than a child, so neither may its exceptions. */
:where(.prose) h4,
:where(.prose) h5,
:where(.prose) h6 { text-wrap: balance; }

/* 56 above, because an h4 is what opens a section. Nothing below: the generic
   `* + *` gives the first paragraph its 32, which is the comp's heading-to-body
   gap. Stating a bottom margin here as well would add to it — they do not
   collapse, they are the same edge measured twice. */
.prose > h4,
.prose > h5,
.prose > h6 { margin-top: 56px; }

/* The one type value prose overrides: h6 is the cyan sub-label here, where the
   ramp's default heading colour is --ink. Nothing draws an h6 on any frame, so
   this is prose's own choice rather than a measurement. */
:where(.prose) h6 { color: var(--cyan-soft); }

/* A heading immediately after another heading has no prose between them, so
   the large top margin would open a hole. */
.prose > h4 + h5,
.prose > h5 + h6 { margin-top: 0.9em; }

/* --- an eyebrow labelling the heading below it ------------------------------
   `:::eyebrow label="ethical principle"` above a `####`. The two are ONE unit:
   a kicker and the heading it kicks. Left to the ordinary rhythm they are two
   blocks — the label takes 32 and the heading takes its 56 — which put 56px of
   air between a label and the words it labels, and read as a stray fragment
   above an unrelated title.

   So the section break MOVES TO THE LABEL: 56 above the eyebrow, 12 between it
   and the heading. Nothing here is invented. 12 is the gap Figma draws between
   this exact pair in the Hero component's Header stack ("VERTICAL, gap 12 —
   the eyebrow and the title", modules/hero/hero.css), and the same 12 the
   Summer camps note uses over its own eyebrow. Two independent readings of the
   same relationship agreeing is as close to a measurement as this page gets —
   it carries no `design:` key of its own.

   CHILD-SCOPED, per the carve-out in CLAUDE.md. `.prose > .eyebrow + h4` is
   (0,2,1) and beats `.prose > h4` (0,1,1); `.prose > .eyebrow` is (0,2,0) and
   beats `.prose > * + *` (0,1,0). Written as descendants they would reach past
   a direct child into every component that renders an eyebrow — the rank
   labels, the note's field label — which is the bug the whole section warns
   about, and it has been reintroduced sideways twice already. */
.prose > .eyebrow { margin-top: 56px; }
.prose > .eyebrow + h4,
.prose > .eyebrow + h5,
.prose > .eyebrow + h6 { margin-top: 12px; }

/* An eyebrow opening the article has nothing to be separated from. */
.prose > .eyebrow:first-child { margin-top: 0; }

/* MEASURED from the Warm-up comp (218:2413), which is the content page's own
   body copy: Exo 2 Light 16 on 26 with +1px, #a7b9cb. It had been inheriting
   body's clamp(16,18)/1.7 and --body's #9fb2c6 — close, and wrong. */
:where(.prose) p {
  margin-bottom: 20px;
  color: var(--body);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

/* AFTER the rule above, and that is load-bearing. Both selectors are (0,0,1),
   so the later one wins — placed first, this was overridden by the very
   declaration it exists to cancel, and p → p measured 20 instead of 18.

   20 is the NESTED default: a paragraph inside a panel, a zone, a card, where
   no adjacency rule reaches. At the top level the article's rhythm owns the
   spacing — 18 between paragraphs, 32 to anything else — so a bottom margin
   there only collapses against it and wins whenever it is larger. */
:where(.prose) > p { margin-bottom: 0; }

:where(.prose) strong { color: var(--ink); font-weight: 600; }
:where(.prose) em     { font-style: italic; }

:where(.prose) a {
  color: var(--cyan);
  text-decoration: underline;
  text-decoration-color: rgba(0, 212, 255, 0.4);
  text-underline-offset: 0.18em;
  transition: color 120ms ease-out, text-decoration-color 120ms ease-out;
}

:where(.prose) a:hover {
  color: var(--cyan-soft);
  text-decoration-color: currentColor;
}

/* Focus rings are on :focus-visible only, so a mouse click doesn't leave a
   ring, but keyboard navigation always shows one. */
:where(.prose) a:focus-visible,
:where(.prose) summary:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
  border-radius: 1px;
}

@media (prefers-reduced-motion: reduce) {
  .prose a { transition: none; }
}

/* --- lists --------------------------------------------------------------- */
/* Figma component set `li` (772:27169).

   Keep native <ul>, <ol> and <li> elements: list semantics, start/value
   handling and screen-reader item counts should not be rebuilt in divs. The
   visible marker is the component's own presentation layered over those
   semantics. Lists belonging to other components (`.timeline`, `.options`,
   `.kit`, …) carry a class and are deliberately excluded here.

   Every Figma `ul`/`ol` that instantiates this component is a vertical stack
   with an 8px gap and no padding. */
.prose :where(ul, ol):is(:not([class]), .list) {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0;
  list-style: none;
}

/* The rule that used to give a top-level list its 20px tail is gone: `> * + *`
   spaces the block AFTER the list instead, which is the same gap without the
   `:not(:last-child)` dance that kept a final list from growing one. */

:where(.prose) :where(ul, ol):is(:not([class]), .list) > li {
  position: relative;
  min-height: 26px;
  margin: 0;
  color: var(--body);
  font-family: var(--body-paragraph-family);
  font-size: var(--body-paragraph-size);
  font-weight: var(--body-paragraph-weight);
  line-height: var(--body-paragraph-line);
  letter-spacing: var(--body-paragraph-tracking);
}

/* Type=for UL — a 16px icon slot, 10px to Paragraph copy. The downloaded SVG
   is the exact arrow-right glyph used by the component, centred inside the
   slot rather than redrawn as a text character. */
:where(.prose) ul:is(:not([class]), .list) > li { padding-inline-start: 26px; }

:where(.prose) ul:is(:not([class]), .list) > li::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 16px;
  height: 26px;
  background: url('/assets/ui/li-arrow.svg') center / 11.6px 8.60002px no-repeat;
}

/* Icon=x — the second Figma `li` variant. It keeps the same 16px icon slot
   and text geometry as the arrow variant; only the component glyph changes. */
:where(.prose) ul.list--icon-x > li::before {
  background-image: url('/assets/ui/li-x.svg');
  background-size: 16px 16px;
}

/* Type=for OL big — Body/Label 13 in a 17px slot, followed by a 10px gap.
   `counter(list-item)` retains the browser's real ordered-list counter, so an
   author can still use `start` or an explicit item value without putting the
   number into translated copy. */
:where(.prose) ol:not([class]) > li { padding-inline-start: 27px; }

:where(.prose) ol:not([class]) > li::before {
  content: counter(list-item, decimal-leading-zero);
  position: absolute;
  inset-block-start: 6px;
  inset-inline-start: 0;
  width: 17px;
  color: var(--amber);
  font-family: var(--body-label-family);
  font-size: var(--body-label-size);
  font-weight: var(--body-label-weight);
  line-height: var(--body-label-line);
  letter-spacing: var(--body-label-tracking);
  text-transform: var(--body-label-case);
  white-space: nowrap;
}

/* Type=for OL small — every current instance is inside a panel (Basic
   defenses and Pair exercises): Label Small 11, Paragraph Small 14, 14px
   marker slot and 14px gap. */
:where(.prose) .panel ol:not([class]) > li {
  min-height: 24px;
  padding-inline-start: 28px;
  font-family: var(--body-paragraph-small-family);
  font-size: var(--body-paragraph-small-size);
  font-weight: var(--body-paragraph-small-weight);
  line-height: var(--body-paragraph-small-line);
  letter-spacing: var(--body-paragraph-small-tracking);
}

:where(.prose) .panel ol:not([class]) > li::before {
  inset-block-start: 7px;
  width: 14px;
  font-family: var(--body-label-small-family);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  line-height: var(--body-label-small-line);
  letter-spacing: var(--body-label-small-tracking);
}

/* One level of nesting is supported by the converter and no more. */
:where(.prose) :where(ul, ol):is(:not([class]), .list) > li > :where(ul, ol):not([class]) {
  margin-top: 8px;
}

/* --- blockquote ---------------------------------------------------------- */
/* The component itself lives in common.css — it is shared, not a prose style.
   All this adds is the rhythm it needs between paragraphs.

   WAS WRONG BEFORE: this file styled quotes as 24px Tektur Medium with an amber
   rule, which is the design system's Display/Quote — a different component. The
   comp's blockquote (229:9641) is Exo 2 Light 14 on a tinted panel. */
/* The comp stacks these in a 20px-gap column, the same rhythm as everything
   else in `.prose > *`. This rule used to say `1.8em 0`, which on a 14px box
   is 25.2px — close enough to look deliberate and wrong on measurement. */
/* Nested (inside a zone, a panel) it takes the container's gap; at the top
   level the rhythm rules space it. Either way it carries none of its own. */
/* Its margin is the article rhythm's, not its own — see the rhythm block. */

/* THE QUOTE'S TYPE IS SET ON THE BOX AND THE TEXT IS IN A <p>, SO THIS RULE IS
   WHAT ACTUALLY DECIDES HOW A QUOTE READS.

   `.blockquote` in common.css is (0,1,0). `.prose p` is (0,1,1) — one more
   ELEMENT, so it wins on every quote's paragraph, and it sets colour, size,
   line-height and letter-spacing. The result: the box was drawn to the comp
   exactly — right rule, right tint, right padding — while the words inside it
   rendered #a7b9cb at 16/26/1px instead of the comp's Exo 2 Light 14/22/1.2 in
   the variant's own colour. An amber quote came out grey.

   `inherit` rather than repeating the values, so the box stays the single place
   the component is specified and the two can never disagree.

   Specificity here is (0,2,1) — one class more than `.prose p`, which is what
   makes it stick. Do not weaken this selector.

   The margin lines are a separate fight: `.prose p` sets a bottom margin, and
   `.blockquote p { margin: 0 }` in common.css has the SAME specificity, so the
   later file won and every quote carried a paragraph's padding inside its box. */
:where(.prose) .blockquote p {
  margin: 0;
  color: inherit;
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  letter-spacing: inherit;
}
:where(.prose) .blockquote p + p { margin-top: 0.9em; }

/* --- rule ---------------------------------------------------------------- */
:where(.prose) hr {
  margin: 2.5em 0;
  border: 0;
  border-top: 1px solid var(--rule);
}

/* --- images -------------------------------------------------------------- */
:where(.prose) img {
  display: block;
  max-width: 100%;
  height: auto;          /* with max-width, prevents aspect-ratio distortion */
  margin: 1.6em 0;
  border-radius: 2px;
}

/* ---------------------------------------------------------------------------
   :::zones — the Warm-up comp's exercise columns

   Figma "Warm-up" (218:2413), sections 02 and 03. Three equal columns filling
   the 880 measure, each a labelled header over an accordion of <details>.

     row        HORIZONTAL; gap 40 in section 02, 20 in section 03 — the comp
                really does draw two, which is why :::zones takes a `gap`
     column     267 (at gap 40) or 280 (at gap 20); both are 880 ÷ 3 with the
                gaps taken out, so the columns are 1fr and the gap decides
     header     VERTICAL gap 6, padding 16 / 0 / 24 / 27
     meta       Tektur Medium 11 / +1.7 / upper / #7dd3fc
     title      Heading/H5 — Tektur Medium 16 / +2 / #ebf2f8

   The 27px left padding on the header is the comp's, and it is what sets the
   header in from the accordion beneath it rather than aligning the two.
   ------------------------------------------------------------------------- */
/* NO MARGIN DECLARED HERE, DELIBERATELY. A component that says `margin: 0`
   says it with one class — (0,1,0) — and the article's rhythm rule
   `:where(.prose) > * + *` is (0,0,0), because `*` and `+` count for nothing.
   So every component that zeroed its own margin also cancelled the 32 the
   article wanted before it, and the blocks sat flush against the paragraph
   above. The container already zeroes its top-level children; anything nested
   is zeroed by a `:where()`-wrapped rule that the rhythm can outrank. */
:where(.prose) .zones {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--zones-gap, 20px);
  align-items: start;
}

.zone { display: flex; flex-direction: column; }

.zone__header {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px 0 24px 27px;
}

.zone__meta {
  margin: 0;
  color: var(--cyan-soft);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}

:where(.prose) .zone__title {
  margin: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: var(--h5-size);
  font-weight: var(--h5-weight);
  letter-spacing: var(--h5-tracking);
  line-height: normal;
  text-transform: uppercase;
}

/* A note under a column — the comp puts the spine's Attention blockquote
   inside its column, 24px below the last item. */
:where(.prose) .zone__items > .blockquote { margin-top: 24px; }

/* The accordion inside a zone is the SAME <details> the prose uses elsewhere,
   restyled for a narrow column. Measured from "Exercise disclosure" (217:4387)
   and its instances:

     item       50px collapsed; expanded is 12 top / 12 gap / 16 bottom
     rule       1px #7dd3fc at 18% along the BOTTOM — not the top
     header     26px high, horizontal gap 12: marker, label
     marker     Tektur Medium 11 / +1.7 / #7dd3fc   ＋ collapsed, － expanded
     label      Exo 2 Light 14 / 24 / +1.2
                #a7b9cb collapsed · #eb8b2d hover · #ebf2f8 expanded
     body       Exo 2 Light 14 / 24 / +1.2 / #a7b9cb, 12 below the header */
:where(.prose) .zone__items details {
  margin: 0;
  border: 0;
  box-shadow: inset 0 -1px rgba(125, 211, 252, 0.18);
  border-radius: 0;
  background: none;
}

:where(.prose) .zone__items details + details { margin-top: 0; }

:where(.prose) .zone__items summary {
  /* Figma's collapsed variant is exactly 50px: 12 + 26 + 12. The explicit
     minimum leaves a 26px content box while preserving Paragraph Small's
     24px line-height. */
  align-items: center;
  gap: 12px;
  min-height: 50px;
  padding: 12px 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

/* THE MARKER IS `.prose summary::before` NOW — there is nothing to override.

   It used to be a typographic ＋ / － here, set in Tektur at 11px, because the
   boxed disclosure below owned a rotating CSS triangle and the two components
   drew different things. Both accordions now instance the same "btn icon"
   plus/minus, so the marker is declared once and this block only differs where
   the components differ: the gap (12 against 16) and the row's padding.

   That deletion also retires a specificity fight documented at length below —
   the ＋ had to out-rank `.prose details[open] > summary::before` or the
   rotation tipped the － on its side. Nothing rotates any more. */

/* Expanded is a different auto-layout distribution: 12px above the 26px
   header, then a separate 12px gap before the body and 16px below it. Keeping
   that gap outside the summary prevents it being counted as header padding. */
:where(.prose) .zone__items details[open] {
  padding-bottom: 16px;
}

:where(.prose) .zone__items details[open] > summary {
  min-height: 38px;
  margin-bottom: 12px;
  padding-bottom: 0;
  color: var(--ink-soft);
}
/* EVERY RULE IN THIS BLOCK CARRIES `.prose`.

   `.prose details` and `.prose summary` are the boxed prose disclosure — same
   specificity, and they sit LOWER in this file, so without the extra class they
   win outright: the zone's rows came out as bordered, filled, rounded cards
   instead of the component's flat rows. That is not a nicety; it is the whole
   difference between the two designs. The MARKER is now deliberately shared,
   which is why nothing here restates it. */

/* State=hover paints the label amber — a real variant of the component set,
   not an emphasis on one item. The comp shows it by leaving one row in that
   state.

   THIS SAID `var(--amber-warm)` AND HAD NEVER WORKED ON THIS PAGE.
   --amber-warm is declared in styles/landing.css and nowhere else. A content
   page does not load that file, so the variable was undefined, the declaration
   was invalid at computed-value time, and the label kept its resting colour on
   every hover. --amber is the token that exists here, and it holds the same
   #eb8b2d.

   It also froze the MARKER, which is how it was finally caught. With `color`
   invalid, nothing on the <summary> actually changed on hover — so the element
   was never restyled, its ::before was never revisited, and the amber
   --marker-ink it inherits never reached the glyph. The FAQ escaped this only
   because its own rule carried a `var(--amber-warm, var(--amber))` fallback.
   A dead declaration is not inert: this one silently pinned a pseudo-element
   to its old colour. */
:where(.prose) .zone__items summary:hover,
:where(.prose) .zone__items summary:focus-visible {
  color: var(--amber);
}

:where(.prose) .zone__items details > :not(summary) {
  margin: 0;
  color: var(--body);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

/* The component's body is flush inside the wrapper; spacing belongs to the
   expanded item's 12px gap and 16px bottom padding above. Paragraph defaults
   must be reset because their collapsing margins otherwise add 14px above and
   20px below the text. */
:where(.prose) .zone__items .disclosure__body { margin: 0; }
:where(.prose) .zone__items .disclosure__body > p { margin: 0; }

/* Narrow viewports: three 267px columns need 880. Derived, not drawn. */
@media (max-width: 860px) {
  .prose .zones { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .prose .zones { grid-template-columns: minmax(0, 1fr); }
  .zone__header { padding-left: 0; }
}

/* ---------------------------------------------------------------------------
   Opening and closing

   The height animation is scripts/disclosure.js's — a <details> cannot be
   transitioned open by CSS, because its body is not rendered until the
   attribute flips and there is no height to start from. See that file.

   What IS css here is everything that can be: the marker and the label change
   colour over the same 260ms, so the row reads as one movement rather than an
   instant recolour followed by a slide.
   ------------------------------------------------------------------------- */
:where(.prose) summary { transition: color 260ms var(--ease-out); }

/* The marker is painted with background-color and dimmed with opacity now, so
   `color` no longer moves it — this used to transition `color` on both and the
   glyph would have snapped from blue to amber while the label eased. The two
   properties here are exactly the two the component's states change. */
:where(.prose) summary::before {
  transition: background-color 260ms var(--ease-out),
              opacity 260ms var(--ease-out);
}

/* The wrapper the script measures. `overflow: hidden` and the height are set
   inline while it animates and removed afterwards, so nothing here fights it —
   this only makes sure a partially-collapsed body never spills. */
:where(.prose) .disclosure__body { min-height: 0; }

/* THE SAME TRAP AS THE BLOCKQUOTE, AND IT SURVIVED THE FIRST SWEEP.

   Both disclosure rules — the zone one above and the generic one below — set
   their type on the WRAPPER (`details > :not(summary)`), but the words are in a
   <p> inside it, and `:where(.prose) p` matches that <p> directly. A direct
   match beats an inherited value no matter how the specificity falls, so the
   accordion bodies rendered at 16/26/1px while the comp's `description` is
   Exo 2 Light 14/22/+1.2 (Figma "Exercise disclosure", all three variants).

   Found by reading getComputedStyle in a real browser. No amount of resolving
   the cascade catches it, because nothing here is a specificity conflict — the
   wrapper rule and the <p> rule are simply about different elements, and both
   win the thing they match. Inheritance is not a cascade question.

   Scoped to `.disclosure__body` so it serves both kinds of disclosure and
   neither has to restate its own type. */
:where(.prose) .disclosure__body > p {
  color: inherit;
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  letter-spacing: inherit;
}

@media (prefers-reduced-motion: reduce) {
  .prose summary,
  :where(.prose) summary::before { transition: none; }
}

/* --- details / summary ---------------------------------------------------
   Written by hand in the content, so the styling has to work with markup this
   file does not control: a <summary> followed by loose <p> children.
   ------------------------------------------------------------------------- */
:where(.prose) details {
  margin: 0.5em 0;
  border: 1px solid var(--rule);
  border-radius: 2px;
  background: var(--panel);
}

:where(.prose) details + details { margin-top: -1px; }   /* collapse adjacent borders */

:where(.prose) summary {
  padding: 12px 16px;
  color: var(--ink);
  font-family: var(--display);
  font-size: 0.95em;
  font-weight: 500;
  cursor: pointer;
  list-style: none;
  display: flex;
  /* The marker is a 16 × 16 box now, not a baseline-sat triangle, and the comp
     centres it against the FIRST LINE of the label — "Accordion - big"
     (628:7041): a 22-tall header row with the 16 icon at y3. flex-start plus
     the 1lh trick on ::before does that for a label of any height; `baseline`
     would hang the box off the text baseline and `center` would drift it to
     the middle of a wrapped question. */
  align-items: flex-start;
  gap: 16px;                     /* comp: header gap 16 */
}

/* The default disclosure triangle is replaced with one that can be coloured
   and swapped. Safari needs the -webkit- pseudo-element removed explicitly. */
:where(.prose) summary::-webkit-details-marker { display: none; }

/* ---------------------------------------------------------------------------
   The marker — Figma "btn icon" Icon=plus / Icon=minus (see --icon-plus above)

   WAS A ROTATING TRIANGLE. The component now carries a plus that becomes a
   minus, so there is nothing to rotate: the glyph itself changes. The
   transition went with the rotation — a mask-image swap cannot tween, and
   pretending otherwise with a cross-fade would be inventing motion the design
   does not specify.

   COLOUR AND OPACITY ARE THE COMPONENT'S STATES, transcribed:
     collapsed  #40d1ff at 80%      (Text/Blue - pale, instance opacity 0.8)
     hover      #eb8b2d at 100%     (Text/Orange)
     expanded   #40d1ff at 100%
   They are custom properties on the <summary> so the two rules that change
   state set one value each, instead of every state restating the whole marker.

   `height: 1lh` is the first-line centring: the box is exactly one line tall
   at the top of the flex row, and the 16px mask is centred inside it. The
   16px fallback before it is for browsers without `lh` — there the icon sits
   3px high rather than centred, which is a nudge, not a break.
   ------------------------------------------------------------------------- */
:where(.prose) summary {
  --marker-ink: var(--cyan-bright);
  --marker-alpha: 0.8;
}

:where(.prose) summary::before {
  content: "";
  flex: none;
  width: 16px;
  height: 16px;
  height: 1lh;
  background-color: var(--marker-ink);
  opacity: var(--marker-alpha);
  -webkit-mask: var(--icon-plus) no-repeat center / 16px 16px;
  mask: var(--icon-plus) no-repeat center / 16px 16px;
}

:where(.prose) details[open] > summary {
  --marker-alpha: 1;
}

:where(.prose) details[open] > summary::before {
  -webkit-mask-image: var(--icon-minus);
  mask-image: var(--icon-minus);
}

/* State=hover — the marker and the label both go amber. The component shows
   it by leaving one row in that state.

   THE ::before IS REPAINTED DIRECTLY, not only through --marker-ink.
   Changing an inherited custom property on the <summary> is supposed to reach
   its pseudo-element, and usually does — but only because some real property
   on the summary changes too and forces the restyle. When the zone's label
   rule was invalid (see the --amber-warm note above) nothing on the summary
   changed, the pseudo was never revisited, and the glyph stayed blue under a
   --marker-ink that had already resolved to amber. Setting the paint on the
   ::before itself removes that dependency: the rule that changes the colour is
   the rule that matches the thing being coloured. The custom properties stay
   because they are how a state is DECLARED once for both accordions; this is
   what makes the declaration land. */
:where(.prose) summary:hover,
:where(.prose) summary:focus-visible {
  --marker-ink: var(--amber);
  --marker-alpha: 1;
}

:where(.prose) summary:hover::before,
:where(.prose) summary:focus-visible::before {
  background-color: var(--amber);
  opacity: 1;
}

:where(.prose) details > :not(summary) {
  margin: 0 16px 12px;
  font-size: 0.95em;
}

:where(.prose) details[open] > summary { color: var(--cyan-soft); }

/* ---------------------------------------------------------------------------
   Narrow viewports
   ------------------------------------------------------------------------- */
@media (max-width: 600px) {
  /* THE PANEL IS NOT EDGE TO EDGE. It was — `.content-page` dropped its gutter
     to 0 here and the panel gave up its border and corner to match, on the
     reasoning that a border at that width "would only draw a box round the
     screen". The grid frame disagrees: at 320–509 the margin is 12 and the
     panel is `viewport − 24`, so it is a card with a visible edge, inset like
     everything else. Read 2026-09-09 from `grid spec`.

     With the gutter back, the two patches that compensated for its absence are
     gone too: the panel keeps its border and radius, and .page__hero no longer
     needs a hardcoded 20px to keep the title off the screen edge. */
  .prose .blockquote { padding-inline: 16px; }
  .prose details > :not(summary) { margin-inline: 14px; }
  .prose summary { padding: 11px 14px; }
}

/* ===========================================================================
   DIALECT BLOCKS — :::tabs / :::tab / :::facts / :::cards
   Figma "Dueling approach" (258:3843) and the component sets it instantiates:
   "Selected weapon content" (314:4880), "equipment requirement" (371:17927),
   "Line art · Training weapon" (289:4294), "Line art · Protective gear"
   (292:4267).

   These live here, not in a page stylesheet, because they are DIALECT
   constructs: any page whose markdown uses them gets them. build/markdown.js
   emits the markup; the two must change together.
   ========================================================================= */

/* FOUR CUSTOM PROPERTIES WERE REMOVED HERE, all of them dead.

   `--tab-h: 113px` and `--panel-edge` were read only by the old `.tab` base
   below, whose shape both variants overrode in full — so no tab on the site
   was ever 113px tall or carried that bottom edge. `--tab-art` and `--card-bg`
   had no reader at all. They survived because the rule that consumed them
   still existed; it was the rule that was dead, not the syntax.

   (The removed comment recorded that --panel-edge and --card-bg had been
   repointed at the token collection to stop colour drift. True, and now moot:
   nothing reads them.) */
.tabs {
  /* Fast enough to feel like a response, slow enough to read as a turn rather
     than a cut. The ease overshoots very slightly on the way out, which is what
     stops a 3D flip looking mechanical. */
  --flip: 420ms;
  --ease-flip: cubic-bezier(.2, .8, .25, 1);

  margin-block: 24px;
}

/* --- the tab strip ------------------------------------------------------- */
/* Equal columns with no gap: the comp butts the options against each other and
   marks the current one with a tinted panel that reads as one shape with the
   content below it. */
/* ONE STRIP, BOTH VARIANTS. This was two rules — `.tabs--art .tabs__list` and
   `.tabs--index .tabs__list` — each restating `position: relative` and its own
   margin, and they had drifted: 5px against 8px, and only the art one carried
   the baseline rule. So the same component looked like two components, and the
   indexed selector on Basic defenses sat on nothing.

   The row owns the quiet baseline; current and hover states draw only their
   own one-pixel indicator over it. Five pixels separate the strip from the
   selected-content surface. Nothing about either number is per-variant, so
   neither is stated per-variant. */
.tabs__list {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  margin-bottom: 5px;
  box-shadow: inset 0 -1px rgba(172, 220, 229, 0.13);
}

/* THE TAB, ONCE. This rule used to describe a shape nothing on the site had:
   113px tall, 12px padding, a 1px transparent border with a coloured bottom
   edge, 4px top corners, amber label. Both variants then overrode every one of
   those declarations with the SAME replacement — border 0, radius 0,
   transparent background, dim label, 12px gap, 16/16/20 padding, and an
   identical 160ms transition — written out twice.

   So the base was stale (an earlier design) and the "variants" were two copies
   of its successor. What actually differs between an art tab and an indexed
   tab is its SIZE, and nothing else; that is all the variant rules say now.

   `letter-spacing` is the token rather than the indexed variant's literal
   `2px`. They are the same number today — that is exactly the drift risk. */
.tab {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 16px 16px 20px;
  overflow: hidden;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--body-dim);
  font-family: var(--display);
  font-size: var(--body-label-size);
  font-weight: var(--body-label-weight);
  letter-spacing: var(--body-label-tracking);
  line-height: normal;
  text-align: center;
  text-transform: uppercase;
  cursor: pointer;
  transition:
    color 160ms var(--ease-out),
    background-color 160ms var(--ease-out);
}

/* The art tab is taller and has a floor width, because it holds line art. */
.tabs--art .tab { height: 129px; min-width: 199px; }

/* Figma keeps the indicator and its light as separate named layers. They are
   aria-hidden spans in the markup: inspectable, inert, and independent of the
   tab's accessible label. */
.tab__active-indicator,
.tab__glow {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  pointer-events: none;
  opacity: 0;
}
.tab__active-indicator { bottom: 0; z-index: 2; }
.tab__glow { z-index: 1; }
.tab__glow--top { top: 0; }
.tab__glow--bottom { bottom: 0; }

.tabs .tab:hover,
.tabs .tab:focus-visible {
  color: var(--amber);
}
.tabs .tab:focus-visible { outline: none; }
.tabs .tab:hover .tab__active-indicator,
.tabs .tab:focus-visible .tab__active-indicator {
  background: #eb8b2d;
  opacity: 1;
}

/* Both tab shapes now share one state language. Their geometry stays in the
   art/index rules; current, hover, indicator and glow are defined once here. */
.tabs .tab[aria-selected="true"] {
  background: rgba(64, 209, 255, 0.02);
  color: var(--ink-soft);
}
.tabs .tab[aria-selected="true"] .tab__glow--top {
  background: rgba(0, 212, 255, 0.2);
  box-shadow:
    0 2px 34px #00d4ff,
    0 0 13px rgba(0, 212, 255, 0.8);
  opacity: 1;
}
.tabs .tab[aria-selected="true"] .tab__active-indicator {
  background: #00d4ff;
  opacity: 1;
}
.tabs .tab[aria-selected="true"] .tab__glow--bottom {
  background: rgba(0, 212, 255, 0.2);
  box-shadow:
    0 -2px 34px #00d4ff,
    0 0 13px rgba(0, 212, 255, 0.8);
  opacity: 1;
}

/* The line art is exported for every state; CSS only swaps exact Figma assets. */
.tab__art {
  width: 100%;
  max-width: 175px;
  aspect-ratio: 175 / 64;
  background: var(--art-default) center / contain no-repeat;
}
.tab:hover .tab__art,
.tab:focus-visible .tab__art { background-image: var(--art-hover); }
.tab[aria-selected="true"] .tab__art { background-image: var(--art-current); }

.tab__label { white-space: nowrap; }

/* --- the indexed tab ------------------------------------------------------
   Figma "[Website Page] Basic defenses" (469:4626), "Weapon selector". The
   refreshed selector now shares the art tabs' indicator/glow state language,
   but keeps its own compact shape: index above a wrapping label, 96px high,
   16/16/20 padding and a 12px gap. Both text roles are centred.              */
/* The indexed tab is shorter, and that is the whole of its difference. Its
   `min-height` is not redundant with `height`: the label wraps to two lines
   here (see `.tabs--index .tab__label`), and without the floor a one-line
   label would let the row collapse below the drawn 96. */
.tabs--index .tab { height: 96px; min-height: 96px; }
.tabs--index .tab:hover .tab__idx,
.tabs--index .tab:focus-visible .tab__idx { color: var(--amber); }
.tabs--index .tab[aria-selected="true"] .tab__idx { color: var(--ink-soft); }
.tabs--index .tab__idx {
  color: var(--body-dim);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  transition: color 160ms var(--ease-out);
}
.tabs--index .tab__label { white-space: normal; }

/* --- the panel ----------------------------------------------------------- */
/* Both refreshed tab shapes use the same selected-content surface: an 8px or
   5px gap separates it from the strip, all four corners are 4px, and a glow
   below the current/hovered cell supplies the visual connection. */
.tabs__panel {
  --surface-radius: 4px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 32px 24px;
  overflow: hidden;
  border: 0;
}
.tabs__panel[hidden] { display: none; }

/* A second pair of glow layers sits at the panel's top edge, directly below
   the current and hovered tabs. tabs.js supplies pixel positions so this stays
   exact for both the four-column weapon row and three-column sparring row. */
.tabs__panel-glow {
  position: absolute;
  top: 0;
  z-index: 1;
  height: 1px;
  pointer-events: none;
}
.tabs__panel-glow--current {
  left: var(--tab-current-left);
  width: var(--tab-current-width);
  background: rgba(0, 212, 255, 0.2);
  box-shadow:
    0 2px 34px #00d4ff,
    0 0 13px rgba(0, 212, 255, 0.8);
}
.tabs__panel-glow--hover {
  left: var(--tab-hover-left);
  width: var(--tab-hover-width);
  background: rgba(235, 139, 45, 0.2);
  box-shadow:
    0 2px 30px #e98a2d,
    0 0 13px #e98a2e;
  opacity: var(--tab-hover-opacity);
  transition:
    left 120ms var(--ease-out),
    width 120ms var(--ease-out),
    opacity 120ms var(--ease-out);
}

/* The Figma indexed panel holds one Body/Paragraph Small text run. In HTML it
   is several semantic paragraphs/list blocks, so 5px reproduces the style's
   paragraph spacing while each line stays 14/24 with +1.2px tracking. */
.tabs--index .tabs__panel { gap: 5px; }
.tabs--index .tabs__panel p,
.tabs--index .tabs__panel li {
  margin: 0;
  color: var(--body);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}
.tabs--index .tabs__panel ul,
.tabs--index .tabs__panel ol { margin-block: 0; }

/* --- facts --------------------------------------------------------------- */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  margin: 0;
  padding-block: 8px;
}
.facts__row { display: flex; flex-direction: column; gap: 6px; }
.facts dt {
  color: var(--cyan-bright);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  text-transform: uppercase;
}
.facts dd {
  margin: 0;
  color: var(--ink);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

/* --- cards --------------------------------------------------------------- */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
}

/* NO UNDERLINE. `.prose a` underlines every link in the article, and a card is
   an <a>; text-decoration on `.card` alone loses to it on specificity. */
:where(.prose) .card,
:where(.prose) .card:hover { text-decoration: none; }

/* The card is the perspective box; .card__inner is what turns. Perspective on
   the container rather than the transform keeps every card in the grid looking
   at the same vanishing point instead of each having its own. */
.card {
  perspective: 900px;
  min-height: 170px;
  text-decoration: none;
}

.card__inner {
  position: relative;
  display: block;
  height: 100%;
  min-height: inherit;
  transform-style: preserve-3d;
  transition: transform var(--flip) var(--ease-flip);
}

.card--flip:hover .card__inner,
.card--flip:focus-visible .card__inner { transform: rotateY(180deg); }

/* Both faces carry the card's own box, because after the turn the back IS the
   card. backface-visibility is what stops the front showing through in mirror. */
/* The comp's "equipment requirement" set: State=default is the dark tone,
   State=hover and =focus the highlighted one — which is exactly the front and
   back of this card. Both faces are .surface, so the radius came from 8 to the
   system's 4 and the front gained the stroke it was drawing transparent. */
.card__face {
  display: flex;
  flex-direction: column;
  gap: 8px;
  height: 100%;
  min-height: inherit;
  padding: 12px;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: border-color var(--flip) var(--ease-flip),
              background-color var(--flip) var(--ease-flip);
}

.card__face--back {
  position: absolute;
  inset: 0;
  padding: 12px;
  transform: rotateY(180deg);
}

/* State=default → State=hover in "equipment requirement": the fill goes dark
   to highlighted AND the stroke comes on. Both are tone properties, so hover
   is two declarations rather than a restated box. */
a.card:hover .card__face--front,
a.card:focus-visible .card__face--front {
  --surface-fill:   var(--panel-fill-highlighted);
  --surface-stroke: var(--panel-stroke-highlighted);
}

.card__art {
  width: 120px;
  height: 64px;
  background: var(--card-art) left center / contain no-repeat;
}
.card__copy { display: flex; flex-direction: column; gap: 6px; }
.card__label {
  color: var(--cyan-bright);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  /* The comp's label box is 14px on an 11px face — the font's own leading. Left
     to inherit, it took the prose line-height (~1.9) and stood 21px tall, which
     is where the compact card's 7px of extra height came from. */
  line-height: normal;
  text-transform: uppercase;
}
.card__detail {
  color: var(--ink);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}
.card__detail:empty { display: none; }

/* The back of the card. A real <img> so it is fetched with the page — the flip
   has to be instant, and a background-image would not exist until the first
   hover asked for it. */
.card__photo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* `:where(.prose) img` gives every image in the prose a 1.6em vertical margin
     and a 2px radius, which is right for an illustration between paragraphs and
     wrong for a photo that fills a card face. It used to be masked: that rule
     also carried `height: auto`, which outranked the `height: 100%` above and
     shrank the photo to its intrinsic size, so the margin had room to sit in.
     Lowering the prose rules' specificity let `height: 100%` win — and exposed
     the margin, which would then push the photo out of its own card.

     A component that opts out of a prose default has to say so. */
  margin: 0;
  border-radius: 0;
}
.card__go {
  position: absolute;
  top: 14px;
  right: 12px;
  z-index: 1;
  width: 16px;
  height: 16px;
  color: var(--cyan-bright);
  background: currentColor;
  -webkit-mask: url(/assets/dueling/icon-external.svg) center / contain no-repeat;
  mask: url(/assets/dueling/icon-external.svg) center / contain no-repeat;
}

/* COMPACT — a card that is art and a label, no detail line. In the comp these
   are the weapon-suitability cells (198×118 against the requirement card's
   198×170), and three things differ beyond the height: the bottom padding is
   20 rather than 12, the line art FILLS the width instead of sitting 120 wide
   at the left, and the label is centred with wider tracking.

   Keyed off the absent detail rather than a hand-written modifier: a card with
   nothing to say below its label IS this shape. */
.card--compact { min-height: 118px; }
/* NO BORDER, unlike the requirement card. The other faces carry a transparent
   1px edge so that gaining a real one on hover cannot resize them — but this
   card has no hover state, so the border is pure cost: it was eating 2px of
   width and leaving the art at 172 against the comp's 174. */
.card--compact .card__face { padding: 12px 12px 20px; border: 0; }
.card--compact .card__art {
  width: 100%;
  background-position: center;
}
.card--compact .card__label {
  text-align: center;
  letter-spacing: 1.7px;
}

/* The weapon-suitability cards are plain <div>s with no back face: the comp
   gives them no hover state at all, so nothing here reacts to one. */

/* On a phone the same component becomes a one-open accordion. The tablist is
   visually flattened into the parent grid, and generator-supplied order values
   place each panel directly after its controlling tab. The DOM and ARIA tab
   relationship stay intact, so desktop and assistive interaction do not need a
   duplicated copy of the content. */
@media (max-width: 700px) {
  .tabs { display: grid; }
  .tabs__list { display: contents; }
  .tab,
  .tabs__panel { order: var(--accordion-order); }
  /* Same consolidation as the desktop rules: `min-height`, `padding` and the
     hairline were stated once per variant with identical values, and the
     alignment flip only on the indexed one.

     The rule moves to each TAB here because `.tabs__list` is `display:
     contents` above — the strip has no box of its own in accordion mode, so
     its baseline cannot draw. Every tab carries its own bottom edge instead,
     which is also what an accordion wants: a line between each row and its
     panel, not one line above the whole stack. */
  .tab {
    height: auto;
    min-height: 64px;
    flex-direction: row;
    justify-content: flex-start;
    gap: 12px;
    padding: 18px 20px;
    text-align: left;
    box-shadow: inset 0 -1px rgba(172, 220, 229, 0.13);
  }
  /* The art tab's only accordion difference: release the 199px desktop floor. */
  .tabs--art .tab { min-width: 0; }
  .tabs--art .tab__art { display: none; }
  .tabs__panel {
    margin: 0;
    padding: 24px 20px;
  }
  /* The horizontal panel glows describe desktop column alignment. In the
     accordion, the selected tab's own indicator is the connecting edge. */
  .tabs__panel-glow { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* No flip, and no fade in its place: the card simply shows its back. A
     half-second rotation is exactly the kind of motion this preference is
     about. */
  .tab, .card__inner, .card__face { transition: none; }
  .card--flip:hover .card__inner,
  .card--flip:focus-visible .card__inner { transform: none; }
  .card--flip:hover .card__face--front,
  .card--flip:focus-visible .card__face--front { visibility: hidden; }
  .card--flip:hover .card__face--back,
  .card--flip:focus-visible .card__face--back { transform: none; }
}

/* ===========================================================================
   :::options / :::panel / :::scale / :::metrics
   Figma "Scoring rules" (391:18902).

   Four more dialect blocks, same rule as the first four: they are named for
   what they ARE, so a second page can use them without inheriting this page's
   vocabulary. Markup in build/markdown.js; change the two together.
   ========================================================================= */

/* --- options: numbered alternatives, one of which applies ---------------- */
/* QUALIFIED, like every other block rule on this page: `.options` is an <ol>,
   and `.prose ol` sets a list padding that outranks a bare class. Unqualified,
   it ate 24px and the row came out 272 wide against the comp's 280 — enough to
   wrap the longest label onto a second line. */
:where(.prose) .options { padding: 0; list-style: none; }

/* No margin. This is a prose block and the article places it — see the rhythm
   rules. A `margin: 0` here is (0,1,0) and, sitting later in the file than the
   rhythm, it won the tie and pinned the block flush against the paragraph
   above. */
.options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
  padding: 0;
  list-style: none;
}

.option {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px 20px;
}

/* The current option differs only in its cyan index. All three cards use the
   same neutral surface and Paragraph copy, matching the component variants in
   Figma; `current` still carries the explanatory suffix in the index itself. */

/* Body/Label Small, read off the style rather than a node: Tektur Medium 11 /
   letter-spacing 1.7 / the font's own line height / UPPER.

   It was Body/Label — Tektur Regular 13 / +2 — until the comp moved the top
   three framework cards down a step. The two styles differ in size, WEIGHT and
   tracking, so changing only the size would have left it Regular at 11px, which
   is a fourth thing that exists in neither the comp nor the design system.

   The uppercase is new here and is a fix rather than part of the move: BOTH
   styles specify textCase UPPER and this rule had never applied it, so
   "03 · our baseline" rendered lowercase against a comp that shouts it. Flagged
   in the handover — it is one line to drop if the lowercase was deliberate. */
.option__idx {
  color: var(--body-dim);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}
.option--current .option__idx { color: var(--cyan); }

.option__label {
  color: var(--body);
  font-family: var(--body-paragraph-family);
  font-size: var(--body-paragraph-size);
  font-weight: var(--body-paragraph-weight);
  line-height: var(--body-paragraph-line);
  letter-spacing: var(--body-paragraph-tracking);
}

/* --- panel: a bordered box around whatever it holds ---------------------- */
.panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: var(--panel-pad);
}

/* `.panel > .panel__label`, not `.panel__label` alone. The label IS a <p>, so
   the panel's own body-copy rule below matches it directly and — at equal
   specificity, later in the file — repainted it as body text. Caught by
   wiring-test §6. One extra class here puts the component back in front. */
/* Body/Label Small — Tektur Medium 11 / +1.7 / UPPER. Was Body/Label.
   Changed globally rather than per page because ALL THREE frames that draw a
   panel label agree: "why the uniform matters" (Uniform and personal
   equipment), "a good defense does five things" (Basic defenses) and "A HIT
   MUST QUALIFY" (Scoring rules) are each Body/Label Small at #F69638. Checked
   before editing — a shared class is only safe to move when every comp that
   uses it moved too. */
.panel > .panel__label {
  margin: 0;
  color: #f69638;
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}

/* The panel's own children keep the panel's rhythm rather than the prose's,
   or the 20px gap would compound with a paragraph's bottom margin. */
:where(.prose) .panel > p { margin: 0; }

/* --- panel tones ---------------------------------------------------------
   Same construct, two keys, both from the Martial arts landscape comp
   (532:5842): "Practice types" and "Cultural language". They differ from the
   default panel in padding (32 not 24) and in carrying a hairline, which is
   why they are variants rather than a restyle — dueling-rules draws the
   default and did not change.

   The text rules are stated rather than inherited. `:where(.prose) p` matches
   a <p> inside a panel DIRECTLY, so a colour set on the wrapper never reaches
   it — the same trap documented above the zone accordions. Every property the
   comp sets differently from the prose default is therefore repeated here.  */
/* Two panels that differ from the default in TONE and INK only, which is now
   all these rules say — the boxes come from .surface. */
.panel--bare,
.panel--important { gap: 26px; }

/* Exo 2 Light 16/26 +1px in both, differing only in ink: the comp's neutral
   body grey, and a warm tint for the amber box.

   `:where()` on the container, so these are (0,0,1) and any component inside a
   toned panel still wins its own properties — `.panel__label` is (0,1,0) and a
   plain `.panel--bare p` would outrank it, which is the same failure the
   wiring suite was written for. They still beat `:where(.prose) p`
   because that rule is also (0,0,1) and sits earlier in this file. */
/* The default panel's ink. FIRST, because every rule in this group resolves to
   (0,0,1) — :where() contributes nothing and :is(p, li) counts as one element —
   so the cascade falls to source order and the TONE has to have the last word.
   Written after, this repainted the important panel's warm grey as body copy. */
:where(.panel) > :is(p, li) { color: var(--body); }

:where(.panel--bare, .panel--important) :is(p, li) {
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

:where(.panel--bare) :is(p, li) { color: var(--body); }

/* THE IMPORTANT PANEL'S INK IS THE ORDINARY ONE. It was a hot salmon when the
   box carried an amber fill, then a warm grey when the fill went; it is now
   simply the panel body's grey, which is what three of the four important
   panels in the file draw (Basic defenses #a7b9cb, Scoring rules #a7b9cb,
   Uniform #b2c7db). Only the BORDER is saturated — that is the whole of what
   the tone means now, and there is no rule left here to say it.
   (The retired values are deliberately not written; a test greps for them.)

   Martial arts landscape still draws its panel's paragraph at the warm grey.
   One layer against three is a leftover, not a rule, so it follows the others
   rather than keeping a tone-wide exception alive for itself. */


/* --- glance: a band of numeralia above the container ----------------------
   Figma "At a glance" on Summer camps (519:5654): HORIZONTAL, gap 32, 96px
   under it, the same 1042 width as the container it precedes — and OUTSIDE
   that container, on the page background rather than the glass.

   Its labels run two lines at this width, so they are brighter than the
   component's own #7a8ca0: the comp overrides them to #d8dee5 on every one of
   the four instances. Recorded as an override rather than folded into the
   Orange variant, because the component set draws both variants at #7a8ca0.  */
.glance {
  display: flex;
  flex-wrap: wrap;
  gap: 32px;
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding-bottom: 96px;
}

.glance .numeralia { flex: 1 1 200px; }
.glance .numeralia__label { color: var(--body-dim); }

/* --- note: a labelled paragraph -------------------------------------------
   The Summer camps comp's field notes — the shared eyebrow in its "in view"
   state, 12px over ordinary prose. The eyebrow's own styling is common.css's;
   this only places it. */
.note {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The chronicle entry's heading — Figma "[Website Page] Summer camps", the
   `label` in each "Group · Entry heading + description".

   It is Heading/H5: Tektur Medium 16 / +2px / AUTO / sentence case. This
   component class keeps the role stable regardless of its semantic element.

   Margins are zeroed because `.note` is a flex column with its own 12px gap;
   the prose heading's 56px top margin would open a hole above every entry. */
.note__label {
  margin: 0;
  /* --ink-soft, NOT --body. The chronicle entry heading is Heading/H5 at
     Text/Heading-bright; it only looked like body copy because the old design
     gave it a grey (#a7b9cb). Sweeping colours by hex put it in the body group
     — a reminder that the role decides the token, never the value it used to
     have. The build suite caught this one. */
  color: var(--ink-soft);
  font-family: var(--display);
  font-size: var(--h5-size);
  font-weight: var(--h5-weight);
  letter-spacing: var(--h5-tracking);
  line-height: normal;
  text-transform: none;
}

/* Two entries are amber in the comp — "Conquer yourself" and "Poland · TODAY".
   The comp overrides the fill on those instances rather than using a variant,
   so it is authored per note in the markdown. */
.note__label--amber { color: var(--amber); }

:where(.note) p { margin: 0; }
:where(.note) p + p { margin-top: 12px; }

/* --- columns: labelled columns inside one bordered box -------------------
   Figma "First session checklist", Uniform and personal equipment (459:4466).
   HORIZONTAL, gap 28, pad 26/28, fill #0f1c26 at 72%, 1px #40d1ff at 22%.   */
/* The highlighted tone — the panel that offers something rather than states
   it. Padding and gap came to 24 with the variable sweep, from 26/28 and 28. */
.columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--panel-pad);
  padding: var(--panel-pad);
}

.column {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The comp heads the first column amber and the second cyan. Read as
   alternation — the two are a pair, "what you bring" and "what we lend", and
   nothing else in the file gives either word a colour of its own. */
/* Body/Label Small — Tektur Medium 11 / +1.7 / UPPER. Was Body/Label.
   The comp's "First session checklist" draws these as `h3` in Wear / Borrow.

   The comp gives the two columns different inks — Wear #F69638, Borrow
   #00D4FF — which the `.column:nth-of-type(even)` rule below already does.
   Only the type scale moves here. */
.column__title {
  margin: 0;
  color: #f69638;
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}

.column:nth-of-type(even) .column__title { color: var(--cyan); }

/* Names and Identity reverses the checklist's semantic colours: positive is
   cyan and caution is amber. The heading and arrow are Figma instance
   overrides, scoped to this one page in every language. The arrow uses the
   canonical shared glyph as a mask so only its colour changes. */
[data-page="library/names-and-identity"] .column:first-child .column__title {
  color: var(--cyan);
}

[data-page="library/names-and-identity"] .column:nth-child(2) .column__title {
  color: #f69638;
}

[data-page="library/names-and-identity"] .column:first-child ul:not([class]) > li::before {
  background: var(--cyan);
  -webkit-mask: url('/assets/ui/li-arrow.svg') center / 11.6px 8.60002px no-repeat;
  mask: url('/assets/ui/li-arrow.svg') center / 11.6px 8.60002px no-repeat;
}

:where(.column) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

/* --- kit: equipment that accumulates by rank -----------------------------
   Figma "Group · Rank stages" (461:7254). The stages are unboxed rows with
   56px of open space between them. There are no rulers or vertical padding
   around a row; its height comes entirely from its content.                */
.kit {
  display: flex;
  flex-direction: column;
  gap: 56px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.kit__tier {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr);
  gap: 32px;
  padding: 0;
}

/* The ornament is the rank ladder's, at half scale. `styles/ranks.css` draws
   it at emblem 96 / chevron 68 × 29 / first chevron 80px below the emblem's
   top / 13.3px step; the comp here halves every one of those. Kept as named
   properties so the relationship survives a later retune of either page. */
.kit {
  --kit-emblem:         48px;
  --kit-chevron-width:  34px;
  /* The component's nominal box is 68 × 26.26 but its art bleeds below that,
     so the whole-component export is 68 × 29 — size to the ART box or the SVG
     letterboxes itself and the chevron comes out narrow. 29 ÷ 2 = 14.5. */
  --kit-chevron-height: 14.5px;
  --kit-chevron-offset: 40px;
  --kit-chevron-step:   6.65px;
}

.tier__mark {
  position: relative;
  width: var(--kit-emblem);
  height: var(--kit-emblem);
  background: url('/assets/ranks/emblem-full.svg') no-repeat center /
              var(--kit-emblem) var(--kit-emblem);
}

/* The stack overlaps the emblem's lower edge and runs past the 48px mark into
   the row's 32px bottom padding, which is what the comp draws. */
.tier__chevron {
  position: absolute;
  left: 50%;
  top: calc(var(--kit-chevron-offset) + var(--i, 0) * var(--kit-chevron-step));
  width: var(--kit-chevron-width);
  height: var(--kit-chevron-height);
  transform: translateX(-50%);
}

.tier__chevron .chevron {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The component carries two state marks — the dark red bar and wing pair that
   the rank ladder reveals at ranks 06 and 07. They are VISIBLE IN THE FILE and
   hidden by whoever draws it; `styles/ranks.css` does the same with opacity so
   it can animate them in. Nothing here has states, so they are simply off.
   This is why the chevron is inlined rather than set as a background: an image
   is opaque to CSS and these marks cannot be reached. */
.tier__chevron .chevron__bar,
.tier__chevron .chevron__wing { display: none; }

.tier__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.tier__label {
  margin: 0;
  color: var(--cyan-bright);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}


:where(.tier__body) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

/* The comp writes the items as one text run with "→" between them, in Tektur
   against the Exo 2 copy. A real list carries the semantics; the arrow is the
   marker, in the display face the comp uses for it. */
:where(.tier__body) ul {
  margin: 0;
  padding-left: 26px;
  list-style: none;
}

:where(.tier__body) li {
  position: relative;
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

:where(.tier__body) li + li { margin-top: 2px; }

:where(.tier__body) li::before {
  content: '→';
  position: absolute;
  left: -26px;
  color: var(--cyan-bright);
  font-family: var(--display);
  font-weight: 400;
}

@media (max-width: 560px) {
  .kit__tier { grid-template-columns: 48px minmax(0, 1fr); gap: 20px; }
}

/* --- timeline: an ordered run of stages down a connecting line -----------
   Figma "Timeline step · Session", Training structure (551:6021). Row is
   HORIZONTAL gap 32: a 45px marker column and the content beside it. The
   connector is ABSOLUTE in the comp at x=22 — the marker's centre — running
   from the marker's bottom edge to the bottom of the step, and it is switched
   off on the last one, so the line ends at the final marker instead of
   trailing into the page.                                                   */
/* Shared Heading/H5 treatment for block titles: Tektur Medium 16 / AUTO / +2.
   .column__title uses the separate Body/Label Small treatment above. */
.panel__title,
.tier__title,
.timeline__title,
.pattern__title,
.semicircle__title,
.branch__title,
.trajectory__title,
.attack-subtitle,
.swing__title,
.impulse__title {
  margin: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: var(--h5-size);
  font-weight: var(--h5-weight);
  letter-spacing: var(--h5-tracking);
  line-height: normal;
}

/* THE SESSION TIMELINE IS A SECTION, NOT A BLOCK INSIDE ONE.

   Training structure's Content container (551:6021) holds exactly two children
   — "Group · Description + safety note" and "Section · Session timeline" — and
   the container's gap is 56. Every content comp uses that same 56 between its
   sections; the implementation supplies it through `:where(.prose) h4`, whose
   56px top margin opens each section on the pages that start one with a
   heading.

   This page starts its second section with the component itself, so nothing
   supplied it and the timeline sat 20 below the safety note — the generic
   `:where(.prose) > *` bottom margin — where the comp asks for 56.

   SCOPED TO THE ADJACENCY, NOT THE PAGE. `.blockquote + .timeline` at the top
   level of .prose occurs here and nowhere else; checked across all 69 built
   pages, not assumed. A timeline that follows a HEADING keeps the heading's
   20 — Footwork's does — because there the heading already opened the section
   and a second 56 would double it.

   .prose is display:block, so this collapses with the note's 20px bottom
   margin to exactly 56 rather than summing to 76. */
:where(.prose) > .blockquote + .timeline { margin-top: 56px; }

/* "Timeline step · Session" (616:15494) gained a Color variant. Blue binds
   Stroke - highlighted, Orange binds Stroke - important, on all three marks:
   the connector, the two marker brackets and the index. Two properties, set
   once on the list, rather than a colour repeated at six declarations. */
/* No margin. This is a prose block and the article places it — see the rhythm
   rules. A `margin: 0` here is (0,1,0) and, sitting later in the file than the
   rhythm, it won the tie and pinned the block flush against the paragraph
   above. */
.timeline {
  --timeline-line:  var(--panel-stroke-highlighted);
  --timeline-index: #2bb8e0;

  padding: 0;
  list-style: none;
}

.timeline--orange,
.timeline__step--orange {
  --timeline-line:  var(--panel-stroke-important);
  --timeline-index: #d06700;
}

/* A single stage may take the other colour — the Footwork comp runs four blue
   steps and an orange fifth. The same two properties, redeclared one level
   down: the connector, the brackets and the index all read the nearest
   ancestor, so the step overrides the run without any of them being restated. */
.timeline__step--blue {
  --timeline-line:  var(--panel-stroke-highlighted);
  --timeline-index: #2bb8e0;
}

.timeline__step {
  position: relative;
  display: grid;
  grid-template-columns: 45px minmax(0, 1fr);
  gap: 32px;
  margin: 0;
}

/* The comp's 1px rule. `bottom: 0` rather than a measured height: every step
   is a different height and the line simply reaches the next marker. */
.timeline__step::before {
  content: '';
  position: absolute;
  top: 45px;
  bottom: 0;
  left: 22px;
  width: 1px;
  background: var(--timeline-line);
}

.timeline__step:last-child::before { content: none; }

/* 45 × 45, radius 2, the number held between two brackets. SPACE_BETWEEN in
   the comp, which is what justify-content does here. */
.timeline__marker {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  width: 45px;
  height: 45px;
  margin: 0;
  border-radius: 2px;
}

/* Both brackets are drawn in Figma with top/left/right borders and no bottom;
   the lower one is flipped, which the API reports as the same border set at a
   y past the frame. Mirrored here rather than restated, so the pair stays a
   pair if the value changes. */
.timeline__bracket {
  width: 45px;
  height: 6px;
  border: 1px solid var(--timeline-line);
  border-bottom: 0;
  border-radius: 2px 2px 0 0;
}

.timeline__bracket:last-child {
  border-bottom: 1px solid var(--timeline-line);
  border-top: 0;
  border-radius: 0 0 2px 2px;
}

.timeline__index {
  color: var(--timeline-index);
  font-family: var(--display);
  font-size: var(--h5-size);
  font-weight: var(--h5-weight);
  letter-spacing: var(--h5-tracking);
  line-height: normal;
}

.timeline__content {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px 0 32px;
}


/* Exo 2 Light 16/26 +1 at #bdd1e3 — a shade brighter than the prose default,
   and stated because `:where(.prose) p` matches these <p> directly. :where()
   on the container so .timeline__title and .timeline__more keep their own. */
:where(.timeline__content) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

/* THE STAGE LINK IS A `more` INSTANCE — this rule only PLACES it.

   It used to draw the whole thing: its own 11/500/+1.7, its own `#7dd3fc`, and
   an arrow via `::after { content: ' →' }`. All three are the component's now
   (`.more` in common.css, from set 772:27004), and leaving them here did real
   damage the moment the markup changed: the pseudo-element arrow and the new
   inline glyph both rendered, so the link read "LEARN MORE → →", and the
   colour override held it at the pale blue while the component is Text/Blue.

   A page positions a component. It does not restate it. */
.timeline__more {
  align-self: flex-end;   /* MAX on the comp's cross axis */
  margin-top: 12px;
}

@media (max-width: 560px) {
  .timeline__step { grid-template-columns: 45px minmax(0, 1fr); gap: 20px; }
  .timeline__content { padding-bottom: 24px; }
}

/* ---------------------------------------------------------------------------
   Basic attacks — illustrated comparison components

   Figma "[Website Page] Basic attacks" (480:4787). The page is built through
   the markdown dialect, not as hand-written output: these are reusable
   structures whose content remains in OUTPUT/. All boxed cards use the shared
   neutral surface; only layout and component typography live here.
   ------------------------------------------------------------------------- */
.attack-intro {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin: 8px 0 56px;
}

:where(.attack-intro) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

.attack-section {
  display: flex;
  flex-direction: column;
  gap: 32px;
  margin: 0 0 56px;
}

.attack-section__title {
  margin: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: var(--h4-size);
  font-weight: var(--h4-weight);
  letter-spacing: var(--h4-tracking);
  line-height: normal;
}

.attack-section__intro {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

:where(.attack-section__intro) p,
:where(.attack-section__intro) ul {
  margin: 0;
}

:where(.attack-section__intro) li { margin: 0; }

.trajectories {
  display: grid;
  grid-template-columns: 292fr 283fr 283fr;
  gap: 12px;
  align-items: stretch;
}

.trajectory {
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}

.trajectory__art {
  display: block;
  width: 234px;
  height: 234px;
  margin: 0;
}

/* Kept inline rather than loaded through <img>: the three Figma exports retain
   their individual groups and paths in the page DOM, so the shared trajectory
   controller can animate the named layers without replacing the artwork. */
.trajectory__svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Layer promotion exists only for the one- or two-second active interval. The
   controller removes .is-animating after the single play-through, avoiding
   permanent compositing layers on a long reading page. */
.trajectory.is-animating [data-trajectory-part="arrow"],
.trajectory.is-animating [data-trajectory-part="nucleus"],
.trajectory.is-animating [data-trajectory-part="aim-orbit"],
.trajectory.is-animating [data-trajectory-part="aim-radius"] {
  will-change: transform, opacity;
}

.trajectory__card {
  display: flex;
  width: 100%;
  min-height: 496px;
  flex: 1;
  flex-direction: column;
  gap: 24px;
  padding: 20px 16px;
  overflow: hidden;
  transition: background-color 180ms var(--ease-out), border-color 180ms var(--ease-out);
}

/* Figma's first `col` shows the trajectory hover state: the same highlighted
   surface token used elsewhere, with no pointer-cursor override. */
.trajectory:hover .trajectory__card {
  --surface-fill: var(--panel-fill-highlighted);
  --surface-stroke: var(--panel-stroke-highlighted);
}

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

.trajectory__body,
.swing__body,
.impulse__body {
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

.trajectory__body {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

:where(.trajectory__body) p,
:where(.trajectory__body) ul,
:where(.trajectory__body) li,
:where(.swing__body) p,
:where(.impulse__body) p {
  margin: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
}

/* These particular `li` instances override the general UL variant to Figma's
   Paragraph Small override. The list itself already supplies the component's
   8px row gap and each item already owns its 26px marker inset, so adding
   another list indent or item margin would double both measurements. */
:where(.trajectory__body) ul:not([class]) { padding: 0; }

:where(.trajectory__body) ul:not([class]) > li {
  min-height: 24px;
  margin: 0;
  font-family: var(--body-paragraph-small-family);
  font-size: var(--body-paragraph-small-size);
  font-weight: var(--body-paragraph-small-weight);
  line-height: var(--body-paragraph-small-line);
  letter-spacing: var(--body-paragraph-small-tracking);
}

:where(.trajectory__body) ul:not([class]) > li::before { height: 24px; }

.attack-progression {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.attack-progression__intro p,
.attack-subsection__intro p,
.attack-subsection__intro ul,
.attack-subsection__intro li {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

.timeline--progression {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: flex-start;
  gap: 24px;
}

.timeline--progression .timeline__step {
  display: grid;
  grid-template-columns: 45px minmax(0, 1fr);
  align-items: center;
  min-width: 0;
  gap: 20px;
  padding-right: 0;
}

.timeline--progression .timeline__step::before { content: none; }
.timeline--progression .timeline__content {
  min-width: 0;
  padding: 0;
}
.timeline--progression .timeline__content p {
  max-width: 16ch;
  text-wrap: balance;
  white-space: normal;
}

.attack-subsection {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.attack-subsection__intro {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.swing-scale {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.swing {
  display: flex;
  min-width: 0;
  min-height: 391px;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 20px 16px;
  overflow: hidden;
}

.swing__title {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 12px;
  padding-bottom: 8px;
}

.swing__value { color: #f69638; white-space: nowrap; }

.swing__art {
  display: block;
  width: 200px;
  height: 200px;
  margin: 0;
}

.swing__body { width: 100%; }

.impulses {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.impulse {
  display: flex;
  min-width: 0;
  align-items: flex-start;
  gap: 20px;
  padding: 16px;
  overflow: hidden;
}

.impulse__art {
  display: block;
  width: 110px;
  height: 110px;
  flex: 0 0 110px;
  margin: 0;
}

.impulse__body {
  display: flex;
  min-width: 0;
  flex: 1;
  flex-direction: column;
  gap: 12px;
}

.attack-outro {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.attack-outro__rule {
  width: 100%;
  height: 1px;
  margin: 0 0 4px;
  border: 0;
  background: var(--panel-stroke-neutral);
}

:where(.attack-outro) > p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

:where(.attack-outro) .blockquote { margin: 0; }

@media (max-width: 1024px) {
  .timeline--progression {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 820px) {
  .trajectories,
  .swing-scale { grid-template-columns: minmax(0, 1fr); }

  .trajectory__card { min-height: 0; }
  .swing { min-height: 0; }
}

@media (max-width: 620px) {
  .attack-intro { margin-top: 0; margin-bottom: 44px; }
  .attack-section { gap: 28px; margin-bottom: 44px; }
  .attack-section__title { font-size: 21px; }
  .impulses { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 460px) {
  .timeline--progression { grid-template-columns: minmax(0, 1fr); gap: 8px; }

  .trajectory__art { width: min(236px, 78vw); height: auto; aspect-ratio: 1; }
  .swing__art { width: min(200px, 68vw); height: auto; aspect-ratio: 1; }

  .impulse {
    flex-direction: column;
    align-items: center;
  }

  .impulse__body { width: 100%; }
}

/* --- branches: peers of a subject, compared in one bordered box ----------
   Figma "Branch comparison" (532:5842). A 1px hairline between entries, not
   around them, so the first has no line above it — the same separator model
   as .scale.                                                               */
/* No padding of its own — each branch carries the 24 and the hairlines fall
   between them, edge to edge. */
.branches { display: block; }

.branch {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 24px;
}


/* NO BRANCH INDEX. Figma "[Website Page] Martial Arts styles" (532:5842)
   draws each frame in "[panel] Branch comparison" as an H3 and a Paragraph
   and nothing else — no numeral node, hidden or otherwise. The rule that
   styled one, and its cyan/amber alternation, went with the `index`
   attribute that fed them. */

/* Body/Paragraph Small — Exo 2 Light 14/24 +1.2 — smaller than the prose around it, and stated for
   the same reason the panel tones state theirs.

   `:where(.branch)`, not `.branch`: at (0,0,1) it still beats
   `:where(.prose) p` on source order, while any component class dropped
   inside a branch keeps its own type. */
:where(.branch) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

:where(.branch) p + p { margin-top: 16px; }

/* --- scale: graded rows with a segmented meter --------------------------- */
.scale { display: block; }

.scale__row {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 20px 28px;
}
/* A rule between rows, not around them — the comp draws separators, not a
   grid, so the first row has no line above it. The hairline is the surface's
   own stroke; see the divider rule beside .surface. */

.scale__value {
  flex: none;
  min-width: 48px;
  color: #f0a416;
  font-family: var(--display);
  font-size: var(--h4-size);
  font-weight: var(--h4-weight);
  letter-spacing: var(--h4-tracking);
  line-height: normal;
}

.scale__meter { flex: none; display: flex; gap: 4px; }
.scale__seg {
  width: 18px;
  height: 3px;
  border-radius: 2px;
  background: rgba(240, 164, 22, 0.9);
  opacity: 0.25;
}
.scale__seg.is-on { opacity: 1; }


/* --- metrics: a figure and the number it measures ------------------------ */
/* --- semicircle: the two reference dials ---------------------------------
   Figma component set "Semicircle", on "[Website Page] Basic defenses"
   (469:4626). Six variants — Direction (downward, upward) × Third (1, 2, 3) —
   and this file ships ONE dial and ONE lightsaber for all six.

   WHY THAT IS FAITHFUL AND NOT A SHORTCUT, both measured rather than assumed:

   • Between the three Thirds, only the lightsaber layer moves. Its centre sits
     ~68px from the dial's centre in every variant — at 12 o'clock, 3 o'clock
     and 6 o'clock — and the blade always points outward. That is not three
     drawings of a blade, it is one blade ORBITING, so one element rotating
     about the dial's centre reproduces all three exactly.
   • Between the two Directions, every child of the variant is the vertical
     mirror of its counterpart, child for child, to within a pixel. So the
     upward column flips the same file rather than shipping a second one.

   The rotation is applied to `.semicircle__arm`, a box the size of the dial
   whose centre IS the dial's centre. The saber is pinned inside it at the 3
   o'clock slot, where the comp draws it horizontally; rotating the arm carries
   it around the circle AND turns it, which is what the variants show. No
   transform-origin arithmetic, and nothing to re-derive if the art is re-cut.

   Angles, read off the variants (CSS is clockwise-positive, Figma is not):
     downward   third 1 = −90° (top)     2 = 0° (right)    3 = +90° (bottom)
     upward     third 1 = +90° (bottom)  2 = 0° (right)    3 = −90° (top)
   ------------------------------------------------------------------------- */
.semicircles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.semicircle {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The dial's own box. `aspect-ratio` from the exported SVG so the arm inside
   is square with it and the rotation stays true at any width.

   It does NOT fill the column: the comp draws a 325-wide dial in a 435 column
   and centres it, so this is 325/435 with `margin-inline: auto`. A percentage
   rather than a pixel width, so the ratio survives the column narrowing. */
.semicircle__dial {
  position: relative;
  width: 74.71%;
  margin-inline: auto;
  aspect-ratio: 398 / 400;
}

/* The plate is the dial's own geometry: the ring and the three markers. It is
   what mirrors for the upward direction — the saber hangs outside it, because
   its three angles are already the mirrored ones. */
.semicircle__plate { position: absolute; inset: 0; }

.semicircle[data-direction="upward"] .semicircle__plate { transform: scaleY(-1); }

.semicircle__ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;                 /* the prose gives images a margin; this is art */
}

/* THE THREE POSITION MARKERS. In the set they are one group placed three
   times, rotated 0 / 90 / 180 about the dial's centre — the saber's own three
   slots — so this is the arm trick again: one drawing, three rotations.

   THE VARIANT DIMS TWO OF THEM. Whichever marker stands at the saber's
   position is at full opacity and the other two at 30%, in all six variants.
   That is the second thing the Third changes, and missing it left every dial
   showing the first position's emphasis whatever was selected. */
.semicircle__mark {
  position: absolute;
  inset: 0;
  transform: rotate(var(--mark-angle, 0deg));
  opacity: 0.3;
  transition: opacity 320ms var(--ease-out);
}

.semicircle__mark[data-slot="1"] { --mark-angle: 0deg; }
.semicircle__mark[data-slot="2"] { --mark-angle: 90deg; }
.semicircle__mark[data-slot="3"] { --mark-angle: 180deg; }

/* The marker's box in the dial's own units: 107 × 119 at (190, 29.4) of
   398 × 400 — where the set draws the first one. */
.semicircle__mark img {
  position: absolute;
  left: 47.74%;
  top: 7.35%;
  width: 26.88%;
  height: auto;
  margin: 0;
}

.semicircle[data-third="1"] .semicircle__mark[data-slot="1"],
.semicircle[data-third="2"] .semicircle__mark[data-slot="2"],
.semicircle[data-third="3"] .semicircle__mark[data-slot="3"] { opacity: 1; }

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

.semicircle__arm {
  position: absolute;
  inset: 0;
  transform: rotate(var(--semicircle-angle, 0deg));
  transition: transform 520ms var(--ease-out);
}

/* The 3 o'clock slot, in the dial's own units: the saber's exported box is
   248.1 × 55 with its left edge at x=153.4 and its top at y=173 of 398 × 400. */
.semicircle__saber {
  position: absolute;
  left: 38.54%;
  top: 43.25%;
  width: 62.34%;
  height: auto;
  margin: 0;
}

.semicircle[data-direction="downward"][data-third="1"] { --semicircle-angle: -90deg; }
.semicircle[data-direction="downward"][data-third="2"] { --semicircle-angle: 0deg; }
.semicircle[data-direction="downward"][data-third="3"] { --semicircle-angle: 90deg; }
.semicircle[data-direction="upward"][data-third="1"]   { --semicircle-angle: 90deg; }
.semicircle[data-direction="upward"][data-third="2"]   { --semicircle-angle: 0deg; }
.semicircle[data-direction="upward"][data-third="3"]   { --semicircle-angle: -90deg; }

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

.semicircle__positions {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* A BUTTON. The comp specifies selection by hover alone; a button adds focus
   and tap without altering the pointer behaviour. Hence the resets. */
.semicircle__position {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0;
  padding: 16px;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 200ms var(--ease-out),
              border-color 200ms var(--ease-out);
}

/* Body/Label Small — Tektur Medium 11 / +1.7 / UPPER, from the style, not a
   node. Was Body/Label (Tektur Regular 13 / +2); the comp's `idx` in every
   "[panel] downward|upward 0N / top" now carries the smaller style.
   The uppercase comes with it: the source text reads "01 · upper" and the
   style's textCase renders it "01 · UPPER". */
.semicircle__idx {
  color: var(--body-dim);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
  transition: color 200ms var(--ease-out);
}

/* Selected: the highlighted tone, and the index in cyan. Driven by the same
   attribute the script sets, so the state is one thing and not two. */
.semicircle__position[aria-pressed="true"] {
  --surface-fill: var(--panel-fill-highlighted);
  --surface-stroke: var(--panel-stroke-highlighted);
}
.semicircle__position[aria-pressed="true"] .semicircle__idx { color: var(--cyan); }

/* Body/Paragraph Small — Exo 2 Light 14 / 24 / +1.2 #b2c7db,
   not the 16/26 panel body. Stated on the paragraph because `:where(.prose) p`
   would otherwise match it directly and win by directness. */
:where(.semicircle__position) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

@media (max-width: 860px) {
  /* No mobile frame exists. Two 435-wide columns need 882; below that they
     stack, which keeps each dial legible rather than shrinking both. */
  .semicircles { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

/* --- faq: the big accordion ----------------------------------------------
   Figma "Accordion - big", on "[Website Page] FAQ" (629:7029). Three variants,
   and every value below is read from one of them rather than invented for the
   default and guessed for the rest:

     collapsed   no fill; ＋ marker #7dd3fc; question #a7b9cb
     hover       Fill - neutral; marker AND question amber
     expanded    Fill - highlighted; － marker; question #ebf2f8

   Only the bottom edge is stroked (Stroke - neutral, INSIDE, 1px), so the run
   reads as a list of rows rather than a stack of boxes. That is why this is not
   a `.surface`: a surface is a box with four edges, and this has one.

   Geometry, from the variants: summary 20/24, the open one 24 at the bottom,
   the answer inset 32 past the item's 24 gutter. A one-line collapsed item is
   62px; the reference two-line expanded item is 142px.
   ------------------------------------------------------------------------- */
.faq {
  --faq-gutter: 24px;
  --faq-answer-inset: 32px;
}

/* Overriding the generic `.prose details` box, which is a bordered panel with
   a radius — a different component. Written with the class first so it does
   not depend on source order to win. */
.faq details {
  margin: 0;
  border: 0;
  box-shadow: inset 0 -1px var(--panel-stroke-neutral);
  border-radius: 0;
  background: transparent;
  transition: background-color 200ms var(--ease-out);
}

.faq details + details { margin-top: 0; }

/* The fill belongs to the ROW, and the row is the whole <details>. Putting it
   on the summary would leave the answer sitting outside its own tint. */
.faq details[open] { background: var(--panel-fill-highlighted); }

.faq summary {
  display: flex;
  /* The component's header uses centred alignment: its 16px marker sits at y3
     inside the 22px one-line reference header. Wrapped questions therefore
     keep the component's own centre alignment rather than inventing a
     first-line override in code. */
  align-items: center;
  gap: 16px;
  padding: 20px var(--faq-gutter);
  color: var(--body);
  font-family: var(--display);
  font-size: var(--h5-size);
  font-weight: var(--h5-weight);
  letter-spacing: var(--h5-tracking);
  line-height: normal;
  cursor: pointer;
  list-style: none;
}

.faq details[open] > summary {
  padding-bottom: 24px;     /* the expanded variant's 24 gap under the header */
  color: var(--ink-soft);
}

/* THE MARKER IS THE SHARED ONE — see --icon-plus at the top of this file.

   This used to redeclare a typographic ＋ / － at 20px wide, because the glyph
   characters are not the same width and the question would otherwise shift as
   the row opened. The component now instances "btn icon" Icon=plus /
   Icon=minus: one 16 × 16 box for both states, so the width problem is gone
   with the characters, and `.prose summary::before` already draws it.

   Nothing is restated here. The only thing this component does differently is
   the padding and the row fill, above. The gap of 16 and the marker's 16 put
   the question at x = 32, which is where the comp has it.

   ONE COLOUR NOTE: the collapsed marker is #40d1ff at 80%, not the #7dd3fc
   this block used to name. That came in with the new glyph and is now set once
   for both accordions. */

/* State=hover fills the row. Scoped to the summary, not the row, so hovering a
   long answer does not light up the question above it. The marker and label
   colours come from the shared rules. */
.faq details:not([open]) > summary:hover,
.faq details:not([open]) > summary:focus-visible { background: var(--panel-fill-neutral); }

.faq summary:hover,
.faq summary:focus-visible { color: var(--amber-warm, var(--amber)); }

/* The answer frame itself is inset by the row's 24px gutter, then its text is
   inset another 32px. This explicit margin also overrides the generic prose
   disclosure's 16px/12px margin, which used to shift the answer 16px too far
   inward and add 12px below the designed component. The tint remains on the
   parent <details>, so it still runs edge to edge behind this inset frame. */
.faq .disclosure__body {
  margin: 0 var(--faq-gutter);
  padding: 0 0 24px var(--faq-answer-inset);
}

/* Exo 2 Light 16/26/+1 — stated on the paragraph itself, not on the wrapper.
   The trap this file already documents twice: `:where(.prose) p` matches the
   <p> DIRECTLY, and a direct match beats anything inherited from an ancestor,
   so type set on `.disclosure__body` never reaches the words. */
.faq .disclosure__body p {
  margin: 0 0 1em;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  letter-spacing: 1px;
}

.faq .disclosure__body > :last-child { margin-bottom: 0; }

@media (max-width: 600px) {
  .faq { --faq-gutter: 16px; --faq-answer-inset: 20px; }
}

/* --- patterns: the Footwork comp's card grid -----------------------------
   "[Website Page] Footwork" (248:3649) draws it three times — Direction cards,
   Movement atlas, Body control cards — with one anatomy: a 24 box (space/24,
   bound), an index, a title, line art pinned right, and a paragraph 14 below.
   The box itself is a `.surface`; nothing here restates a fill or a stroke.

   FLEX-WRAP, NOT A GRID, because the atlas mixes widths inside one group:
   three full-width cards, then two rows of two. `auto-fit` cannot express that
   — every track would be the same size — so each card declares its own share
   of the row and the row wraps around it.

   The comp draws these at radius 6 while the Panel collection's radius, which
   every other box on the site uses, is 4. The 6 is not bound to the variable
   and 4 is the consolidated value, so these follow the system. */
.patterns {
  --patterns-gap: 20px;
  --patterns-index: #f69638;

  display: flex;
  flex-wrap: wrap;
  gap: var(--patterns-gap);
}

/* The modifiers group indexes in cyan rather than amber. */
.patterns--cyan { --patterns-index: var(--cyan); }

.pattern {
  flex: 1 1 100%;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: var(--panel-pad);
}

/* `calc` rather than a percentage, because the gap is between the cards and a
   50% card plus a 20px gap does not fit. */
.pattern--half  { flex-basis: calc((100% - var(--patterns-gap)) / 2); }
.pattern--third { flex-basis: calc((100% - 2 * var(--patterns-gap)) / 3); }

.pattern__head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: 48px;     /* the art's height, so a card without art aligns */
}

/* Tektur 13 / +2 uppercase — the same index the panel label and the column
   title draw, which is why the value is repeated here and not re-derived. */
.pattern__index {
  flex: none;
  color: var(--patterns-index);
  font-family: var(--display);
  font-size: var(--body-label-size);
  font-weight: var(--body-label-weight);
  letter-spacing: var(--body-label-tracking);
  line-height: 21px;      /* aligns the 13px index to the 16px title */
  text-transform: uppercase;
}

/* The art sits at the far right of the header, whatever the title's length. */
.pattern__art {
  flex: none;
  margin-left: auto;
  width: 72px;
  height: 48px;
  background: var(--pattern-art) center / contain no-repeat;
}

/* The direction cards' art is 48 wide, the atlas's 72; `contain` fits either
   inside the box without the markup having to say which it is. */

/* Body/Paragraph Small — Exo 2 Light 14/24 +1.2. `:where()` keeps the card's
   own components in control of the cascade. */
:where(.pattern) p {
  margin: 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

@media (max-width: 860px) {
  /* Two- and three-up rows collapse rather than shrinking below readable
     width; the comp has no mobile frame, so this is an implementation
     choice — see BUILD.md. */
  .pattern--half,
  .pattern--third { flex-basis: 100%; }
}

.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 24px;
}

.metric {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: var(--panel-pad);
}

.metric__art {
  flex: none;
  width: 150px;
  height: 100px;
  background: var(--metric-art) center / contain no-repeat;
}

.metric__body { display: flex; flex-direction: column; gap: 12px; }

.metric__value {
  color: #f0a416;
  font-family: var(--display);
  font-size: var(--h5-size);
  font-weight: var(--h5-weight);
  letter-spacing: var(--h5-tracking);
  line-height: normal;
}

/* Not an eyebrow — it only shared the eyebrow's grey. That token went with the
   eyebrow's grey state, so the value is stated here, where it is actually used. */
.metric__label {
  color: var(--body-dim);
  font-family: var(--display);
  font-size: var(--body-label-small-size);
  font-weight: var(--body-label-small-weight);
  letter-spacing: var(--body-label-small-tracking);
  line-height: normal;
  text-transform: uppercase;
}

/* --- figures: images side by side --------------------------------------- */
/* The comp pairs the two scoring photographs, 430 wide each with a 20px gap in
   the 880 column. Equal columns rather than intrinsic widths, so a pair of
   differently-sized exports still lines up. Below the panel's width they stack
   rather than shrink past legibility. */
.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}


.figures img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
}

@media (max-width: 640px) {
  .scale__row { flex-wrap: wrap; gap: 12px 16px; }
  .scale__label { flex-basis: 100%; }
  .metric { flex-direction: column; align-items: flex-start; }
}

/* ===========================================================================
   :::steps — a numbered sequence of stages
   Figma "Neoteric Martial Arts" (237:10172), the three NMA levels.

   Row: a hairline, then a 125px gutter holding the counter and the icon, then
   the body. The counter's number is 98px and its word 15px, which is why the
   two are separate spans rather than one string — one is a figure to be read
   across the page, the other a label beneath it.
   ========================================================================= */
.steps { display: flex; flex-direction: column; gap: 48px; }

/* Divided, not boxed — the rule between steps is the surface stroke; see the
   divider rule beside .surface. */
.step {
  display: grid;
  grid-template-columns: 125px 1fr;
  gap: 40px;
  padding-top: 24px;
}

.steps > .step:first-child { border-top: 1px solid var(--panel-stroke-neutral); }

.step__gutter { display: flex; flex-direction: column; gap: 40px; align-items: center; }

/* THE COUNTER IS ONE MARK, NOT TWO STACKED. The comp draws a huge numeral at
   10% opacity and lays a translucent band across it carrying the word — the
   word sits ON the figure, not under it. Read from the node tree: "index" is
   98px Tektur Bold at 10% opacity, and "Frame 90" is ABSOLUTE at y=22, 125×24,
   filled #000 at 10%, holding the label at full strength.

   Stacking them vertically, which is what this did first, loses the effect
   entirely: two solid cyan lines instead of a ghost with a plate over it. */
.step__counter {
  position: relative;
  display: grid;
  place-items: center;
  /* The gutter centres its children, which makes a flex item shrink to its
     content — the counter came out 106 wide (the numeral's own width) instead
     of filling the comp's 125px column, so the band under it was short too. */
  width: 100%;
  height: 70px;
  margin: 0;
  color: var(--cyan);
  font-family: var(--display);
  text-align: center;
}

.step__index {
  grid-area: 1 / 1;
  align-self: start;
  width: 100%;
  font-size: 98px;
  font-weight: 700;
  /* 71.2% of 98 — the comp's own line height, and what makes a 98px glyph sit
     in a 70px box. */
  line-height: 0.712;
  opacity: 0.1;
}

/* The band. It is a piece of frosted glass over the numeral: a 10% black plate
   with a 13px BACKGROUND BLUR, so the ghosted figure shows through it softened
   rather than merely dimmed. Figma's effect is BACKGROUND_BLUR radius 13.

   The word is UPPER by text case, not by the characters — the source keeps
   "level" / "уровень" / "poziom" in lower case and the transform does the rest,
   which is what lets each language stay readable in the markdown.

   Figma nudges each numeral's tracking by a pixel or two (8, −1, 0) to
   re-centre it by hand; rather than copy three arbitrary values, the numeral is
   centred honestly at zero tracking and the band is centred on the box. */
.step__word {
  grid-area: 1 / 1;
  align-self: center;
  justify-self: stretch;
  padding: 2px 0;
  background: rgba(0, 0, 0, 0.1);
  -webkit-backdrop-filter: blur(13px);
  backdrop-filter: blur(13px);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 5px;
  text-indent: 5px;              /* tracking is added after the last glyph too */
  line-height: normal;
  text-transform: uppercase;
}

.step__art {
  width: 112px;
  height: 112px;
  background: var(--step-art) center / contain no-repeat;
}

.step__body { display: flex; flex-direction: column; gap: 16px; }

:where(.prose) .step__title {
  margin: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: var(--h4-size);
  font-weight: var(--h4-weight);
  letter-spacing: var(--h4-tracking);
  line-height: normal;
}
:where(.prose) .step__body > p { margin: 0; }
:where(.prose) .step__body > .blockquote { margin: 0; }

/* The gutter is ornament: below the panel's width it would take a third of the
   column, so the counter goes inline above the body and the icon steps aside. */
@media (max-width: 720px) {
  .step { grid-template-columns: 1fr; gap: 20px; }
  .step__gutter { flex-direction: row; align-items: center; gap: 20px; }
  .step__index { font-size: 64px; }
  .step__art { width: 72px; height: 72px; }
}
