/* ============================================================================
   JT LANDING PAGE — v2.0.0
   Styles for the page built by build/landing.js.

   Loads AFTER /styles/common.css, which owns the palette, the reset, the base,
   the skip link, the display heading, the lede, the eyebrow and the Button.
   What is left here is the landing page itself: its frame, its six sections,
   the hero figure, and the reveal and ambient motion that drive them.

   SOURCE
   Figma frame "Landing" (38:2476), measured — not eyeballed from a screenshot.
   Every colour, size, weight, letter-spacing and gap below was read off the
   node tree. Component states come from every variant of their component sets.

   The comp is drawn at 1920 with a 1344 content column and 288px gutters. The
   column is no longer a landing-only width: 1344 is the shared grid container
   from "[Foundation] Grid & vertical rhythm", so the landing and the content
   pages now sit on the same 12-column field. Below 1440 the column is the
   viewport minus the range's margin; it never exceeds 1344.

   WHAT IS *NOT* FROM THE DESIGN — RESPONSIVE BEHAVIOUR
   No mobile or tablet frame exists. Every breakpoint here is a judgement made
   during implementation and is a candidate for a future comp (CLAUDE.md asks
   that these be flagged rather than left implicit):

     1200px  hero stops being two columns — copy 910 + art 600 no longer fit.
     1080px  the three NMA levels go from 3 columns to 2, then 1 at 720px.
     1000px  community copy and stats stack; stats become a row, then a column
             at 620px.
     900px   the CTA band stacks and its buttons go full width.
     760px   the rank ladder stops fitting seven insignia and scrolls
             horizontally rather than wrapping — the ladder reads as a sequence
             and wrapping breaks that reading.

   NAMING
   --jt- belongs to the frozen modules; page CSS must not declare it. The
   exception is SETTING a module's documented knob, which is how the modules are
   meant to be tuned. This file styles nothing inside .jt-menu or .jt-footer.
   ========================================================================== */

:root {
  /* --- overrides of common.css -------------------------------------------
     Three tokens where this page legitimately differs. Overridden rather than
     renamed, so the role keeps one name across the site.

       --amber   the hero figure's amber is #f0a416; common.css carries the
                 footer's warmer #eb8b2d, which this page also uses as
                 --amber-warm.
       --rule    hairlines here are 20%, against 16% on content pages.
       --content-max  the landing column is the full 1344 grid container;
                 content pages use the narrower 1040 reading panel. */
  --amber:       #f0a416;
  --rule:        rgba(125, 211, 252, 0.20);
  --content-max: 1344px;

  /* --- landing-only palette ----------------------------------------------- */
  --cyan-art:    #02ccf2;   /* line art */
  --amber-warm:  #eb8b2d;
  --amber-deep:  #d06700;   /* rank connectors */

  /* --- layout -------------------------------------------------------------
     The gutter IS grid/margin from the shared breakpoint system, so the landing
     column lines up with the content pages at every range. Stepped rather than
     fluid because the grid defines discrete margins per range; see the
     "[Foundation] Grid & vertical rhythm" frame.

       320–509   12      1025–1440  48
       510–719   24      >1440      48, and .page centres at 1440

     1344 + 2 × 48 = 1440 exactly, which is why .page caps there. */
  --gutter:      12px;

  /* The comp's between-column gap in S1, S2, S4 and S7 is 80. The coefficient
     is 80/1440, not 80/1600, so it reaches 80 exactly at the 1440 viewport
     where the column reaches its 1344 cap — the same correction the figure and
     media widths needed when the column stopped tracking the viewport. */
  --col-gap:     clamp(32px, 5.56vw, 80px);

  /* Re-measured from the rebuilt frame: the content column is 1344 wide with
     `VERTICAL gap 340, pad 250/0/250/0`. The 340 still exceeds rhythm/page
     (200) — flagged in the foundation frame's migration table as a decision,
     not a fix, so it stays until called. Both scale: at 390px wide, 340px of
     empty space is most of a screen. The page's bottom padding is set on
     .page below. */
  --section-gap: clamp(96px, 17.71vw, 340px);

  /* --- the first screen ---------------------------------------------------
     THE TOP PAD IS MEASURED AGAINST THE VIEWPORT, NOT THE COMP.

     The brief is "on load, the first section is roughly in the centre of the
     screen", and a fixed pad cannot deliver that: the comp's 250 centres S1
     only on a viewport about 1170px tall, and pushes it well below centre on a
     laptop. So the pad is what it has to be to put the hero's midpoint on the
     viewport's midpoint:

         pad = 50svh − (half the hero's height)

     THE MENU IS NOT SUBTRACTED, and that is not an oversight. It is fixed, so
     it overlays the page rather than occupying layout space — .page begins at
     y=0 and its padding is measured from the top of the viewport, not from
     under the nav. Subtracting 60 here was the first version and it lifted the
     hero 60px above centre, measured in the browser at 1710 × 842.

     --hero-half is half the hero's MEASURED height (571 in EN at the 1344
     column), not half the comp's 551. It is still an approximation: the height
     moves with the language, the wrap of the title and the column width, and
     "approximately centred" is the requirement. Overriding this one number is
     how a future retune happens.

     svh, not vh: on mobile Safari `vh` is the LARGE viewport — the height the
     page has only after the URL bar retracts — so on load, when the bar is
     still showing, a vh-based pad overshoots and pushes the hero below centre.
     svh is the small viewport, which is what "on load" actually means.

     The clamp is the safety rail. On a short screen the formula goes small or
     negative and the hero would slide under the fixed nav; on a very tall one
     it would open with more emptiness than the comp ever draws. The 112 floor
     leaves 52px of clearance below the 60px bar; 340 is the section rhythm. */
  --hero-half:   286px;
  --page-top:    clamp(112px, calc(50svh - var(--hero-half)), 340px);

  /* The hero figure's own knobs are NOT here any more — they belong to
     modules/balance/, which owns its rotation groups, its label radius and its
     colours. This page sets only how wide the figure is; see .hero__figure. */
}

/* grid/margin per breakpoint range. Min-width so the cascade reads upward and
   the 320 value is the base, matching the mobile-first rest of the file. */
@media (min-width:  510px) { :root { --gutter: 24px; } }
@media (min-width:  720px) { :root { --gutter: 32px; } }
@media (min-width: 1025px) { :root { --gutter: 48px; } }

/* Only the typography. Everything structural about <body> — margin, min-width,
   overflow, isolation, colour, family, weight — is in common.css and is not
   repeated. These three are Figma Body/Paragraph: Exo 2 Light 16/26 +1px. */
body.landing {
  font-size: 16px;
  line-height: 26px;
  letter-spacing: 1px;
}

/* ---------------------------------------------------------------------------
   Page frame
   ------------------------------------------------------------------------- */
.page {
  max-width: calc(var(--content-max) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-top: var(--page-top);
  padding-bottom: clamp(80px, 10vw, 160px);
}

.section + .section { margin-top: var(--section-gap); }

/* ---------------------------------------------------------------------------
   S1 · Hero
   Comp: horizontal, space-between, centred. Copy 598 + gap 80 + art 666 = 1344.
   ------------------------------------------------------------------------- */
.section--hero {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--col-gap);
}

/* The hero copy is the HERO MODULE — its type, its internal gaps and its
   per-line spans are in modules/hero/hero.css. This page says only how wide the
   block is and how it shares the row with the figure.

   In the 1344 column the copy is the flexible side and the figure is the fixed
   one: 598 + 80 + 666 = 1344. There is no max-width any more — 910 was the
   width the copy happened to have in the old 1600 column, and carrying it
   forward would only ever be dead code or, on a wider container, wrong. */
.hero__copy {
  flex: 1 1 auto;
}

/* --- the golden balance figure ------------------------------------------
   Rendered and styled by modules/balance/. This page says only how wide it is
   and nothing else — no geometry, no colours, no motion. Everything that used
   to be here is in modules/balance/balance.css.

   The comp draws it at 666 in the 1344 column — 49.6% of it.

   This is a PERCENTAGE, not a vw, and that changed with the column. 42vw was
   right when the column was 1600 and tracked the viewport; now the column caps
   at 1344, so 42vw under-draws the figure exactly at the design width (605 at
   1440, against the comp's 666). A percentage resolves against .section--hero,
   which IS the column, so the figure holds the comp's share at every width. */
.hero__figure { --jt-balance-width: min(666px, 49.6%); }

/* ---------------------------------------------------------------------------
   S2 · Neoteric Martial Arts
   Comp: vertical gap 100. Top row: copy 853 + gap 80 + video 411 = 1344.
   Levels: three columns filling 1344 at gap 56 — 411 each.
   ------------------------------------------------------------------------- */
.section--nma { display: flex; flex-direction: column; gap: 100px; }

.nma__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--col-gap);
}

/* No max-width: the media block is the fixed side at 257, so the copy resolves
   to the comp's 1007 on its own. */
/* WIDTH ONLY. The stacking and the 32 between eyebrow, title and lede belong
   to the Hero module now — this class says how the block shares its row and
   nothing else. It used to declare `display: flex; flex-direction: column;
   gap: 24px`, three properties the component already owns and one of which
   (24) disagreed with it. */
.nma__copy { flex: 1 1 auto; }

/* ---------------------------------------------------------------------------
   S2 video — Figma "video frame" (715:23911)

   MEASURED — re-read after the comp changed; the earlier values are noted so
   the diff is legible rather than mysterious.
     frame     411 × 347 LANDSCAPE  (was 318 × 269, and 256.88 × 341 portrait
               before that),
               1px INSIDE stroke #89e3f5 at 11%  (was #d06700 at 40%),
               corner radius 4
     video     VIDEO paint, scaleMode FILL  → object-fit: cover
     controls  "Frame 267", HORIZONTAL, gap 12, centred in the frame
     button    the "Start" set (715:23968): 70 × 70, radius 36, 20 × 20 white
               glyph, #000 at 40% — 80% on State=Hover  (hover was 60%)

   The box turned from portrait to landscape, which matters for object-fit: the
   clip is portrait, so `cover` now crops it hard top and bottom instead of
   barely at all. That is what the comp's FILL paint asks for; a `contain` here
   would letterbox inside the border.

   The radius lives on the RECTANGLE in Figma, together with the stroke — not
   on the frame around it. Here both are on .video, which is the element that
   carries the border and clips the media, so the rounding reaches the video
   pixels instead of just the outline.

   The width is a percentage of the 1344 column, not a vw: see .hero__figure
   for why. 411 / 1344 = 30.58%.
   ------------------------------------------------------------------------- */
.video {
  flex: 0 0 auto;
  position: relative;
  width: min(411px, 30.58%);
  /* The comp's own 411 × 347. */
  aspect-ratio: 411 / 347;
  margin: 0;
  overflow: hidden;
  border-radius: 4px;
  /* INSIDE stroke in Figma, so it must not grow the box. */
  box-sizing: border-box;
  border: 1px solid rgba(137, 227, 245, 0.11);
}

.video__media {
  display: block;
  width: 100%;
  height: 100%;
  /* scaleMode FILL. A portrait clip in a portrait box barely crops, but the
     paint says cover and a different edit should not letterbox. */
  object-fit: cover;
  /* Until the first frame decodes the element is transparent, which over the
     starfield reads as an empty bordered box. This is the frame's own dark. */
  background: rgba(3, 10, 17, 0.45);
}

/* Centred over the video, not in flow: the comp positions the row at the
   frame's centre and the video fills the whole frame behind it. */
.video__controls {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

/* ---------------------------------------------------------------------------
   The controls appear on hover, not permanently.

   THIS IS GATED ON `hover: hover` AND THAT IS THE WHOLE POINT.
   A phone has no hover. Hiding the controls there and waiting for a hover that
   never comes would leave the pause button and the Instagram link permanently
   invisible and, worse, silently un-pressable-looking — the exact bug that
   "reveal on hover" patterns ship with. So the hiding only exists for pointers
   that can actually hover; everywhere else the controls stay as they were.

   :focus-within is not decoration either. The controls remain in the tab order
   while transparent, so without it a keyboard user would tab to a button they
   cannot see. Focus reveals the pair, exactly as a mouse would.

   Opacity, not display or visibility: those would drop the buttons out of the
   accessibility tree and make the reveal un-animatable.
   ------------------------------------------------------------------------- */
@media (hover: hover) {
  .video__controls {
    opacity: 0;
    transition: opacity 180ms ease-out;
  }

  .video:hover .video__controls,
  .video__controls:focus-within { opacity: 1; }

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

/* One component, two elements — a <button> that changes this page and an <a>
   that leaves it. See build/landing.js. */
.video__btn {
  flex: none;
  display: grid;
  place-items: center;
  /* The comp is 70 in a 257 frame, and the frame shrinks with the column, so
     this scales with it rather than swallowing a narrow frame. The floor is
     the 44px minimum touch target.

     The percentage resolves against the figure's PADDING box, not its border
     box: the 1px border on each side makes that 255, so the comp's 70 is
     70/255 = 27.45%, not 70/257. At 27.24% it measured 69px in the browser. */
  width: clamp(44px, 27.45%, 70px);
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 50%;                       /* radius 36 on a 70 box */
  background: rgba(0, 0, 0, 0.4);           /* State=Default */
  color: #fff;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: background-color 180ms ease-out;
}

/* State=Hover. 0.8 in the current comp; it was 0.6 when this was first built,
   and the darker value is what makes the glyph read once the controls are only
   on screen while the pointer is over the video. */
.video__btn:hover,
.video__btn:focus-visible { background: rgba(0, 0, 0, 0.8); }

.video__btn:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}

.video__icon {
  /* 20 in a 70 button. Same proportion as the button shrinks. */
  width: clamp(14px, 28.57%, 20px);   /* 20 / 70 = 28.57% */
  height: auto;
}

/* ---------------------------------------------------------------------------
   The toggle carries BOTH glyphs and shows one.

   EVERY SELECTOR HERE IS SCOPED TO [data-action="toggle"], and that is a fix,
   not a flourish. These rules used to say `.video__btn .video__icon`, which
   also matched the Instagram link — it has exactly one glyph, so it is
   `:nth-of-type(1)`, and the paused rule that hides the first icon hid ITS
   icon too. Pause the video and the second button went blank: a black circle
   with nothing in it. Reported as "icon missing on hover" because pausing is
   what you do with the pointer over the frame.

   Swapping `d` on a single path would animate nothing and leaves no way to
   style the two glyphs differently, so this stays two elements and a display
   switch — now asking the right button.
   ------------------------------------------------------------------------- */
.video__btn > * { grid-area: 1 / 1; }

[data-action="toggle"] .video__icon:nth-of-type(2) { display: none; }
.video[data-paused="true"] [data-action="toggle"] .video__icon:nth-of-type(1) { display: none; }
.video[data-paused="true"] [data-action="toggle"] .video__icon:nth-of-type(2) { display: block; }

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

/* --- the three levels -----------------------------------------------------
   Comp: `levels` HORIZONTAL gap 56, three 411 columns (411 × 3 + 56 × 2 = 1344).
   Each column is `group · kicker` VERTICAL gap 32:
     level-1        HORIZONTAL gap 24  →  the 112 glyph beside
       group · rule VERTICAL   gap 20  →  kicker over H4
     body                                the paragraph, full column width
   ------------------------------------------------------------------------- */
.levels {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 56px;
}

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

.level__head { display: flex; align-items: flex-start; gap: 24px; }

/* The glyph is the fixed side; the heading takes what is left. `min-width: 0`
   because a flex item's default min-width is auto, and a long unbroken word in
   a title would otherwise push the column past its track instead of wrapping. */
.level__art { flex: 0 0 112px; width: 112px; height: 112px; }
.level__svg { display: block; width: 100%; height: 100%; overflow: visible; }

.level__heading {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* Re-measured: 13 Tektur Regular / +2px / UPPER / #02ccf2.
   Was 11px, weight 500, +1.7px and dimmed to 0.92 — all four drifted, and the
   opacity in particular was doing the work the colour should. */
.level__kicker {
  margin: 0;
  color: var(--cyan-art);
  font-family: var(--display);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 2px;
  text-transform: uppercase;
}

/* THE LEVEL TITLE HAS NO TYPE RULE. It is an <h4>, the comp's layer is "H4"
   carrying Heading/H4, and the shared ramp in common.css dresses every h4 on
   the site — family, 23/SemiBold/+2, and --ink (#cbddea), which is what the
   comp says.

   This block used to restate all five properties, and its colour was wrong:
   --ink-soft (#ebf6fe) against the comp's #cbddea, with a comment beside it
   naming the value it was not using. That is the whole argument for one ramp
   rather than three. Nothing replaces it — the tag is the style. */

/* Figma Body/Paragraph Small: Exo 2 Light 14 / 24 / +1.2px. */
.level__text {
  margin: 0;
  color: var(--body);
  font-size: 14px;
  font-weight: 300;
  line-height: 24px;
  letter-spacing: 1.2px;
}

/* ---------------------------------------------------------------------------
   S4 · Community
   Comp: horizontal gap 80 — copy fills to 853, stats 411 wide, stats gap 40.
   ------------------------------------------------------------------------- */
.section--community {
  display: flex;
  align-items: flex-start;
  gap: var(--col-gap);
}

/* Width only — see .nma__copy. */
.community__copy { flex: 1 1 auto; }

/* 411 / 1344 = 30.58%. A percentage rather than a fixed basis, for the same
   reason as .video and .hero__figure: below the 1440 cap the column shrinks and
   a frozen 316 would eat a growing share of it. */
.stats {
  flex: 0 0 min(411px, 30.58%);
  display: flex;
  flex-direction: column;
  gap: 40px;
}

/* The figures are the SHARED numeralia component — styling in common.css. The
   hairline this section used to draw above each one is gone with it: the comp's
   stats frame holds three bare numeralia instances and no rule. */

/* ---------------------------------------------------------------------------
   S5 · The Path of Ranks
   Comp: vertical gap 64. Top row: copy 853 + gap 80 + photo 411 = 1344, with
   the ladder — seven 72px insignia and connectors filling the space between —
   below it.
   ------------------------------------------------------------------------- */
/* The section stacks its own parts — the top row, then the ladder — at the
   comp's 64. The gaps INSIDE the copy are the Hero's 32 and are not this; they
   are different measurements of different things, which is why both survive.
   The 24 that used to be here was the old single-column rhythm. */
.section--ranks { display: flex; flex-direction: column; gap: 64px; }

.ranks__top {
  display: flex;
  align-items: flex-start;
  gap: var(--col-gap);
}

.ranks__copy { flex: 1 1 auto; }

/* --- the photograph -------------------------------------------------------
   Figma "IMG_0045 1" (804:22417): 411 × 351, radius 2, 1px stroke #89e3f5 at
   11% — the same hairline the video wears, which is why both read as one
   family rather than two one-off treatments.

   It is an <a>, so it needs the block/relative treatment a bare inline anchor
   would not get, and the image inside must fill it exactly. `display: block` on
   the img kills the inline-baseline gap that would otherwise show as a stripe
   of background below the photo inside the border. */
/* NO STROKE, at rest or on hover. The video keeps its hairline; this does not.
   Asked for directly, and the re-exported photograph carries its own edge. */
.ranks__photo {
  flex: 0 0 min(411px, 30.58%);
  display: block;
  aspect-ratio: 411 / 351;
  overflow: hidden;
  border-radius: 2px;
  /* The photo is a link, so it still has to answer a pointer and a keyboard.
     With the border gone the only affordance left is the image itself, which
     is why the brightness stays. */
  transition: filter 200ms ease;
}

.ranks__photo img {
  display: block;
  width: 100%;
  height: 100%;
  /* The export is already the box's aspect ratio, so cover crops nothing; it
     is here so a re-export at a different ratio degrades to a crop rather than
     to a squash. */
  object-fit: cover;
}

.ranks__photo:hover { filter: brightness(1.12); }

/* Keyboard focus needs something a brightness change cannot provide — an
   outline is drawn outside the box, so it survives the overflow clip that the
   border used to live inside. */
.ranks__photo:focus-visible {
  filter: brightness(1.12);
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}

/* --- the rank ladder ------------------------------------------------------
   Rendered and styled by modules/ladder/ — the same component the rank system
   page shows. This page says only where it sits. Everything that used to be
   here (.ranks, .rank, .rank__index, .rank__name, .rank-connector) went with
   it, along with the four tokens it had been borrowing from this file.

   No margin-top: the section's own 64 gap is the whole spacing now. The 40 that
   used to sit here was stacked on top of the old 24 to approximate it. */

/* ---------------------------------------------------------------------------
   S6 · Holocron
   Comp: vertical gap 32, centred. Glyph 120×80. Slider row space-between:
   arrow 44, gap 10, quote 1236, gap 10, arrow 44 = 1344. Stepper below.
   ------------------------------------------------------------------------- */
.section--holocron {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  text-align: center;
}

.holocron__glyph { width: 120px; height: 80px; }
.holocron__svg { display: block; width: 100%; height: 100%; overflow: visible; }

.holocron__body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  /*width: 100%;*/
}

.holocron__stage {
  flex: 1 1 auto;
  max-width: 1236px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}

/* Display/Quote Display — Tektur Medium 32 / the font's own line height / +4,
   centred, in Text/Heading. The comp's text box is 1200 wide inside a 1344
   section; the three lines are hard breaks in the copy, not wrapping.

   `line-height: 1.31` was here from when the style had a fixed leading. The
   style says AUTO, so this says normal — a 32px line at 1.31 is 42px against
   the font's own ~38, which quietly stretched the block by 12px over 3 lines. */
.quote {
  margin: 0;
  max-width: 1200px;
  color: var(--ink);
  font-family: var(--display);
  font-size: clamp(19px, 1rem + 1.1vw, 32px);
  font-weight: 500;
  letter-spacing: 4px;
  line-height: normal;
  text-align: center;
  /* The lines are long and the tracking is wide; without this a narrow
     viewport pushes the quote past the section instead of breaking it. */
  overflow-wrap: break-word;
}
.quote__line { display: block; }

/* Body/Label Small in Text/Paragraph-pale. Was --muted (#6f8296); the comp
   is #7a8ca0, which is the pale-paragraph token. */
.holocron__source {
  margin: 0;
  color: var(--body-dim);
  font-family: var(--display);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 1.7px;
  text-align: center;
  text-transform: uppercase;
}

/* "HOLOCRON — SAYINGS" is the underlined half of the source line and the only
   part that links. The comp gives it no colour of its own — it is the same
   pale grey as the words before it, distinguished by the underline alone. */
.holocron__source-link {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.holocron__source-link:hover { 
    color: var(--cyan);
    text-decoration: none;
}

.holocron__source-link:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
  border-radius: 2px;
}

/* NO SLIDER ARROWS, NO STEPPER DOTS. The comp hides `arrow / prev`,
   `arrow / next` and `slider controls` in S6 · Holocron, so the section is
   a single quote. The ~75 lines that styled the two component sets
   "slider arrow" (60:8281) and "stepper dot" (60:8248) went with the
   markup and the script that drove them, along with the `.no-js` gate that
   existed only to hide controls JS could not power. */

/* ---------------------------------------------------------------------------
   S7 · CTA
   Comp: horizontal, space-between, centred. Copy fills to 1037 + gap 80 +
   actions 227 = 1344. Actions stack with a 16px gap at the right edge.
   ------------------------------------------------------------------------- */
/* counterAxisAlignItems = MAX in the comp: the actions column sits on the
   BOTTOM edge of the row, level with the last line of the copy, not centred
   against it. */
.section--cta {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--col-gap);
}

/* No max-width — the actions column is the fixed side, so the copy resolves to
   the comp's 1037 by itself. */
/* Width only — see .nma__copy. The 22ch cap on the heading went with it: the
   comp gives the CTA copy 1033 of the 1344 column and lets the title wrap
   where the line breaks in the strings say it does, which is what the module's
   titleLines already reproduce. */
.cta__copy { flex: 1 1 auto; }

/* Comp: 311 wide, vertical gap 32, both buttons FILL the column so they are
   the same width as each other rather than each hugging its own label. */
.cta__actions {
  flex: 0 0 min(311px, 23.14%);
  display: flex;
  flex-direction: column;
  gap: 32px;
}
.cta__actions .btn { width: 100%; }

/* ---------------------------------------------------------------------------
   Reveal states

   The initial hidden state is applied only when JS is running (.js on <html>,
   set before first paint). If the script never runs, everything is simply
   visible — the motion spec's rule that no animation blocks first paint and
   all copy is in the DOM beforehand.
   ------------------------------------------------------------------------- */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 480ms var(--ease-out), transform 480ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-revealed { opacity: 1; transform: none; }

/* The CTA heading may reveal line-by-line below the fold. The hero title is
   intentionally excluded so the LCP text is readable from first paint even
   if the page script is delayed or fails. */
.js .section--cta .jt-hero__line-inner {
  transform: translateY(110%);
  opacity: 0;
  transition: transform 620ms var(--ease-expo), opacity 620ms var(--ease-expo);
  transition-delay: var(--line-delay, 0ms);
}
.js .section--cta .is-revealed .jt-hero__line-inner,
.js .section--cta .jt-hero__line-inner.is-revealed { transform: none; opacity: 1; }

/* THE EYEBROW HAS NO ENTRANCE OF ITS OWN ANY MORE.

   The motion companion says "index fades in, rule draws L→R, label follows",
   written when every eyebrow was drawn with a rule. The set has had no rule for
   a while and now has no grey state either, so both halves of that spec have
   nothing left to act on: there is no stroke to draw and no colour to warm up.
   The eyebrow still rides the section's own `[data-reveal]` fade — it just no
   longer animates internally.

   Whether the slashes should get an entrance is a question for the motion
   frame, not something to invent here.

   The ladder's connector hairlines. The level columns had one each and the
   stats had one too; both went when their comps became bare frames with no
   rule, and their reveals went with them. */
.js .jt-ladder__link::before { transform: scaleX(0); transition: transform 400ms var(--ease-out); }
.js .jt-ladder__row.is-revealed .jt-ladder__link::before { transform: scaleX(1); }

/* Ambient loops. These run only once the figure has been revealed, so they
   never fight the draw-in.

   Paths the design draws dashed keep their pattern throughout — the script
   never touches stroke-dasharray on those. See prepPaths() in landing.js. */
/* The figure's ambient loops, its rotation groups and their keyframes moved to
   modules/balance/balance.css with the figure itself. The keyframes went too:
   they were named jt-spin / jt-spin-ccw / jt-pulse, and nothing else on this
   page ever used them. */

/* The motion spec's global rule: replace every transform with a short opacity
   fade, and stop all loops. */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js .section--cta .jt-hero__line-inner {
    transform: none !important;
    transition: opacity 120ms linear !important;
  }
  .js .jt-ladder__link::before { transform: scaleX(1) !important; transition: none !important; }
  /* Belt and braces: under reduced motion landing.js returns before it ever
     prepares a path, so no stroke-dashoffset is set. This keeps any that
     somehow survive from hiding artwork. */
  .js svg path { stroke-dashoffset: 0 !important; transition: none !important; }
  /* The figure stops itself — see modules/balance/balance.css. */
}

/* ===========================================================================
   Responsive — derived here, not drawn in Figma. See the header note.
   ========================================================================= */

/* The hero's two columns stop fitting. In the 1344 column the copy is 598
   beside a 666 figure; the figure shrinks with 42vw on the way down, but below
   ~1200 the copy is squeezed under ~500 and the two-column reading fails.
   Below that the art moves under the copy and centres. */
@media (max-width: 1200px) {
  .section--hero { flex-direction: column; align-items: flex-start; gap: 56px; }
  .hero__figure { --jt-balance-width: min(520px, 90vw); align-self: center; }
}

/* The three level columns are 427 at 1344 and shrink with the container. They
   hold their reading down to roughly 300 each, which the 1080 breakpoint keeps
   them above: 1080 − 64 gutters = 1016, minus 2 × 32 gap, / 3 = 317. Two
   columns first, then one at 720. */
@media (max-width: 1080px) {
  .levels { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; }
  .section--nma { gap: 64px; }
}

/* The NMA video, the ranks photograph and the community stats all stop earning
   their column at the same width — they are the same 316–318 side against the
   same copy, so they turn at the same breakpoint rather than each at its own. */
@media (max-width: 1000px) {
  .nma__top { flex-direction: column; }
  /* Stacked, the video and the photo keep their own intrinsic width rather
     than stretching to the full column: both are small, and a 1000px-wide one
     would be a band of crop. Centred under the copy instead. */
  .video { width: min(411px, 100%); align-self: center; }

  .ranks__top { flex-direction: column; }
  .ranks__photo { flex: 0 0 auto; width: min(411px, 100%); align-self: center; }

  .section--community { flex-direction: column; }
  .stats { flex: 1 1 auto; width: 100%; flex-direction: row; gap: 32px; }
}

/* The CTA band stacks; its buttons stop being a fixed-width column. */
@media (max-width: 900px) {
  .section--cta { flex-direction: column; align-items: stretch; gap: 40px; }
  .cta__actions { flex: 1 1 auto; width: 100%; flex-direction: row; flex-wrap: wrap; gap: 16px; }
  .cta__actions .btn { width: auto; min-width: 0; flex: 1 1 200px; }
}

/* The rank ladder's own narrow-viewport behaviour — scrolling rather than
   wrapping, because wrapping would put Magister under Novice and imply a reset
   — moved into modules/ladder/ with it. What is left here is the bleed: the row
   reaches the screen edges so it reads as scrollable rather than clipped, and
   that depends on THIS page's gutter, which the module cannot know. */
@media (max-width: 760px) {
  .ranks__ladder {
    margin-inline: calc(-1 * var(--gutter));
    padding-inline: var(--gutter);
    padding-bottom: 12px;
  }
}

@media (max-width: 720px) {
  .levels { grid-template-columns: minmax(0, 1fr); gap: 40px; }
  .holocron__body { gap: 4px; }
  .quote { letter-spacing: 2px; }
}

@media (max-width: 620px) {
  .stats { flex-direction: column; }
  .jt-hero__actions .btn { flex: 1 1 100%; }
}
