/* ============================================================================
   JT RANK SYSTEM PAGE — v2.0.0
   Figma "Content page · Rank system" (238:10751).

   Loads after common.css and content.css: the page reuses the content-page
   shell — the same panel, the same header, the same prose — and adds three
   things of its own, which is all that lives here.

     1. the hero ladder: seven insignia, six connectors, five sphere spans
     2. the gutter: an emblem and four chevrons that gather as you scroll
     3. the seven rank sections inside the panel

   RESPONSIVE — this one IS drawn, unusually, and it was RE-MEASURED
   2026-09-09 off section "Rank system - Hero responsive behavior", which now
   holds four frames rather than five:

     320–509    art 462 × 98, insignia 54, connectors HIDDEN — wider than the
                viewport, so the hero scrolls horizontally
     510–767    insignia 54, connectors HIDDEN — it fits without scrolling and
                stretches with the screen. The frame draws 462 because that is
                the art at the narrow end of the band, not a ceiling
     768–1024   art 709 × 98, insignia 54, connectors drawn, still stretching
     1025+      insignia 72 — and the row fills the panel column, which is
                1042 at its widest. The section's 1025 frame measures the art
                at 922, but that is the column at THAT width, not a ceiling

   The old table said insignia 72 from 768 up and named a 1440 band; both are
   gone from the design. The numbers that changed are exactly the ones that had
   been derived rather than read.
   ========================================================================== */

:root {
  /* The comp's ladder at its largest, and the insignia that set its rhythm. */
  /* RE-MEASURED. The rebuilt frame's `Hero pic` is 1043 wide at x 438, and the
     Content container is 1042 at x 439 — so the ladder is FLUSH with the panel.
     It was 1342.7 here, from the older comp where it deliberately overhung the
     panel by 150px on each side. That overhang is gone. */
  --ladder-max:        1042px;
  --ladder-insignia:   72px;

  /* The gutter column and the ornament inside it. */
  --gutter-width:      96px;
  --gutter-gap:        32px;
  --emblem-size:       96px;
  --chevron-width:     68px;
  /* The component's nominal box is 68 × 26.26, but its art bleeds 2.7px below
     that, so the whole-component export is 68 × 29. Size the element to the ART
     box: at 26.3 the SVG letterboxes itself and the chevron comes out narrow. */
  --chevron-height:    29px;
  /* Stacked chevrons overlap: the comp spaces them 13.3px apart, roughly half
     their height, so they nest like a chevron rank insignia rather than
     forming a column of separate marks. */
  --chevron-step:      13.3px;
  /* The first chevron sits 80px below the emblem's top edge. */
  --chevron-offset:    80px;
  /* The whole ornament sits this far below the row it belongs to. Read by
     scripts/ranks.js, which shifts the resting position, the float trigger,
     every gather point and the state marks together — so the hand-off stays
     seamless whatever this is set to. */
  --gutter-drop:       48px;
  /* How far AHEAD of ranks 06 and 07 the gathered chevrons change appearance —
     the bar at 06, the wings at 07. The marks are on the stack, which floats at
     the viewport centre, while the rank they announce is still below it; firing
     on arrival made the change read as a consequence of the rank rather than an
     announcement of it. Read by scripts/ranks.js. */
  --mark-lead:         96px;

  --rank-gap:          48px;
}

/* ---------------------------------------------------------------------------
   Hero ladder
   ------------------------------------------------------------------------- */
/* FLUSH WITH THE PANEL. The rebuilt frame puts `Hero pic` at x 438 × 1043 and
   the Content container at x 439 × 1042 — the same column. The older comp had
   the ladder overhanging by 150px each side; that is gone, and so is the
   1342.7px width that produced it.

   The 64px below is the Hero pic frame's own bottom padding. It used to be
   `var(--content-header-gap)`, which content.css stopped defining when the hero
   moved into its module — so this margin had silently become zero. */
.ladder-hero {
  width: 100%;
  max-width: var(--ladder-max);
  margin: 0 auto 64px;
}

/* IT SCROLLS OUT OF THE SCREEN, NOT OUT OF THE PADDING.

   The row rests where the rest of the page rests — on the gutter, level with
   the title above it — but when it scrolls it must disappear at the true screen
   edge. Anything else looks like a clipping bug: the ranks vanish against an
   invisible line a few pixels in, with empty page beside them.

   That is what `margin-inline: -gutter` + `padding-inline: gutter` buys. The
   box is widened until its edges ARE the screen's; the padding puts the content
   back on the gutter at rest. One pair of values, two jobs.

   THE HISTORY IS WORTH THE LINE. This is what the file did originally, keyed to
   --content-page-gutter. It briefly became a plain 8px padding — which narrowed
   the content box, so the row measured short of the panel — and then a padding
   of --content-pad-inline, which is the PANEL's inner padding and not the page
   margin, so the row clipped early. The gutter is the right variable because it
   is the distance from the screen edge to the column the row sits in, which is
   exactly the distance the row has to bleed to reach the edge again.

   It also gives the overhanging rank labels room at every width, so the 8px
   allowance that was here for them is gone. */
.ladder-hero__scroll {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  margin-inline: calc(-1 * var(--content-page-gutter));
  padding-inline: var(--content-page-gutter);
}

/* THE STAGE IS WHAT THE BREAKPOINTS MOVE, because it is the one box that holds
   BOTH parts — the ladder and the sphere spans beneath it. The comp's
   "Rank system - Hero responsive behavior" draws them at one width per band
   (462 / 462 / 710 / 923), never at two, and the spheres are placed in percent,
   so sizing this element carries them along for free.

   THE LADDER IS FLUSH WITH THE PANEL, and 922 IS NOT A CAP. The responsive
   section draws the art 922 wide in its 1025 frame, and that was briefly read
   here as the art's natural maximum — so above 1280 the ladder sat 120px
   narrower than the panel beneath it, which is what it looked like: a row that
   had stopped growing for no reason.

   922 is simply what the column measures in a 1025-wide frame. The page comp
   is the one that settles it: `Hero pic` is 1043 at x 438 and the Content
   container 1042 at x 439 — the same column. The art fills whatever column it
   is given, and --ladder-max IS that column. */
.ladder-hero__stage {
  position: relative;
  width: 100%;
  max-width: var(--ladder-max);
  /* A min-width DOES apply below 768 — the comp's 462, set at that breakpoint.
     Above it the module's own floor decides when the row stops squeezing, via
     --jt-ladder-link-min and `min-width: min-content` on its row. */
  margin-inline: auto;
}

/* --- the ladder ----------------------------------------------------------
   Rendered and styled by modules/ladder/ — the same component the landing page
   shows; this page's `Hero pic` holds an instance of the landing's master.
   Everything that was here (.ladder__rank, .ladder__insignia, .ladder__index,
   .ladder__name, .ladder__link and its ornaments) went with it.

   THE DIAMOND AND ARROW ARE BACK, and this note is the third thing it has said.
   They were drawn, then removed in favour of round caps on both ends because
   the comp had changed, and the comp has since changed again: the connector is
   one vector with DIAMOND_FILLED where it leaves a rank and ARROW_EQUILATERAL
   where it arrives. Re-read 2026-09-09. The module draws it, once, for both
   pages — see modules/ladder/ladder.css.

   This page keeps only its own contributions: how big the insignia are at each
   breakpoint, whether the connectors draw at all, and the sphere spans. */
.ladder-hero__ladder {
  --jt-ladder-insignia-w: var(--ladder-insignia);
  --jt-ladder-insignia-h: calc(var(--ladder-insignia) * 78 / 72);
  /* This page's ladder is narrower than the landing's — the comp gives it 88px
     connectors against 181 — so it may squeeze further before scrolling. */
  --jt-ladder-link-min: 8px;
  /* The stage already scrolls; a second scroller inside it would trap the
     gesture between them. */
  overflow: visible;
}


/* --- the five sphere spans --- */
/* Three stacked lines under the ladder, at the comp's 185 / 215 / 247. */
.spheres {
  /* Hero pic: VERTICAL gap 56 between the ladder and this group, which is 80
     tall — both unchanged in the rebuilt frame. */
  position: relative;
  height: 80px;
  margin-top: 56px;
}

.sphere {
  position: absolute;
  left: var(--sphere-left);
  width: var(--sphere-width);
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.sphere[data-row="1"] { top: 0; }
.sphere[data-row="2"] { top: 30px; }
.sphere[data-row="3"] { top: 62px; }

/* The span lines end in filled dots. In the comp this is one 1px vector with
   CIRCLE_FILLED on both vertices, so the dots are CENTRED on the line's two end
   points — the line spans centre to centre, and each dot overhangs it by half
   its own diameter. Exported diameter: 5.3333.

   Pseudo-elements rather than a background-image or a wider box, so the dots
   stay welded to the ends however the percentage-placed span resizes. */
.sphere__line {
  position: relative;
  display: block;
  height: 1px;
  background: rgba(0, 212, 255, 0.6);
  --dot: 5.3333px;
}

.sphere__line::before,
.sphere__line::after {
  content: "";
  position: absolute;
  top: 50%;
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: rgba(0, 212, 255, 0.6);
}
.sphere__line::before { left: 0;  transform: translate(-50%, -50%); }
.sphere__line::after  { right: 0; transform: translate(50%, -50%); }

.sphere__label {
  /* UPPER in the comp, like every other label on this drawing. Set here rather
     than uppercased in the strings so the Russian source stays readable and the
     three languages cannot disagree about it. */
  text-transform: uppercase;
  /*color: var(--ink);*/
  font-family: var(--display);
  font-size: 10px;
  font-weight: 400;
  line-height: 1.3;
  text-align: center;
}

/* ---------------------------------------------------------------------------
   The panel's contents
   ------------------------------------------------------------------------- */
/* THE CONTENT-VISIBILITY OPT-OUT IS GONE, because there is nothing left to opt
   out of. content.css no longer puts `content-visibility: auto` on `.prose > *`
   — it made the document shrink while the reader scrolled and left empty space
   below the footer; the note there has the measurements.

   Recorded because the reasons this page needed the opt-out are the reasons
   NOT to reintroduce the property without checking here first:

     • the grid is a single ~3400px element, so a `contain-intrinsic-size`
       placeholder is out by a factor of thirty and the grid reports a 120px box
       until rendered — and scripts/ranks.js measures that box to place the
       ornament, so it must be real;
     • `content-visibility: auto` implies `contain: layout style paint`, which
       makes the element a containing block for `position: fixed` descendants,
       and the gutter section below depends on where that lands.

   `content-visibility` is not inherited and its initial value is `visible`, so
   removing the declaration changes nothing today. */

.rank-system__intro { max-width: var(--measure); }
.rank-system__intro p + p { margin-top: 1.15em; }

/* anchor | list. The left column is ornament, so it is aria-hidden and
   contributes nothing to the reading order. The cell is EMPTY — see the gutter
   section below for why the ornament itself lives outside this panel. */
.rank-system__grid {
  display: grid;
  grid-template-columns: var(--gutter-width) 1fr;
  gap: var(--gutter-gap);
  margin-top: 64px;
}

.rank-system__list { display: flex; flex-direction: column; gap: var(--rank-gap); }

/* A hairline above every rank, including the first — the comp draws one before
   rank-01 and between each pair. */
.rank { position: relative; padding-top: var(--rank-gap); }
.rank::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: rgba(125, 211, 252, 0.14);
}

/* Frame 253 in the comp: a vertical auto-layout with an 8px gap and no padding.
   The gap alone is not enough — see the specificity note further down, where
   `.prose h2`'s 1.8em top margin was adding ~68px to it. */
.rank__head { display: flex; flex-direction: column; gap: 8px; }

/* Each rank's eyebrow carries its sphere's colour, which build/ranks.js writes
   as --eyebrow-ink from the strings file. All this page adds is the dimming.

   The comp draws all seven at 50%, and the reader's own rank should be the one
   that is fully lit — so an eyebrow comes up to full as it crosses the viewport
   centre, the same line the emblem stack is pinned to. It LATCHES: once lit it
   stays lit, matching every other `is-in-view` on the site, and scrolling back
   up does not put the page back into a dimmer state than the reader has
   already seen. scripts/ranks.js sets the class. */
/* Doubled selector, and `margin: 0` restated, for the same reason as the block
   further down: this eyebrow is a <p> inside `.prose`, so `.prose p`'s
   `margin-bottom: 1.15em` outranked the component's own `margin: 0` and added
   ~15px under it — the 8px head gap was measuring 23. */
.rank .rank__eyebrow {
  margin: 0;
  opacity: 0.5;
  transition: opacity 420ms var(--ease-out);
}
.rank .rank__eyebrow.is-in-view { opacity: 1; }

/* FULLY LIT ON MOBILE, and the breakpoint is the one the ornament already uses.

   The dimming is half of a pair: an eyebrow comes up as it crosses the viewport
   centre, which is the line the floating emblem stack is pinned to. Below 768
   that stack does not exist — `.gutter` is `display: none` — so the other half
   of the pair is missing and all that is left is a rank's own name at half
   opacity, with nothing on screen to explain why.

   It reads as unfinished rather than as anticipation, and on a phone these
   labels are the page's main way of telling you which rank you are looking at.
   So below 768 they are simply legible.

   scripts/ranks.js keeps running and keeps latching `is-in-view` — it costs
   nothing and means a resize up past 768 finds the right state already set
   rather than a screen of eyebrows that never dimmed. */
@media (max-width: 767px) {
  .rank .rank__eyebrow { opacity: 1; }
}

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

/* EVERY SELECTOR BELOW IS DOUBLED — `.rank .rank__name`, not `.rank__name`.

   These are an <h2> and two <p> inside `.prose`, and content.css styles those
   generically: `.prose h2` (0,1,1) and `.prose p` (0,1,1) both outrank a bare
   `.rank__name` (0,1,0), so the values written here were being thrown away
   silently. The rank titles were rendering at the prose heading's clamp — up to
   38px, weight 700, negative tracking — instead of the comp's 20/500/+5, and
   `.prose h2`'s `margin-top: 1.8em` was opening ~68px between the eyebrow and
   the title on top of the 8px the head frame asks for.

   Adding `.rank` takes these to (0,2,0), which wins. Anything else added here
   for a prose element needs the same treatment. */

/* Heading/H4 — Tektur SemiBold 24/AUTO with 2px tracking. */
.rank .rank__name {
  margin: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: 24px;
  font-weight: 600;
  letter-spacing: 2px;
  line-height: normal;
  text-wrap: initial;
}

/* No shared style behind this one — it is set by hand on the node, so the
   values are transcribed rather than named: Exo 2 Light Italic 14/AUTO, 4px. */
.rank .rank__motto {
  margin: 32px 0 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 14px;
  font-style: italic;
  font-weight: 300;
  letter-spacing: 3.2px;
  line-height: normal;
}

/* Body/Paragraph — Exo 2 Light 16/26 with 1px tracking. */
.rank .rank__body {
  margin: 32px 0 0;
  color: var(--body);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 300;
  letter-spacing: 1px;
  line-height: 26px;
}

/* ---------------------------------------------------------------------------
   The gutter ornament

   Resting state: the emblem beside rank-01 and one chevron beside each of
   ranks 02–05, all scrolling with the page like any other content.

   scripts/ranks.js takes over from there — see it for the state machine. It
   sets .is-floating on the gutter and data-gathered on each chevron; this file
   only says what those states look like.

   WHY THE GUTTER IS NOT IN THE GRID
   `.gutter-anchor` is: an empty cell that holds the left column open and gives
   the script something panel-relative to align to. The gutter itself is emitted
   as a SIBLING of `.content-container` and positioned onto the page from there,
   because the emblem pins itself with `position: fixed` — and a fixed element is
   fixed to the VIEWPORT only if no ancestor has claimed it.

   `.content-container` carries `backdrop-filter: blur(20px)`. A backdrop-filter,
   like a filter, a transform, `will-change` on either, or `contain: paint`,
   MAKES AN ELEMENT A CONTAINING BLOCK FOR ITS FIXED DESCENDANTS. Inside the
   panel the emblem was pinned to the panel — which scrolls — so viewport offsets
   were applied against a moving origin and it drifted off the page.

   Anything that must pin to the viewport therefore has to be emitted outside the
   glass panel. There is no CSS escape from within it.
   ------------------------------------------------------------------------- */
/* Out of flow entirely: the script writes top / left / height in document
   space. Being out of flow, writing them back cannot disturb the layout it has
   just measured. */
.gutter {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--gutter-width);
  z-index: 1;
}

/* Until it has been placed it has no meaningful position, so it is not shown.
   That also covers JS being off: this is decoration, and a decoration parked at
   the top-left corner of the document is worse than none at all. */
.gutter:not([data-placed]) { visibility: hidden; }

/* Positioned in the gutter at rest and when parked; the script writes `top`.
   While floating it becomes fixed — see the phase rules below. */
.gutter__emblem {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--emblem-size);
  height: var(--emblem-size);
}
.emblem { display: block; width: 100%; height: 100%; }

.gutter__chevron {
  position: absolute;
  left: 14px;                       /* the comp centres 68 in the 96 column */
  width: var(--chevron-width);
  height: var(--chevron-height);
  /* Each chevron rests beside its own rank; the script measures where and sets
     this. Until it runs they sit in document flow order below the emblem. */
  top: var(--chevron-rest, 0px);
  transition: transform 220ms var(--ease-out);
}
.chevron { display: block; width: 100%; height: 100%; overflow: visible; }

/* The two state marks are hidden in the component and shown by the scroll
   states — rank-06 shows the wide bar, rank-07 swaps it for the pair. */
.chevron__bar,
.chevron__wing { opacity: 0; transition: opacity 200ms var(--ease-out); }

.gutter[data-state="bar"]   .chevron__bar  { opacity: 1; }
.gutter[data-state="wings"] .chevron__wing { opacity: 1; }

/* PHASES — scripts/ranks.js sets data-phase and writes the offsets.

     rest   nothing is written; the ornament scrolls with the document
     float  pinned to the viewport centre, so the offsets are VIEWPORT-space
     park   stopped at the end of the list, so they are gutter-space again

   Only `float` changes the positioning scheme. `park` deliberately keeps the
   chevrons gathered: the stack the reader watched assemble should not fly back
   apart at the last rank. */
.gutter[data-phase="float"] .gutter__emblem { position: fixed; z-index: 2; }
.gutter[data-phase="float"] .gutter__chevron[data-gathered="true"] {
  position: fixed;
  z-index: 1;
}
.gutter__emblem { z-index: 2; }
.gutter__chevron[data-gathered="true"] { z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .gutter__chevron,
  .chevron__bar,
  .chevron__wing { transition: none; }
}

/* ===========================================================================
   Responsive — the hero widths ARE drawn; the gutter's are not.
   ========================================================================= */

/* THE FOUR BANDS ARE DRAWN, not derived. Figma section "Rank system - Hero
   responsive behavior" holds one frame per band, and this is what they measure:

     band        frame   ranks · line-art   insignia   connectors
     1025+       1025    922 × 129          72         drawn
     768–1024     768    709 ×  98          54         drawn
     510–767      510    462 ×  98          54         HIDDEN
     320–509      320    462 ×  98          54         HIDDEN

   Two things fall out of that table and neither was in the code before.

   THE INSIGNIA DROP TO 54 AT 1024, not at 767. The 768 frame draws them at 54
   already; the old rule waited until 767, so the whole 768–1024 band rendered a
   ladder a third larger than drawn.

   THE CONNECTORS ARE SWITCHED OFF BELOW 768, line and arrowheads together —
   the frames set the connector invisible, not merely uncapped. The ranks stay
   evenly spread because the grid track survives; only its ink goes. */

/* 768–1024: smaller insignia, connectors still drawn, width free to stretch. */
@media (max-width: 1024px) {
  :root {
    --ladder-insignia: 54px;
  }
  /* The label type steps with the insignia — the comp draws 9.75/8.25 in every
     frame below 1025, against 13/11 in the 1025 one. Leaving them at full size
     while the insignia shrank is what made the names collide. */
  .ladder-hero__ladder {
    --jt-ladder-index-size: 9.75px;
    --jt-ladder-name-size:  8.25px;
  }
  /* Smaller insignia need their labels further apart, not tucked under — this
     travels with --ladder-insignia, not with the phone breakpoint. */
  .ladder-hero__ladder { --jt-ladder-label-lift: 0px; }
  .spheres { margin-top: 44px; }
}

/* Below 768 the connectors stop drawing, and the row gets a floor: seven 54px
   insignia and six 14px gaps is the comp's 462, which is also the narrowest the
   art is ever drawn. Above 510 that fits and the row simply stretches; below
   it, it cannot, and .ladder-hero__scroll takes over — one rule, two outcomes,
   which is why there is no separate 510 breakpoint here. */
/* Below 768 the gutter goes entirely: 96px plus a 32px gap is a third of a
   phone screen, and it is ornament. The ranks take the full column, and
   scripts/ranks.js does not run its stack at all at this width. */
@media (max-width: 767px) {
  /* --ladder-insignia is NOT set here any more. It moved up to the 1024 band,
     because that is where the comp drops the insignia to 54 — keeping a second
     copy here would have made the 768–1024 band the only one still at 72. */
  .ladder-hero__ladder {
    --jt-ladder-link-ink: hidden;
    /* With the connectors gone the tracks close to about 14px, and the rank
       names — wider than their 54px column in every language — ran into each
       other: at 375 the RU labels overlapped by 7 and 11px, EN by 7 and 3, PL
       by 7. The label sizes above are the real fix — they were the measurement
       that had been missed — and this is the safety net beside them: letting a
       column hug its own label is the only version that a longer translation
       cannot break, where a chosen gap would hold only until the next word. */
    --jt-ladder-rank-w: auto;
  }
  /* A FLOOR, NOT A WIDTH — the row stretches to the column and never shrinks
     below what the labels need.

     The row is `width: 100%` of this element, so this element's width IS the
     grid's width. Pin it and the grid, unable to fit its `auto` tracks, shrinks
     them below their content rather than overflowing: the widest label absorbed
     the remainder and overlapped its neighbour while the narrower ones beside
     it were sized correctly.

     `min-width: max-content` with the base rule's `width: 100%` reads as "as
     wide as the column, but never narrower than the labels". Below 510 the
     content wins and the hero scrolls; from 510 up the column wins and the row
     grows with the screen, which is what the brief asked for.

     I ARGUED AGAINST THIS AND WAS WRONG TWICE OVER. First I read the comp's
     320–509 and 510–767 frames — both 462 — as "fixed below 768", and set
     `width: max-content`, which froze the row and stopped it stretching. The
     462 is simply the art at its natural size at the narrow end of the band,
     not a ceiling. Second, I had tried `min-width: max-content` earlier,
     measured it computing to 0px, and ruled it out — but at that moment an
     unterminated comment a few lines above had swallowed the rule, so I was
     measuring its absence. The lesson is in the comment balance check now.

     THE SPHERES ARE WHY THIS SITS ON THE STAGE rather than on the row: they are
     placed in percentages of this element, so this is the box that must size,
     or every span slides out of line with the ranks it labels. */
  .ladder-hero__stage  { min-width: max-content; }
  .rank-system__grid { grid-template-columns: 1fr; gap: 0; }
  /* The anchor collapsing to zero width is also the script's signal to stop
     placing the gutter — see place() in scripts/ranks.js. */
  .gutter-anchor { display: none; }
  .gutter { display: none; }
  .spheres { height: 66px; margin-top: 32px; }
  .sphere[data-row="2"] { top: 24px; }
  .sphere[data-row="3"] { top: 50px; }
  .sphere__label { font-size: 8.25px; }
}

/* 360–509: the ladder cannot shrink below the comp's 461.6, so the hero scrolls
   sideways and bleeds to the screen edges to make that obvious. */
/* The bleed lives in the base rule now — it is the same at every width,
   because the gutter it bleeds by already steps with the breakpoints. */

@media (max-width: 509px) {
  /* Room for the scrollbar the row now needs. */
  .ladder-hero__scroll { padding-bottom: 8px; }
}
