/* ============================================================
   bpa-bifrost, shared site layer
   ------------------------------------------------------------
   Loads AFTER brands/bifrost/group.css. Everything here is a
   PROMOTION CANDIDATE: markup the Figma draws that the engine
   (core/ui-core.css §6.3) does not yet own. It is deliberately
   short, and it should get shorter as pieces move upstream into
   the engine. Nothing here invents a value: every number is a
   token or a measurement off the Figma frame, and there is not
   one raw hex.

   Tracked in DECISIONS.md. Do not add to this file without
   asking first whether the thing belongs in the engine instead.
   ============================================================ */


/* ------------------------------------------------------------
   G1 · THE HERO BAND                              PROMOTE
   ------------------------------------------------------------
   Figma draws two different 100vh openers and the engine only
   implements one of them:

     "General Intro (100vh)"  -> .ui-section--intro   EXISTS
     "Hero (100vh)"           -> nothing              MISSING

   They are not the same component. --intro puts the art band on
   top and a title/aside ROW underneath. The Hero centres a
   title, a lead and a button row in the band itself, and the
   arch motif sits behind them, clipped by the section's bottom
   edge.

   All four brand Homes use the Hero. It belongs in the engine as
   .ui-section--hero, not here. Built on --intro's own tokens so
   the promotion is a move, not a rewrite.

   Measured off 40004577:9469 (1440 x 766): section 766 tall,
   title block at y 238, buttons at y 455, arch clip box at
   x 544 / y 509, 896 x 257.
   ------------------------------------------------------------ */
.bf-hero {
  --ui-section-inset: var(--s-w-s);
  --ui-intro-offset: 0px;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: calc(100vh  - var(--ui-intro-offset));
  min-height: calc(100svh - var(--ui-intro-offset));
  padding-block: 0;
  gap: 0;
  overflow: hidden;                 /* the clip IS the arch design */
}

/* The arch field. Bottom-right, clipped by the section. Sized as
   a fraction of the band rather than the drawn 896px, because the
   drawn box is 62% of the 1440 frame and that ratio is what
   survives three grids. */
.bf-hero__band {
  /* A flow item, not an overlay: the file gives the band the bottom
     third of the section (509 of 766 = 33.5%) and puts the content
     stack in what is left, so the buttons never sit on the arcs. */
  position: relative;
  flex: 0 0 auto;
  /* 509 of 766 leaves 33.5% of the section for the band. Typed in
     svh rather than % because the hero's height comes from
     min-height, and a percentage block-size against an indefinite
     parent height collapses to zero. min-block-size keeps the art
     alive on a short viewport. */
  block-size: 33.5svh;
  min-block-size: var(--s-w-xxl);
  inline-size: 100%;
  align-self: stretch;
  overflow: hidden;
  pointer-events: none;
}

/* The content stack sits above the art. */
.bf-hero__inner {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: var(--s-w-s);
  inline-size: 100%;
  /* The hero's own inset is S, but its content column is the grid's.
     This pays the difference so the title measure matches every other
     band's.

     REBUILT 26 August. Lasse: the hero text has to follow the same
     margins as the rest of the page. It did not, and the reason is
     box-sizing: the engine sets border-box on .ui-section and this
     element is not one, so 100% PLUS the padding overflowed the hero
     by the padding on each side, and the flex centring split the
     overflow symmetrically. On Group's centred stack that is
     invisible (a symmetric box centres the same text). On Studios'
     left-aligned stack it put the title at exactly the hero's own
     20px inset at every width: measured 20 against the bands' 170
     at 1440. border-box makes the padding real, and the content box
     becomes the grid's 1100 at 1440, which is what the comment above
     always claimed.

     Also FLOORED at the band inset, for exactly the reason G4 floors
     the plated bands: the tablet grid margin is 20px and the band
     inset is also 20px, so the difference came out at 0 and the hero
     text sat at 20 while every plated band's text sat at 40 (plate
     inset plus G4's floor). Same three-width shape as G4's own fix:
     1440 and 393 already agree, tablet is the width that moves. */
  box-sizing: border-box;
  padding-inline: max(var(--bf-band-inset), calc(var(--grid-margin) - var(--bf-band-inset)));
}

/* Figma draws the hero title at H2, not H1: "Title h2", one line
   45px tall and two lines 109, which is 64px on the 100% leading
   the type scale specifies. So the element is an <h1> for the
   document outline and .ui-h2 for the size. Recorded as a
   divergence in DECISIONS.md rather than silently corrected. */
.bf-hero__title {
  max-inline-size: var(--ui-section-title-max, 100%);
  margin: 0;
}

.bf-hero__lead {
  /* The reading measure, 540, exactly as the file draws it. The
     section's 49.09% fraction is relative to a content box the hero
     does not have. */
  max-inline-size: var(--ui-measure, 540px);
  margin: 0;
}

.bf-hero__actions {
  margin-block-start: var(--s-w-s);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-w-xs);
}

/* The band is a decoration, and on a short viewport it is the
   first thing that should give. */
@media (max-width: 639px) {
  .bf-hero { min-height: 0; }
  .bf-hero__inner { padding-block: var(--s-w-xl) 0; }
  .bf-hero__band { block-size: var(--s-w-xxl); }
}


/* ------------------------------------------------------------
   G2 · THE STATS BENTO                            PROMOTE
   ------------------------------------------------------------
   .ui-stats is drawn and built as a four-track row
   (212.5 / 212.5 / 353 / 262) with a --tall modifier. Home draws
   something else: 40004577:9421 is three cells, 258 / 544 / 258
   at a 20 gutter, and the middle cell is its own 2 x 2 grid with
   the top tile spanning both columns.

       +-------+---------------+-------+
       |       |     wide      |       |
       | tall  +-------+-------+ tall  |
       |       | small | small |       |
       +-------+-------+-------+-------+

   258 + 20 + 544 + 20 + 258 = 1100, the desktop content box.
   190 + 20 + 190 = 400, the drawn block height.

   Belongs in the engine as a .ui-stats--bento body variant: the
   head, the tokens and .ui-stat itself all carry over unchanged,
   only the body grid differs. Two of the four brand Homes are
   likely to want it.
   ------------------------------------------------------------ */
.ui-stats--bento          { grid-auto-rows: auto; }
.ui-stats--bento__group   { display: contents; }

@media (min-width: 640px) {
  .ui-stats--bento        { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1120px) {
  .ui-stats--bento {
    /* The drawn track widths, as ratios, exactly as §6.3's own
       stats grid states them. */
    grid-template-columns: 258fr 544fr 258fr;
    grid-auto-rows: minmax(400px, auto);
  }
  .ui-stats--bento__group {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(2, minmax(0, 1fr));
    gap: var(--grid-gutter);
  }
  .ui-stats--bento__wide { grid-column: 1 / -1; }
}


/* ------------------------------------------------------------
   THE UNRESOLVED STAT VALUES
   ------------------------------------------------------------
   Three of the five tiles read a literal X in Figma. They are
   not invented here. This makes the placeholder legible as a
   placeholder instead of shipping as a design choice, and it is
   removed the moment real figures arrive (PROJECT.md, open
   decision 4).
   ------------------------------------------------------------ */
.ui-stat__value[data-placeholder] { color: var(--ui-ink-40, var(--c-ink-2)); }


/* ------------------------------------------------------------
   NOT A GAP, recorded so nobody re-solves it
   ------------------------------------------------------------
   Two things that looked like gaps on a first read and are not:

   1. Sector Exposure's thirteen icon chips. .ui-pill already has
      .ui-pill__icon, .ui-pill-cloud already wraps two centred
      rows, and every sector mark is already in
      brands/bifrost/assets/bifrost-icons.svg as bf-i-<slug>.
      templates/sections/editorial.html ships this exact block.

   2. The Thesis. It is .ui-section--content, text-only variant.
      There is no --content-text modifier by design: text-only and
      image-left are one layout, and an empty column 1-6 is the
      whole difference.
   ------------------------------------------------------------ */


/* ------------------------------------------------------------
   G3 · THE PAGE SHELL THAT PAYS NOTHING            PROMOTE
   ------------------------------------------------------------
   Two rules in the engine disagree about where a band lives:

     ui-core.css ~4975  "a section that paints a ground has to be
                         page-level, i.e. OUTSIDE .ui-page"
     validate.mjs       errors when there is no .ui-page at all

   Every proof page in templates/sections/ resolves this by having
   a .ui-page that holds only the lab notes. A real page has no lab
   notes, so it needs a shell that names the medium and then gets
   out of the way: no inline margin (each band pays its own), no
   block padding (each band pays its own), and no reassignment of
   --ui-section-inset to 0, which is what nesting normally does and
   what would inset the full-bleed hero and the accent CTA band.

   Upstream this should be either a .ui-page--bands variant in the
   engine or a validator that accepts page-level sibling bands.
   ------------------------------------------------------------ */
.ui-page--bands {
  padding-inline: 0;
  padding-block: 0;
}
.ui-page--bands > .ui-section {
  --ui-section-inset: var(--grid-margin);
  padding-inline: var(--ui-section-inset);
}
.ui-page--bands > .bf-hero {
  --ui-section-inset: var(--bf-band-inset);
  padding-inline: var(--ui-section-inset);
}


/* ------------------------------------------------------------
   G4 · SECTIONS ARE PLATES, NOT GROUND              PROMOTE
   ------------------------------------------------------------
   The largest visual delta between the engine and the file, and
   it is systematic rather than a Home quirk.

   The engine paints .ui-section with --ui-ground (Background/Base,
   #DED9D6) and runs it full-bleed, so a stack of sections is one
   uninterrupted field. The file draws every band as a 1400-wide
   PLATE (Background/Core, #EFEBE9) inset 20px inside the 1440
   frame, with --radius-component corners, on the Base ground. The
   20px gaps between bands are what make the ground visible at all.

   Every frame in every one of the four brand sections is drawn
   this way, so this is the engine's §6.3, not Bifrost's Home. It
   should become the default for .ui-section, or a documented
   --plated variant.

   The hero is the exception: full-bleed, no inset, no radius,
   because the navbar sits on it. The footer is the other: it
   stays on the ground, which is what the engine already does.
   ------------------------------------------------------------ */
.ui-page--bands > .ui-section:not(.bf-hero):not(.bf-pagehead) {
  border-radius: var(--radius-component);
  margin-inline: var(--bf-band-inset);
  inline-size: auto;
  /* The content column still lands on the grid margin: the band's own
     inset plus what is left of it.

     FLOORED at the band inset. The engine's tablet grid margin is 20px
     (--grid-margin-t, off the drawn 834 frame), and the plate's own
     margin is also 20px, so the subtraction came out at exactly 0 and
     every band's text sat flush against the plate's edge at 834. Found
     20 August while measuring the legal pages: prose x = 20 in a plate
     starting at 20. Type touching the boundary of the surface it sits on
     is wrong whatever the token arithmetic says. No effect at 1440
     (149.92px) or 393 (20px); tablet only. */
  --ui-section-inset: max(
    var(--bf-band-inset),
    calc(var(--grid-margin) - var(--bf-band-inset))
  );
}
/* The CTA is excluded from the plate fill and only from that: it is
   the one section that paints its own ground (the accent), and
   --ui-ink-on-accent makes its title and lead knockout, so a plate
   underneath them renders the whole band blank. It still takes the
   inset and the radius. */
.ui-page--bands > .ui-section:not(.bf-hero):not(.ui-section--cta) {
  background: var(--ui-plate);
}

/* The band gap: superseded by R1 at the end of this file, which covers the
   hero-to-first-band seam this selector missed. Left as a marker so the
   history of the bug is visible where the rule was. */
.bf-hero { background: var(--ui-plate); }
/* The navbar sits on the hero plate, not on the ground. */
.ui-nav { background: var(--ui-plate); }


/* ------------------------------------------------------------
   G5 · THE CARDS HEAD IS CENTRED ON HOME            RAISE
   ------------------------------------------------------------
   The engine centres the head for --pills and --stats and leaves
   --cards-aligned left. The file centres all three on Home
   (40004577:9240: title text at x 280 in an 1100 box, which is the
   540 measure centred).

   Not obviously an engine change: --cards-aligned may legitimately
   be left-headed elsewhere. Raised for design rather than pushed
   upstream.
   ------------------------------------------------------------ */
.ui-section--cards-aligned .ui-section__head { align-items: center; }
.ui-section--cards-aligned .ui-section__title { text-align: center; margin-inline: auto; }


/* ------------------------------------------------------------
   G4a · HERO BUTTONS ON A PLATE                     RAISE
   ------------------------------------------------------------
   Consequence of G4, not a separate finding. A .ui-btn at rest
   takes its plate from the background tokens, which is Core. The
   hero is now also Core, so the button disappears into it. The
   file draws the two hero buttons on Base against the hero's
   Core, i.e. the plate steps DOWN rather than up.

   Whether the button should invert like this on a plated section,
   or whether plated sections should use --ui-plate-up for their
   controls, is an engine question. One rule here, and it is the
   whole extent of the workaround.
   ------------------------------------------------------------ */
.bf-hero .ui-btn:not(.ui-btn--outline) { background: var(--ui-ground); }


/* ------------------------------------------------------------
   FIXES FROM THE FIRST LIVE REVIEW, 13 August 2026
   ------------------------------------------------------------
   Three things Lasse caught on the deployed page, all three
   confirmed against the Figma frame before changing anything.
   ------------------------------------------------------------ */

/* R1 · The ground has to show between the hero and the first band.
   The earlier rule was `:not(.bf-hero) + .ui-section`, which gives a
   top margin to every band EXCEPT the first one after the hero, since
   that one's previous sibling IS the hero. So the hero plate and the
   first section plate butted together and the 20px of ground never
   appeared. Every band gets the gap. */
.ui-page--bands > .ui-section + .ui-section {
  margin-block-start: var(--bf-band-inset);
}
/* The page head carries the navbar, so it is the first band and takes no
   gap above it, for the same reason the hero does not. Named here because
   G4's exception list is the one place that should know about it. */
.ui-page--bands > .bf-pagehead { margin-block-start: 0; }

/* R3 · The band inset is not one value.
   Measured off the CTA band on all three frames:

       mobile   393   inset 11   -> XS
       tablet   834   inset 20   -> S
       desktop 1440   inset 21   -> S

   So it is XS below the tablet breakpoint and S above it. The earlier code
   used S everywhere, which inset the mobile plates by twice the drawn
   amount. One token, used by the plate margin, the band gap, the section
   inset arithmetic and the footer, because in the file they are all the
   same measurement. */
:root { --bf-band-inset: var(--s-w-xs); }
@media (min-width: 640px) { :root { --bf-band-inset: var(--s-w-s); } }


/* R2 · The footer spans the same width as the CTA band.
   The engine gives .ui-footer `padding-inline: var(--grid-margin)`, so
   its content sat at 170 while the accent band above it started at 20.
   Measured on the frame: footer content starts at 24 and the newsletter
   ends at 1418, against the band's 20 to 1420. So the footer is inset by
   S like every other band, not by the grid margin. The 4px is the
   column title's own --s-w-xxs padding.

   PROMOTE alongside G4: a plated-section engine should inset the footer
   the same way, since the two decisions are the same decision. */
.ui-footer {
  padding-inline: var(--bf-band-inset);
}


/* ------------------------------------------------------------
   G6 · THE SYSTEM COLOURS ARE NOT DECLARED         PROMOTE
   ------------------------------------------------------------
   ui-core.css §6.3's meter says, in its own comment, that it
   "reads --c-system-* and does not declare" them, and nothing in
   core/ or brands/ declares them either. So every .ui-meter dot
   falls back to var(--ui-ink) and the whole scorecard renders as
   four black dots per row, which destroys the one thing it is
   for: Apollo scores red to green.

   Sampled off the Studios Apollo frame (40004577:15329):

       step 1  #FF5001   (the Forge orange, reused as the red)
       step 2  #FF8300
       step 3  #FFB259   (the Studios accent)
       step 4  #3ABFAD
       empty   the page ground, which the engine already gives

   These are HOUSE values, not Studios values: the same ramp would
   score a Forge or a Group entity. They belong in
   brands/bifrost/tokens/bifrost.tokens.json under shared, next to
   the fonts, so the generator emits them into all four packs.
   They are here because this repo does not regenerate the packs.
   ------------------------------------------------------------ */
:root {
  --c-system-red:    #FF5001;
  --c-system-orange: #FF8300;
  --c-system-yellow: #FFB259;
  --c-system-green:  #3ABFAD;
}


/* ------------------------------------------------------------
   G5a · THE STACKED CARDS HEAD IS CENTRED TOO
   ------------------------------------------------------------
   Same finding as G5, one section along. The file centres the head
   on Studios' stage cards (40004577:15442) exactly as it does on
   Group's aligned cards, and the engine leaves both left.
   ------------------------------------------------------------ */
.ui-section--cards-stacked .ui-section__head { align-items: center; }
.ui-section--cards-stacked .ui-section__title { text-align: center; margin-inline: auto; }


/* ------------------------------------------------------------
   G7 · THE ARCHES LIGHT UP UNDER THE CURSOR      REMOVED, WRONG SHAPE
   ------------------------------------------------------------
   Open decision 10, answered by Lasse on 19 August: hovering an
   arch should light that arch.

   In the SHARED layer, not the Studios one: the arch field is on
   Group Home's hero and on every legal page, and the first version
   of this rule sat in site/studios.css, which the Group page does
   not load. Caught by measuring pointer-events on the live field
   rather than by reading the cascade.

   :hover cannot do this. Each arc is an <i> whose BOX is a
   2314px square (the circle is drawn with border-radius on a
   square), so the boxes overlap almost completely and a pointer
   anywhere in the field would be inside eight of them at once.
   The engine knows this and sets pointer-events: none on the field.

   So the hit test is geometric, in site/index-controls.js: for
   each arc it compares the pointer's distance from that circle's
   centre against the circle's radius, and lights the arc when the
   pointer is within a tolerance of the circumference. That is an
   exact test against the drawn line rather than against a box, and
   with nine arcs it is nine subtractions per pointer move.

   Promote: this is the interactive-arches behaviour the Figma
   hover frames document, it is the same on all four brands, and
   the geometry belongs next to the component that owns --arch-r
   and --arch-cx rather than in a site layer.

   TAKEN OUT 20 AUGUST. Lasse: "remove the coloring because it's
   wrong. It should be in between the lines of the arches and not
   as you implemented it here." The hit test was right and the
   target was wrong: it lit the stroke, and the design fills the
   gap between two strokes. Those gaps are the lens-shaped
   intersections of neighbouring circles, so the next version wants
   a clip-path or an SVG mask over a filled shape, not a border
   colour on an <i>. Off entirely until then, because half-right
   motion on a policy page is worse than none.
   ------------------------------------------------------------ */
/* Nothing here on purpose. The rules that were here lit the arc LINE in
   --c-accent, and the design lights the ground BETWEEN two neighbouring
   arcs, which is a lens-shaped intersection of two circles rather than a
   stroke: a clip-path or an SVG mask, not a border colour. Off until that
   exists. Previous implementation in git at 8012ced. */


/* ------------------------------------------------------------
   G8 · A SIGNUP WITH NO ENDPOINT YET
   ------------------------------------------------------------
   Seven .ui-signup forms across the site, all of them pointed at
   nothing until content/site.json gets an endpoint. The build marks
   them data-bf-pending and this disables them, because an input the
   visitor can type an address into and a button that does nothing
   with it is a worse promise than a form that admits it is not
   ready. Removed by the build, not by hand, the moment the endpoint
   is set.

   Recommendation on record: a hosted POST endpoint, not an embedded
   widget. An embed brings its own CSS into a system that has one
   token contract and would be the only thing on the site not
   obeying it.
   ------------------------------------------------------------ */
.ui-signup[data-bf-pending] {
  opacity: 0.55;
}
.ui-signup[data-bf-pending] .ui-signup__input,
.ui-signup[data-bf-pending] .ui-signup__submit {
  pointer-events: none;
  cursor: not-allowed;
}

/* ------------------------------------------------------------
   G9 · THE SCROLL REVEAL                          PROMOTE
   ------------------------------------------------------------
   A 24px rise and a fade, once, never reversed. It was 12px and it
   lived in site/studios.css until 20 August, when Lasse asked for
   "a little bit more motion when we scroll down, things have to
   appear in a more website kind of way" and for the Group Home to
   have it too. Moving it here is the point of the change: the page
   that needed it does not load the Studios layer, so the version
   in that file added the classes and painted nothing, measured at
   transition-duration 0s. Second time that exact mistake, so:
   anything site/index-controls.js can apply to more than one page
   belongs in this file.

   Three distances, all one gesture:
     band  24px   the section arriving
     item  14px   a card inside a staggered grid, a detail of it
     part  24px   a band that reveals its contents instead of
                  itself, which is what a page of prose needs

   THE HIDDEN STATE IS APPLIED BY THE SCRIPT, NOT HERE. A
   stylesheet that hides content and waits for a script to reveal
   it is one failed request from a blank page. These rules only say
   what to do once the class exists.

   Nothing moves on continuous scroll: no parallax, no scaling,
   nothing that keeps moving while the user does.
   ------------------------------------------------------------ */
.bf-reveal {
  opacity: 0;
  transform: translateY(24px);
}
.bf-reveal.is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity   var(--dur-slow) var(--ease) var(--bf-reveal-delay, 0ms),
    transform var(--dur-slow) var(--ease) var(--bf-reveal-delay, 0ms);
}
.bf-reveal--item { transform: translateY(14px); }
.bf-reveal--part { transform: translateY(24px); }

/* Belt and braces. The script skips the reveal entirely under
   reduced motion, so this only matters if the setting changes
   after load, mid-session. */
@media (prefers-reduced-motion: reduce) {
  .bf-reveal,
  .bf-reveal.is-in,
  .bf-reveal--item,
  .bf-reveal--part {
    opacity: 1;
    transform: none;
    transition: none;
  }
}


/* ------------------------------------------------------------
   G18 · THE HERO ENTRANCE                          PROMOTE
   ------------------------------------------------------------
   The landing moment, asked for by Lasse on 26 August: the reveal
   excludes the hero on purpose, which left the page opening
   completely static. site/index-controls.js (section 7) now
   choreographs the hero in on load, opt-in per hero via
   data-bf-hero-intro. The rising elements reuse .bf-reveal whole,
   so this block only owns the two states the entrance adds:

   --flat is the navbar's. It fades with everybody else but does
   not rise: the bar carries the logo, and a bar that slides in
   reads as a bug rather than a welcome. Declared after --item and
   --part so it wins on the element that has both.

   The settle is the painting's. One slow ease from a hair
   zoomed-in to rest, three of the system's slow beats, once,
   never again: it is the same "arrival, then stillness" grammar
   as the reveal, at the hero's own scale. The class is added by
   the script, so a page without JavaScript shows the painting at
   rest, which is the correct still version.

   In the SHARED layer even though only Studios opts in today,
   for the reason that is now a rule: the script applies these
   classes on any page, and the fourth bug in this repo was motion
   CSS written into a layer the moving page did not load.
   ------------------------------------------------------------ */
.bf-reveal--flat { transform: none; }

.bf-intro-settle {
  animation: bf-intro-settle calc(var(--dur-slow) * 3) var(--ease) both;
}
@keyframes bf-intro-settle {
  from { transform: scale(1.03); }
  to   { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .bf-intro-settle { animation: none; }
}


/* ============================================================
   G9 to G15 · MOVED OUT OF site/studios.css, 20 AUGUST
   ============================================================
   Bifrost Forge's Home needs the image hero, the overlaid navbar,
   the card baselines, the filter and pager states, the hidden-card
   fix and the accent tag wrapper. Forge does not load studios.css,
   which is one page's layer, so these seven blocks moved into the
   shared layer rather than being duplicated or than Forge loading
   a file named after another brand.

   This is the promotion path the architecture describes, taken for
   the reason it exists: a thing that turns out to be true for two
   brands belongs in the shared layer. Six of the seven were already
   marked PROMOTE or RAISE when they were written, which is the note
   from that day paying off. Their original reasoning is unchanged
   below, including the measurements it was derived from.

   A thing that turns out to be true for all four should be proposed
   for the engine instead. That is still the next step for all seven.
   ============================================================ */

/* ------------------------------------------------------------
   G9 · THE IMAGE HERO                             MOVED FROM studios.css
   ------------------------------------------------------------
   Measured off 40004577:14619 (1440 x 766): full-bleed artwork,
   the navbar overlaid at y 0, content left-aligned on the grid
   margin, title at y 238, lead at y 455 on the 540 measure, and
   a one-line stat row at y 524.

   Built on .bf-hero from the shared layer so the two heroes stay
   one component with two faces. If .ui-section--hero is ever
   promoted into the engine (shared layer, G1), this becomes its
   --image variant rather than a separate thing.
   ------------------------------------------------------------ */
.bf-hero--image {
  position: relative;
  /* the artwork covers it; the inverted ground would otherwise
     paint black behind a photograph for no reason */
  background: var(--c-ink);
  justify-content: center;
}

.bf-hero--image .bf-hero__art {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}
.bf-hero--image .bf-hero__art > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 62% center;   /* keeps the airship in frame as it narrows */
}

/* Left-aligned, unlike Group's centred stack. */
.bf-hero--image .bf-hero__inner {
  align-items: flex-start;
  text-align: start;
  z-index: 1;
}
.bf-hero--image .bf-hero__lead,
.bf-hero--image .bf-hero__title { margin-inline: 0; }

/* The stat row: three figures on one line, the numerals at body
   bold and the labels at the second ink. Drawn as a single text
   line in the file, so it is one line here too. */
.bf-hero__stats {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-w-xs);
  margin: 0;
  color: var(--ui-ink);
}
.bf-hero__stats > span { white-space: nowrap; }
.bf-hero__stats b { font-weight: var(--fww-body-bold); }
/* The rule between figures. A pseudo-element on the FOLLOWING
   figure, so it can never orphan at the end of a wrapped line. */
.bf-hero__stats > span + span::before {
  content: "|";
  color: var(--ui-ink-40);
  padding-inline: var(--s-w-xxs) var(--s-w-xs);
}

/* ── MOBILE ────────────────────────────────────────────
   The shared layer lets .bf-hero drop its min-height below 640,
   on the reasoning that the arch band is decoration and should be
   the first thing to give. An image hero has nothing to give: the
   painting IS the band, and collapsing to the text height leaves
   a 330px strip with the stat row touching its bottom edge.

   So the image variant keeps a real opener, capped so it never
   pushes the first band entirely below the fold, and pays its own
   padding on both sides rather than only the top. The stat row
   stacks and drops its rules, because three figures on one line
   at 393 wrap mid-phrase.
   ------------------------------------------------------------ */
@media (max-width: 639px) {
  .bf-hero--image { min-height: min(100svh, 620px); }
  .bf-hero--image .bf-hero__inner {
    padding-block: var(--s-w-xl);
    justify-content: flex-end;
  }
  .bf-hero__stats {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-w-xxs);
  }
  .bf-hero__stats > span + span::before { content: none; padding: 0; }
}


/* ------------------------------------------------------------
   G10 · THE NAVBAR OVER THE ARTWORK                MOVED FROM studios.css
   ------------------------------------------------------------
   The file draws the navbar inside the hero frame at y 0, over
   the painting, with knockout links and a light pill. The engine
   builds .ui-nav as a page-level band with its own plate, and
   its inverted rule paints a solid ground, which would put a
   black bar over the painting.

   So: still a page-level sibling (the engine is right about that),
   overlaid and transparent. Alongside G1 this is the second half
   of a real .ui-section--hero: a hero and the bar that sits on it
   are one design decision.
   ------------------------------------------------------------ */
.bf-page {
  position: relative;   /* the containing block for the overlaid bar */
}
.ui-nav--over {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 5;
  background: transparent;
}
/* The engine's inverted nav paints --ui-ground. Not over a photograph. */
.ui-nav--over.ui-mode--inverted { background: transparent; }


/* ------------------------------------------------------------
   G11 · A CARD WITH NOWHERE TO GO
   ------------------------------------------------------------
   Seventeen of the forty portfolio cards have no destination:
   eleven studios whose pages are not built, and six companies
   whose sites read "under progress" in the handover pack. The pack
   is explicit (rule 5.2) that those are to-dos, so those cards
   render as <div> with no arrow.

   REVISED 19 August. The first version also suppressed the accent
   hover on them, on the reasoning that a fill promises a click.
   Lasse wants the yellow on every card, so the fill is back: it
   reads as "this row is alive" rather than as "this is a button",
   and the missing arrow is still the honest signal about where the
   card does and does not go.

   The cursor is the one thing left saying so, and it stays.
   ------------------------------------------------------------ */
.ui-card--static { cursor: default; }

/* If a future portfolio.json arrives with an unwritten description,
   the renderer prints the pack's own "under progress" marker rather
   than substituting copy. Dimmed and italic so it never reads as
   the real one-liner. Currently unused: all forty descriptions in
   the 19 August pack are written. */
.ui-card__body--pending {
  font-style: italic;
  opacity: 0.6;
}


/* ------------------------------------------------------------
   G12 · ONE BASELINE ACROSS FORTY CARDS            MOVED FROM studios.css
   ------------------------------------------------------------
   The engine bottom-anchors a text card's copy, which is right for
   three cards and wrong for forty: a two-line title pushes its
   description down, so scanning the index means the eye re-finding
   the title on every row. Lasse, 19 August: give the title room for
   three lines, top-align it, and put every title and every
   description on the same line.

   Two fixed reserves, and both are MEASURED at the narrowest card
   rather than guessed. Counted in a browser across all forty:

     card width      title lines      description lines
     260 (1440)      max 2           max 5   ("1255")
     342 (834)       max 1           max 4
     333 (393)       max 1           max 4

   So the description reserve steps with the breakpoint: five lines
   above 1120, four below. Reserving five everywhere would leave a
   blank line at the foot of every tablet and mobile card.

   min-block-size, NOT block-size. A fixed height would align these
   forty and silently clip the forty-first: the pack's copy ships
   verbatim, so a longer entry has to push its own card taller
   rather than lose a line. Alignment today, graceful tomorrow.

   ⚠ TWO title lines, revised 19 August. Three was asked for first,
   and three was exactly what made the group sit high: a spare line
   under a one-line title opens a 68px hole between the title and
   the description, and pulls the whole block up the card. Two is
   the measured maximum, so alignment is unchanged and the group
   drops 24px. Still min-block-size, so a future three-line title
   takes its third line rather than losing it.

   Promote: any index past a handful of cards has this problem, and
   it is a property of the CARD, not of Bifrost.
   ------------------------------------------------------------ */
.ui-section--cards-colorful .ui-card__head {
  align-items: flex-start;
}
.ui-section--cards-colorful .ui-card__title {
  display: block;
  min-block-size: calc(2 * var(--lhw-h4) * var(--fsw-h4));
}
/* The arrow belongs on the FIRST line, not floating in the middle
   of an empty three-line box. The engine sizes it to the 34px
   control box on purpose (it reproduces the file's Link box), so
   the width stays and only the height comes down to one title line,
   which re-centres the glyph against that line. */
.ui-section--cards-colorful .ui-card__arrow {
  height: calc(var(--lhw-h4) * var(--fsw-h4));
}
.ui-section--cards-colorful .ui-card__body {
  min-block-size: calc(4 * var(--lhw-body) * var(--fsw-body));
}
@media (min-width: 1120px) {
  .ui-section--cards-colorful .ui-card__body {
    min-block-size: calc(5 * var(--lhw-body) * var(--fsw-body));
  }
}

/* The stage cards have the same eye-annoyance for the same reason:
   "Scale" carries a four-line description where the other two carry
   three, so its title sits a line higher. One reserve, one row. */
.ui-section--cards-stacked .ui-card__body {
  min-block-size: calc(4 * var(--lhw-body) * var(--fsw-body));
}


/* ------------------------------------------------------------
   G13 · PAGER AND CHIP STATES THE ENGINE HAS NOT MET YET
   ------------------------------------------------------------
   Both pagers are now real controls, so they have a state the
   engine's static proof pages never needed: at the first page the
   previous arrow has nowhere to go. Dimmed and inert rather than
   removed, so the row does not reflow as you page through it.

   The pagers are <button> throughout, which the engine already
   anticipated: .ui-pager__item sets background: none and border: 0.
   ------------------------------------------------------------ */
.ui-pager .ui-link[disabled] {
  opacity: 0.3;
  pointer-events: none;
  cursor: default;
}
.ui-pager__item[aria-current="page"] { color: var(--ui-ink); }

/* The portfolio pager sits in the filter row now, in the slot the
   search vacated, so it stays in view while the cards change under
   it. The engine's --pager rule aligns to the end of a COLUMN, which
   in this row would push it to the bottom of the tallest item. */
.ui-section__filter-row > .ui-section__pager {
  align-self: center;
  margin-inline-start: auto;
}


/* ------------------------------------------------------------
   G14 · MAKE `hidden` ACTUALLY HIDE A CARD          MOVED FROM studios.css
   ------------------------------------------------------------
   The UA stylesheet's `[hidden] { display: none }` is a single
   attribute selector, so `.ui-card { display: flex }` outranks it
   and a card with the hidden attribute stays on the page. Found by
   measuring: after paginating to twelve, the rendered page height
   had not changed at all.

   This is the engine's to fix, once, for every component that sets
   display: `[hidden] { display: none !important }` in section 1 is
   the usual one-liner. Scoped here rather than made global so it
   cannot surprise anything else on the page.
   ------------------------------------------------------------ */
.ui-card[hidden],
.ui-list__row[hidden],
.ui-person[hidden] { display: none; }


/* ------------------------------------------------------------
   G15 · THE ACCENT TAG WRAPPER MUST NOT PAINT      MOVED FROM studios.css
   ------------------------------------------------------------
   The middle stage card's "Month 3-9" tag is the accent-filled cell
   of the Tag component, and the engine's only route to that cell is
   `.ui-mode--alternative .ui-tag[aria-pressed="true"]`, which needs
   the mode on an ANCESTOR. So the markup nests: a positioning
   wrapper carrying .ui-card__tag and the mode, with the real tag
   inside it.

   The engine paints .ui-card__tag itself (`background:
   var(--ui-card-tag-plate)`), which on a wrapper puts a grey
   square-cornered rectangle behind the amber pill. Reported by
   Lasse, 19 August. The wrapper positions and nothing else.

   Raise: this is the cost of the mode living on an ancestor. A
   `.ui-tag--accent` modifier would let the tag be one element and
   make this rule unnecessary.
   ------------------------------------------------------------ */
.ui-card__tag.ui-mode--alternative {
  background: transparent;
  border: 0;
  padding: 0;
}


/* ------------------------------------------------------------
   MOVED OUT OF site/studios.css, 20 AUGUST (second pass)
   ------------------------------------------------------------
   The page head turned out to be the site's most reused section,
   not a legal-page detail. It is on the five legal pages, on Forge
   Companies (40004579:19816) and on Forge The Way (40004579:20427),
   and the Forge frames draw it identically down to the 783 x 90 h1
   box and the 500-wide description at x 900.

   Caught the way these always are: the Companies page measured a
   band 0px tall, because `--u` and every rule under .bf-pagehead
   were in a layer that page does not load. Third time this exact
   mistake has cost a bug, and the second time in one day. It was
   already marked PROMOTE.
   ------------------------------------------------------------ */

/* ------------------------------------------------------------
   G16 · THE PAGE HEAD, MEASURED OFF THE FIGMA FRAME  MOVED FROM studios.css
   ------------------------------------------------------------
   Rebuilt 20 August. The first version was invented, because I
   built a legal page head from the words "Terms and Privacy pages"
   without opening the frame that draws one. Lasse: "what is in
   production doesn't correspond to what is in the Figma file".
   It did not. Source of truth is 40004577:14367 (Privacy) and
   40004577:13542 (Terms), both 1440 x 2474.6 in UI SECTIONS -
   STUDIOS, plus 40004577:14549 for the hover state.

   WHAT THE FRAME ACTUALLY DRAWS, at 1440:

     General Intro (100vh)   0    to 766, FULL BLEED, no plate
       Navbar                0    to 75
       Header               75    to 566, arches, inset 20 left
       rule                 565,  full bleed, 1px
       Content             566    to 766
         Title             586    to 746   (20px padding)
           h1 Privacy      x 20,  128px display, one line
           Description     x 920, w 500: body copy, then a
                                  Read more button 40px below

   Four things the first version got wrong, all structural:
     1. It was a PLATE. The frame is full bleed with the navbar on
        it, exactly like the hero. So this section opts out of G4.
     2. The title was ABOVE the arches. It is below them, under
        the rule, on the same line as a right-hand column.
     3. There was no rule and no right-hand column at all.
     4. The arch geometry was Group Home's hero, not this frame's.

   THE ARCH GEOMETRY, fitted to the 1440 render rather than read
   off the layer tree, because the layer x values for the nested
   vector groups are inconsistent with their own parents:

     9 circles, diameter 1326.151  ->  r 663.08
     pitch 133.73                       (the same pitch as the
                                         hero: it is the constant)
     circle tops flush with the top of the arch band
     first centre at x = r, so circle 1's left edge is flush with
       the band's left edge

   Predicted arc 1 left extremity at the rule: 682.5 - 639.3 =
   43.2. Measured in the render: 43.0. Right extremity predicted
   1321.8, measured 1322.0. That is the fit confirmed twice, which
   is why these numbers are trusted and the layer tree's are not.

   Everything scales off one unit, --u = min(1vw, 14.4px), so at
   1440 and above every value reproduces the frame exactly and
   below 1440 the whole composition scales as one piece. There is
   no legal frame at 834 or 393 to check against; scaling is the
   extrapolation, and it keeps the arc count and the crop identical
   at every width, which no fixed-px version does.

   Promote as .ui-section--pagehead: every page that is not a Home
   needs this, on all four brands, and there will be a lot of them.
   ------------------------------------------------------------ */
.bf-pagehead {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;                 /* the clip IS the arch design */
  padding-block: 0;
  /* .ui-section is a flex column with `gap: var(--ui-section-lead)`, which
     is 40px of head-to-body rhythm this band does not want: the arches, the
     rule and the content row are one continuous object and the rule has to
     touch the arches. */
  gap: 0;
  /* NO 100svh floor, on purpose. The frame is 766 tall including the 75px
     navbar, and the parts already add up to exactly that at 1440: 490 of
     arches, the 1px rule, and a 200 content row is 691, which is 766 minus
     the bar. So a viewport floor is redundant where it would be right and
     actively wrong everywhere else: at 393 it left 574px of slack, the
     band was the only flex-grow child, and it grew until the nine circles
     were fully visible as a coil instead of clipped into a horizon.
     Measured, not guessed: the mobile band came out 574 tall against a
     134 intent. Derive the height from the drawn parts, not from a
     viewport unit that happens to agree at one width. */
}
/* Opt out of G4. The plate treatment (radius, 20px side margins, a
   grid-margin content inset and the inter-band gap) is right for
   every band except the two that carry the navbar: the hero, and
   this. G4 excludes .bf-hero by name and this is the second case. */
.ui-page--bands > .bf-pagehead {
  border-radius: 0;
  margin-inline: 0;
  margin-block-start: 0;
  inline-size: auto;
  --ui-section-inset: 0px;
}

/* The arch band. 34.1u tall (491 at 1440), inset by the band inset
   on the LEFT only, and running to the viewport edge on the right.
   That asymmetry is what the frame draws: Interactive Arches is at
   x 20 with width 1420, so it starts on the plate line and bleeds
   off the right. Logged as a question in DECISIONS rather than
   quietly symmetrised, because it may be a drawing slip. */
.bf-pagehead__band {
  --u: min(1vw, 14.4px);
  position: relative;
  /* NOT flex: 1. See the note on .bf-pagehead: a growing arch band is how
     the mobile head turned into a coil. */
  flex: 0 0 auto;
  /* 491 at 1440, less the 1px rule below it, so the arches plus the rule
     are exactly the frame's 491-tall Header, and arches plus rule plus the
     200 content row are exactly the frame's 766 less the navbar. */
  block-size: calc(34.1 * var(--u) - 1px);
  min-block-size: calc(18 * var(--u));
  margin-inline-start: var(--bf-band-inset);
  overflow: hidden;
}

/* The rule under the arches. Full bleed, 1px, and it is structural
   rather than decorative: it is what turns a clipped circle field
   into a horizon. Not --rule-w, which is 2px and is the arch and
   table stroke. Measured 1px in the render at 1440. */
.bf-pagehead__rule {
  flex: 0 0 auto;
  margin: 0;
  border: 0;
  border-block-start: 1px solid var(--c-ink);
}

/* The content row. 900/1400 for the title, 500/1400 for the aside,
   which is the frame's split expressed as a ratio so it holds at
   every width above the stack point. */
.bf-pagehead__content {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: var(--s-w-m);
  box-sizing: border-box;   /* the engine sets this on .ui-section, not here */
  padding: var(--bf-band-inset);
  min-block-size: 200px;    /* the frame's Content frame, 566 to 766 */
  align-content: start;
  /* Not stretch. A stretched h1 is as tall as the aside beside it, which
     makes every measurement of it a measurement of the wrong box and cost
     twenty minutes chasing a cap-trim bug that was not there. */
  align-items: start;
}
@media (min-width: 1120px) {
  .bf-pagehead__content {
    grid-template-columns: minmax(0, 900fr) minmax(0, 500fr);
    column-gap: 0;
  }
}
.bf-pagehead__title {
  margin: 0;
  /* The frame gives the h1 783px of a 900px column for one word. A
     policy title is not one word, so it wraps inside the column
     rather than pushing the aside. */
  max-inline-size: 100%;
  text-wrap: balance;
}
/* The right column: body copy, then the button 40px under it. The
   frame insets this block 20px from the top of the content row, so
   the h1's cap line and the copy's first line do not align, which
   is the drawn relationship rather than an accident. */
.bf-pagehead__aside {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-w-m);
}
@media (min-width: 1120px) {
  .bf-pagehead__aside { padding-block-start: var(--s-w-s); }
}
.bf-pagehead__aside > .ui-deflist,
.bf-pagehead__aside > .ui-body { margin: 0; }

/* The Read more control. Same problem the navbar has and the same fix:
   .ui-btn fills with --ui-plate, and this band IS a plate, so the default
   button is invisible on it. The engine solves it for .ui-nav with
   --ui-nav-control; this is the second place that needs it, which is the
   argument for the token being about controls-on-a-plate rather than about
   the navbar. Written as :not(:hover) for the reason given at §5.1: a flat
   override would silently kill every hover rule above it.

   --ui-ground, NOT --ui-nav-control. That token is declared ON .ui-nav, so
   outside the bar it is undefined, and `background: var(--undefined)` is
   invalid at computed-value time: the fill silently became transparent and
   the button vanished completely rather than falling back. --ui-ground is
   what the nav token resolves to in the default mode, and sampling the
   frame's own pill confirms it: #DED9D6, the same fill as the navbar's
   Back to Bifrost. */
.bf-pagehead__aside .ui-btn:not(:hover):not([data-state="hover"]) {
  background: var(--ui-ground);
  color: var(--ui-ink-60);
}

/* The frame strokes these arcs at 1px. --rule-w is 2px and is the table
   and section stroke, which the house component reasonably uses as its
   default; at this radius 2px reads as a diagram rather than a horizon.
   Scoped to the page head so Group Home's hero, which is signed off at
   the component default, does not move. */
.bf-pagehead__band .bf-arch > i { border-width: 1px; }

/* A policy is numbered clauses, and 1.1 through 3.2 read as one wall
   without a step between the groups. The engine's prose owl gives
   every sibling the same gap; a heading gets more room above it. */
.ui-longform__prose > .ui-h4 { margin-block-start: var(--s-w-m); }
.ui-longform__prose > :first-child { margin-block-start: 0; }

/* The official document title, above the clauses. The frame keeps a
   Title h2 slot here and leaves it hidden because it had no copy for
   it; a policy page has to state which document it is, and the h1 is
   the short display title. */
.bf-longform__doctitle {
  margin: 0 0 var(--s-w-m);
  color: var(--ui-ink-60);
}


/* ------------------------------------------------------------
   G17 · A REAL PORTRAIT IS NOT A PLACEHOLDER              RAISE
   ------------------------------------------------------------
   Lasse, 20 August: "the pictures of the founders have been
   squared a little bit too much. We want the corners to be as
   round as the community pictures."

   He is right and the engine says why. `.ui-person__portrait`
   rounds with --radius-soft, which tokens.css defines as 4px and
   comments "portrait placeholders only". The community gallery
   next to it rounds with --radius-component, 20px on Studios. So
   the two sat five times apart, and the difference was visible
   because both are on the same page a screen apart.

   4px is a defensible radius for an empty grey plate standing in
   for a photograph. It is the wrong radius for a photograph, and
   the moment the real portraits landed the token stopped matching
   what it is named for.

   RAISE: either --radius-soft loses the "placeholders only"
   comment and the person component moves to --radius-component,
   or the component keeps both and switches on whether it has an
   image. The first is simpler and is what the file draws.

   Shared layer, not the Studios one: a people row is not a
   Studios idea, and this is the fourth bug caused by putting a
   rule in one page's layer.
   ------------------------------------------------------------ */
.ui-person__portrait { border-radius: var(--radius-component); }


/* ------------------------------------------------------------
   G18 · AN EMPTY PORTRAIT NEEDS AN EDGE                 PROMOTE
   ------------------------------------------------------------
   Two of the sixteen people in the rail have no portrait yet, and
   their cards rendered as a name floating under 450px of nothing.
   `.ui-person__portrait` fills with --ui-plate, and on the bone
   ground the section paints, --ui-plate IS the ground. Measured:
   #EFECEA against #EFECEA, a 1.00:1 step.

   FOURTH INSTANCE OF THE SAME PATTERN. A control or a slot that
   fills with the surface it sits on is invisible: `.ui-btn` on a
   plate, `.ui-tag--filled` on a plate, `--ui-nav-control` used
   outside `.ui-nav`, and now the empty portrait. It is one engine
   question and it is asked as one in DECISIONS.md.

   The fix is not invention: ui-core 6.3 already solved exactly
   this for the other empty media slot, and its comment could have
   been written about this one ("visible enough for a filled card
   and NOT visible enough for an empty rectangle, which then reads
   as a gap in the layout rather than as a picture that has not
   arrived"). So this is that rule, verbatim, on the component the
   engine did not apply it to. Same hairline, same token, same
   `:not(:has(img))` scope, so a card with a photograph is
   untouched.

   PROMOTE: it belongs three lines below `.ui-content__media
   :not(:has(img))` in ui-core, not here. Any brand with a team
   rail and a person who has not sent a photograph has this.
   ------------------------------------------------------------ */
.ui-person__portrait:not(:has(img)) {
  border: var(--rule-w-fine) solid var(--ui-ink-20);
  background: transparent;
  /* One line the engine's version does not have, and should. The page is
     content-box (there is no `*` reset in the stack), and `aspect-ratio` sizes
     the CONTENT box, so a hairline on all four sides makes the empty slot 2px
     taller than the filled ones beside it and drops its name by 2px. Measured:
     353x450 with a photograph, 355x452 without, in the same row. Carry this
     into ui-core with the rule. */
  box-sizing: border-box;
}


/* ------------------------------------------------------------
   G19 · THE PAGER'S NUMBERS GLIDE WHEN THE WINDOW MOVES
   ------------------------------------------------------------
   Lasse, 8 September: five numbers between the arrows, "and then
   they just glide and change as they change". The window is in
   site/index-controls.js; this is only the motion.

   The engine already transitions .ui-pager__item, but only its
   colours, and the property is declared as a list rather than
   with `transition-property`, so adding opacity and transform
   means restating it. Same duration and easing tokens, so this
   is the same transition with two more properties on it.

   The entering state is applied BY THE SCRIPT and removed two
   frames later. Nothing here can leave a number invisible on a
   page without JavaScript, which is the same contract the scroll
   reveal works to.

   SHARED LAYER: paint() runs on the portfolio, the founder rail
   and the gallery. A rule for a class the script adds does not
   belong in one page's file. That mistake has cost four bugs.
   ------------------------------------------------------------ */
.ui-pager__item {
  transition: background-color var(--dur-fast) var(--ease),
              border-color     var(--dur-fast) var(--ease),
              color            var(--dur-fast) var(--ease),
              opacity          var(--dur-fast) var(--ease),
              transform        var(--dur-fast) var(--ease);
}

/* Every cell is as wide as the widest number the pager can show, so
   the strip does not grow when 9 becomes 10 and the arrows stay put
   while the numbers move. The count comes from paint(), which knows
   how many pages there are; the fallback keeps a JS-free page on the
   engine's own min-width. */
.ui-pager__item {
  min-width: max(var(--s-w-xs), calc(var(--bf-pager-digits, 1) * 1ch + 6px));
}

/* 10px, not more: this is a control settling, not a carousel. The
   sign follows the direction of travel, so numbers arriving from
   the right enter from the right. */
.ui-pager__item.is-pager-in-right { opacity: 0; transform: translateX(10px); }
.ui-pager__item.is-pager-in-left  { opacity: 0; transform: translateX(-10px); }

@media (prefers-reduced-motion: reduce) {
  .ui-pager__item { transition: none; }
  .ui-pager__item.is-pager-in-right,
  .ui-pager__item.is-pager-in-left { opacity: 1; transform: none; }
}


/* ------------------------------------------------------------
   G20 · THE SIGNUP HONEYPOT
   ------------------------------------------------------------
   One text field named `company`, off-screen, never announced,
   never tabbable. A person cannot reach it; a bot that fills
   every input fills it, and the endpoint drops anything that
   arrives with it set.

   `clip-path` and a 1px box rather than `display: none` or
   `visibility: hidden`: both of those are exactly what a
   competent bot checks for before deciding what to fill. This is
   the standard visually-hidden recipe with `white-space: nowrap`
   so a long value cannot force a scrollbar.

   Shared layer: the renderer puts this field in every signup on
   the site, and there are seven of them.
   ------------------------------------------------------------ */
.bf-hp {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
