/* sections/intro-cards-face.css — everything INSIDE a card: the title bar, the
   grip, the index, the title, the body, the open state's box, the z tiers and
   the mobile arrangement. Split out of intro-cards.css at 1012 lines against
   000 §3.1's 900 (200 §7, cut line from 199 §8).

   ⚠️ MOVE-ONLY. Nothing in this file was edited when it was created; the
   concatenation of intro-cards.css and this file is byte-identical to the
   single file it replaced, and that was checked rather than asserted.

   ⚠️ LINKED IMMEDIATELY AFTER intro-cards.css AND THE ORDER IS LOAD-BEARING.
   Both files carry `.card` rules and CSS resolves ties by source order, so a
   link that lands before intro-cards.css silently changes which declaration
   wins. index.html's four intro stylesheets are in cascade order for this
   reason. */

/* ── THE TITLE BAR (176 §4, 178 §1.4) ──────────────────────────────────────
   Xinlu: "we need to add a deeper, better frosted-effect horizontal bar to
   visually better distinguish the title from the body text on the card … move
   that horizontal bar to the TOP of our cards."

   ⚠️ TINT, NOT A SECOND FILTER, AND THE REFERENCE AGREES. 176 §2.1's example
   gets its bar from rgba(255,255,255,0.05) alone — one backdrop-filter per
   card, none on the bar. 176 §4.3 predicted a nested backdrop-filter would be
   inert here because a child of a filtered element has that element's own flat
   painted background as its backdrop, and blurring a flat colour returns the
   same flat colour. C17 measures it; whichever way it comes back, tint is what
   ships this round, because it is what the reference does and it is free.

   ⚠️ IF A GENUINELY DEEPER FROST IS EVER WANTED the structure has to change —
   the filter comes off .card and onto two sibling panels inside it, each with
   the page as its real backdrop. That doubles the frosted layers from five to
   ten and C9 prices five at +467 us per composited frame while sliding. It is
   a bigger change than it looks; 176 §4.3 item 2 is the write-up.

   ⚠️ THE HEIGHT DEFAULTS TO --card-dy AND THAT IS DERIVED, NOT PICKED. Each
   card is offset UP by exactly --card-dy, so that is roughly the band of a back
   card the fan leaves visible — a bar the same height means each back card
   shows its own bar and no stray edge of body, which is
   176-reference-card-stack.png's look. Perspective foreshortens the back cards
   so it is approximate; C18 asserts it rather than this comment claiming it. */
.card__head {
  margin: 0;
  font-weight: 400;
  /* ⚠️ A VARIABLE, NOT 1rem, AND IT RIDES --card-open (198 §3 point 3, point 6).
     Two things needed it. The mobile title had to shrink to fit ONE line on a
     220px card, and the OPEN card's title had to grow back — an open card is a
     reading surface, and a title sized for a 220px band looks lost on it.
     --card-k is the one scalar both ride; see .card below for why it is
     declared there and not on #intro. */
  font-size: calc(var(--card-title-size, 1rem) * var(--card-k, 1));

  /* ⚠️ AN EXPLICIT LINE-HEIGHT, BECAUSE `normal` IS 2.3x IN THIS FACE AND THAT
     IS WHAT PUSHED THE TITLE OUT OF THE BAND (198 §3 point 3). MEASURED: at
     0.8125rem the title's ink ran 30.08px tall against a 27.76px band on card 2
     — so no title-y offset could have fitted it and shrinking the type further
     would have had to fight a constant 2.3 multiplier rather than the size.
     ⚠️ THE DESKTOP DEFAULT IS `normal`, so desktop renders byte-identically and
     C19's 8.0/11.3/16.2/25.4 stand. */
  line-height: var(--card-title-lh, normal);
  flex: none;

  /* ⚠️ TWO NUMBERS NOW, AND SEPARATING THEM IS 181 §4 (from 180 §4).
     C18 passed in 179 because the tinted band's height and the band that must
     stay covered were ONE value — so shortening the bar, which is what Xinlu
     asked for, would have re-opened exactly what C18 was built to catch.

       --card-body-top   the HEAD's box height. Stays at --card-dy, because
                         that is the offset each card sits above the one in
                         front and therefore the band the fan leaves visible.
                         C18 runs against THIS.
       --card-bar-h      the TINTED band's height. Xinlu's to shorten freely.
                         Default 0.62 x --card-dy (181 §4's starting point).

     The space between them is bare card face, which is what the front card
     shows there anyway. */
  /* ⚠️ MULTIPLIED AT THE POINT OF USE, NOT AT THE POINT OF DECLARATION. The
     three metrics below all take var(--card-k), which .card resolved against
     its own --card-open; multiplying inside cards.js instead would need one
     write per card per frame for something CSS interpolates for free. */
  min-height: calc(var(--card-body-top, 70px) * var(--card-k, 1));
  box-sizing: border-box;
  /* ⚠️ NO PADDING HERE ANY MORE — IT MOVED TO .card__grip, AND THAT IS A HIT
     TARGET FIX (200 §3). The band a visitor sees and aims at is --card-dy tall
     and starts at the card's top edge; the grip's box started BELOW this
     padding, so the top of every band was live-looking and dead. C41's probe at
     each band's centre returned `intro-card-N-head` — the <h3> — on all five
     cards even after 200 §1 cleared the badge off them.

     With the padding on the grip and the head stretching it, the grip's box IS
     the head's box, so every pixel of the band belongs to the button. The title
     does not move: the grip keeps its own baseline alignment and now carries
     the offset that used to be here. C39 re-run confirms it. */
  padding: 0;

  /* ⚠️ TOP-ANCHORED, NOT CENTRED, AND 181 §2 ASKED WHICH IT WAS BEFORE THE
     CHANGE: it was CENTRED, so 181 §4's shortening would NOT have fixed the
     overlap for free. Measured on the pre-fix build, the title's lowest ink
     against the top edge of the card in front: -8.4 / -6.4 / -1.9 / +4.5 px on
     the four pairs. Centred in a band taller than the visible strip puts the
     back cards' titles below that strip, so their descenders cross the front
     card's border. Anchored to the top, ?cardbarh= now pulls the title up with
     it instead of leaving it where it was. C19 asserts >= 4px on all four. */
  display: flex;
  /* ⚠️ stretch, NOT flex-start, AND 181 §4's TOP-ANCHORING IS UNAFFECTED. That
     rule is about where the TITLE sits inside its band, and the title's
     alignment is set by .card__grip's own `align-items: baseline` one level
     down. What stretches here is the BUTTON, so it fills the band it is the
     control for — see the padding note above. */
  align-items: stretch;

  /* ⚠️ ONE SIGNED DIAL, SPLIT INTO A COLOUR AND A MAGNITUDE BY cards.js,
     because CSS cannot branch on a sign. --card-bar-c is "255, 255, 255" or
     "0, 0, 0"; --card-bar-a is the absolute value. 0 gives a fully transparent
     wash, which is the no-bar control.
     ⚠️ A HARD-STOP GRADIENT, NOT A BACKGROUND COLOUR, so the tint's height is
     --card-bar-h while the element's box stays --card-body-top. */
  background: linear-gradient(to bottom,
    rgba(var(--card-bar-c, 255, 255, 255),
         calc(var(--card-bar-a, 0.05) * var(--card-in, 1)))
      0 calc(var(--card-bar-h, 43px) * var(--card-k, 1)),
    transparent calc(var(--card-bar-h, 43px) * var(--card-k, 1)));
}

/* ⚠️ tabindex="-1" IS IN THE MARKUP ON PURPOSE (173 §3.2): the rail is the
   keyboard path, this is a pointer affordance, and five more tab stops would
   duplicate the rail while making 171 §9.3's "focus must not open a card"
   harder rather than easier. It is still a real <button>, so it is announced
   and it is activated by a real click rather than by a handler on a <div>. */
.card__grip {
  display: flex;
  align-items: baseline;
  gap: 12px;
  width: 100%;
  background: none;
  border: 0;
  /* Moved off .card__head so the button covers the whole band — see there. */
  padding: calc(var(--card-title-y, 8px) * var(--card-k, 1)) 20px 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
}

/* ?cardclick=0 makes closed cards inert (171 §9.2). The cursor goes with it:
   a pointer cursor on something that does nothing is the defect 156 §12
   describes, one layer up. */
.cards-noclick .card:not(.is-open) .card__grip { cursor: default; }

.card__grip:focus-visible {
  outline: 2px solid var(--rail-target, var(--accent));
  outline-offset: 4px;
}

/* Matched to the rail's indices exactly — intro.css:343-349 above, the same
   system stack at the same 0.85em and the same tracking. PAYLOAD ZERO, and
   the two halves of the section then number their five things identically. */
.card__index {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85em;
  letter-spacing: 0.04em;
  opacity: 0.65;
  flex: none;
}

/* Blinker for titles, Poppins for body — Xinlu's standing rule of 2026-09-03
   (156 §1.12, 000 §8), the same pair .intro__title and .rail__text use. */
.card__title {
  font-family: 'Blinker Subset', system-ui, -apple-system, sans-serif;
  letter-spacing: 0.02em;
}

/* ⚠️ THE MASK IS ON THE BODY AND NEVER ON .card, AND THIS IS THE HAZARD
   COMMENT 173 §2 item 5 ASKS FOR. Measured (172 §12.4), sampling a row near
   the card's lower edge where the mask is nearly clear:

     mask on .card__body   first-difference 1  — the glass is intact
     mask on .card         first-difference 215 — THE GLASS IS GONE

   The mask is not ignored on .card; it works, and that is the problem. It
   fades the frosted panel away together with the text and the visitor sees the
   raw, unblurred background through the bottom of the card. Moving this one
   line "up to the card to simplify it" is therefore a visible regression that
   looks like a rendering bug. No fallback is needed — an overlaid gradient
   pseudo-element measures identically — so there is nothing here to fall back
   to and nothing to remove.

   ⚠️ AND THERE IS NO ELLIPSIS. Xinlu chose the fade and ABANDONED the ellipsis
   outright (170 §3.2.2). ?cardfade=0 means NO FADE, not dots. A later reader
   must not reinstate three dots on the strength of finding them proposed in a
   document. */
.card__body {
  /* The other half of the padding move (see .card and .card__head above).
     No `gap` on .card any more, so this owns the space under the bar. */
  padding: 14px 20px 18px;
  box-sizing: border-box;

  /* ⚠️ GREY AT REST, WHITE WHEN OPEN (187 §4). Xinlu: "all text is white when
     static, with no distinction between primary and secondary elements … only
     when a card is clicked and pops out should the card's text turn white."

     ⚠️ .card__body ONLY. .card__title and .card__index keep .card's own
     `color: #fff` — he excluded them explicitly, and C25's control is the
     title's alpha reading 1.0 at every value of --card-open.

     ✅ NO NEW TIMELINE AND NO NEW SCALAR. --card-open already exists and this
     file already interpolates two other things on it (the tint above, the fade
     below), so 166 §2.4's one-timeline rule is satisfied by construction rather
     than by care. The extra --card-in factor is the arrival fade, so the body
     darkens with the rest of the card instead of floating over a black block.

     ⚠️ A COLOUR ALPHA AND AN ELEMENT OPACITY MULTIPLY. `opacity` below is 1 on
     the front card, so at rest the effective alpha is this one — but they are
     two separate mechanisms and 188 says so rather than leaving it implied. */
  color: rgba(255, 255, 255,
    calc((var(--card-body-alpha, 0.72)
          + (1 - var(--card-body-alpha, 0.72)) * var(--card-open))
         * var(--card-in, 1)));

  /* ⚠️ THE BACK CARDS' BODIES ARE NOT PAINTED — see cards.js's --card-front
     block for why opacity rather than display, and for what it looked like
     without this. max() so an opening back card brings its body up with it on
     the same --card-open the rest of the open state rides; there is still only
     one number driving the open. */
  opacity: max(var(--card-front, 1), var(--card-open, 0));
  margin: 0;
  font-family: 'Poppins Subset', system-ui, -apple-system, sans-serif;
  font-weight: 300;
  /* ⚠️ A VARIABLE BECAUSE MOBILE'S BODY MUST SIT BELOW ITS TITLE (200 §4).
     Xinlu: "the body text on the front card is larger than the card's title
     size." He is right — 199 §3 took the title to 13px to fit one line and left
     the body at desktop's 14px, so the two rulings met at an inverted
     hierarchy. 199 §1 reported it as his call; this is the call.
     ⚠️ 0.875rem ON DESKTOP IS THE OLD LITERAL, so that path is unchanged. */
  font-size: var(--card-body-size, 0.875rem);
  line-height: 1.55;
  letter-spacing: 0.01em;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 var(--card-fade-from, 62%), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 var(--card-fade-from, 62%), transparent 100%);
}

/* ⚠️ THE OPEN CARD IS THE ONLY THING THAT MOVES, AND IT GROWS RATHER THAN
   SCALING (170 §3.2.2 P3). Every card carries its full text at every size and
   the resting box simply does not show it all, so opening is a card growing
   with its words the same size and more of them visible — not the same words
   made bigger, and not content swapped underneath mid-animation, which is
   where flicker lives.

   The whole open state is driven by ONE number, --card-open, which the shared
   timeline writes (171 §8.3, 173 §3.1). Width, height, position, skew and both
   tints all read it, so there is exactly one clock and nothing can drift.

   ⚠️ z-index CANNOT be interpolated and must not be: card 5 has to come out in
   FRONT of cards 1-4, so it is raised at the START of the open and restored at
   the END of the close (171 §8.4), by a class the timeline toggles. Two tiers,
   because a direct swap has one card opening while another retracts and the
   opening one belongs on top. */
.card {

}

.card.is-open   { z-index: 900; }
.card.is-closing { z-index: 800; }

/* The body's fade retreats as the card opens: at rest it says "there is more",
   open it shows all of it.

   ⚠️ ?cardfade= IS THE FADE'S HEIGHT AND 0 MEANS NO FADE (171 §12). Built the
   other way round first — as the opaque fraction — and ?cardfade=0 then
   blanked the body entirely instead of removing the fade, which is C10's
   control failing on a build where the check itself would have passed. So the
   mask's opaque stop is 100% MINUS the fade, and every end of the range means
   what the flag says: 0 -> opaque to 100%, no fade at all; 100 -> the fade
   covers the whole body; 38 -> the bottom 38% is the fade.

   ⚠️ AND 0 IS NOT AN ELLIPSIS. Xinlu chose the fade and abandoned the ellipsis
   outright (170 §3.2.2); a later reader must not reinstate dots here. */
.card {
  /* ⚠️ THE FADE IS INTERPOLATED BETWEEN TWO ENDS NOW, NOT SWITCHED OFF AT ONE
     (199, and it is a defect 198 §4 created rather than one it names). Desktop's
     open card holds the whole body — MEASURED, 11 lines laid out into 22.7 lines
     of room, 0 overflow — so lifting the fade entirely there is correct and
     ?cardfadeopen= is 0, which makes this expression identical to what it
     replaced. 198 §4's portrait mobile card does NOT: 25 lines laid out into
     23.5 of room, 2.9 lines lost. With the fade fully lifted those lines were
     cut mid-word against a hard edge, which reads as a rendering fault rather
     than as more text.

     ⚠️ AND IT IS NOT AN ELLIPSIS, for the reason the block above gives. */
  --card-fade-from: calc(100% - (var(--card-fade, 38%)
    + (var(--card-fade-open, 0%) - var(--card-fade, 38%)) * var(--card-open)));
}

/* ── REDUCED MOTION (171 §11) ──────────────────────────────────────────────
   ⚠️ THE SECOND OF THE THREE PLACES --card-slide IS STATED. The third is
   cards.js. Neither is redundant: 036 §7.5's rule is to state the value in
   both places precisely so that either one failing is visible.

   ⚠️ AND THE FAN'S 3D STAYS. It is static layout, not motion — the rail's
   precedent above is to zero the animated DIALS and never to redraw the
   resting frame, and intro.css:403 records what happened the one time a
   reduced-motion rule redrew it (the marker got LONGER under reduce than at
   rest). What reduce removes here is the slide's motion and the open's
   duration, not the arrangement. cards.js runs the open with duration 0. */
@media (prefers-reduced-motion: reduce) {
  #intro { --card-slide: 1; }
}

/* ── MOBILE (196 §4, §5, §6) ───────────────────────────────────────────────
   ⚠️ THIS BLOCK REPLACES "MOBILE: NOT THIS PASS", WHICH SAID THE CARDS WERE
   REMOVED FROM THE DOM BELOW THE BREAKPOINT. They are not any more: cards.js's
   dead() branch is gone and C11 went with it (196 §7.3 — replaced by C35, not
   deleted). The old text is kept nowhere else, so: it read that a hidden block
   still holding its box and its five cards' markup "is a defect wearing a
   flag". ⚠️ THAT IS STILL TRUE OF `display: none`, AND NOTHING BELOW USES IT.
   The mobile arrangement moves boxes; it never hides one.

   ⚠️⚠️ SCOPED BY CLASS, NOT BY @media, AND THAT IS LOAD-BEARING. #intro gets
   .is-cards-mobile from cards.js on the SAME comparison that picks the JS
   defaults (innerWidth < ?cardbp=). An @media rule here would freeze the CSS
   half of the arrangement at 767 while ?cardbp= moved the JS half, so dragging
   the breakpoint — which 185 §5 item 3 leaves open and is Xinlu's to drag —
   would produce a mobile layout wearing desktop geometry, silently. One
   comparison, in one place, moves both halves.

   ⚠️ intro.css's own @media (max-width: 767px) block is NOT the same decision
   and is deliberately left alone: it places the RAIL, which has its own
   MOBILE_MAX and does not read ?cardbp=. cards.js:186 explains why 767 and
   ?cardbp= 768 name the same edge. */

/* ⚠️ THE CLIP LETS THE OPEN CARD OUT, AND ONLY THEN (198 §4). See cards.js's
   lift() for why this is safe: a card is only ever open while the section is
   pinned, and intro-rail is inactive for that whole window, so --card-slide is
   parked at 1 and the clip has nothing to clip. Without this the portrait open
   card 198 §4 asks for is cut off at --cards-bottom, which 197 §6.3 measured
   and shows in assets/197-mobile-360-openfull.png.

   ⚠️ MOBILE ONLY. On desktop the open card is sized against the space beside
   the rail and has never reached the clip's edge; leaving the rule scoped keeps
   194 §1's settled desktop out of it entirely. */
.is-cards-mobile.is-card-open .intro__cards { overflow: visible; }

/* ⚠️⚠️ THE RAIL FADES UNDER AN OPEN CARD ON MOBILE, AND IT IS NOT A FLOURISH —
   IT IS THE COST OF 198 §4 MEETING THE CARD'S OWN MATERIAL. The card is frosted
   glass: a --card-alpha 0.667 tint over a backdrop-filter, interpolating to
   --card-open-alpha 0.82. On desktop the open card sits BESIDE the rail with
   the section's dark ground behind it, so the glass has nothing to reveal. 198
   §4 puts it ON TOP OF THE RAIL, and the first build of that rendered the
   rail's five titles and the word "Intro" straight through the card's body copy
   — both unreadable. Measured by looking; the render is in 199's assets.

   ⚠️ THE ALTERNATIVE WAS TO MAKE THE OPEN CARD OPAQUE, AND IT WAS REJECTED.
   That would delete the frosting on the one card where it is most visible, and
   C16, C24, C28 and C30 all exist to protect it. What changed is what sits
   BEHIND the card, so that is what this addresses.

   ⚠️ pointer-events GOES WITH THE OPACITY. A rail at opacity 0 that still takes
   taps is 156 §12's defect — an invisible control that does something — and the
   margins around the open card are where 198 §4 says the close now lives.
   Keyboard is unaffected: Escape still closes and close() restores focus. */
.is-cards-mobile.is-card-open .intro__rail {
  opacity: 0;
  pointer-events: none;
}

/* ⚠️ ON BOTH EDGES, AND THE DURATION IS THE OPEN'S OWN. cards.js publishes
   --card-open-dur from ?cardopendur=, so the fade cannot drift from the
   movement it accompanies. The rule is on .intro__rail rather than inside the
   block above so the fade back IN on close is transitioned too. */
.is-cards-mobile .intro__rail {
  transition: opacity var(--card-open-dur, 0.6s) ease;
}

/* ⚠️ NO TRANSITION UNDER reduce — 171 §11's rule is to zero the animated dials
   and leave the resting frame alone. The rail still goes to 0 while a card is
   open, because that is the LAYOUT (something opaque is over it), not motion. */
@media (prefers-reduced-motion: reduce) {
  .is-cards-mobile .intro__rail { transition: none; }
}

/* THE CLIP STOPS ABOVE THE RAIL. --cards-bottom is written by cards.js from
   the rail's measured top edge; the 50% fallback is what shows for the one
   frame before measure() runs, and it is a fallback rather than a value —
   deliberately the top half, so a failed measurement leaves the cards away
   from the rail rather than on top of it.

   ⚠️ SHARED BY SELECTOR LIST, NOT BY A HOISTED CUSTOM PROPERTY (000 §5's
   scoping bullet, and 196 said this pass would reach for it). --intro-inset
   resolves in the element that DECLARES it; both of these need the same three
   edges, and a shared `--cards-box` on #intro would resolve its var()s against
   #intro. The two rules are one rule with two subjects. */
.is-cards-mobile .intro__cards,
.is-cards-mobile .intro__glow {
  /* Full width. ⚠️ NOT inset like desktop's `right: var(--intro-inset)`: the
     cards slide in from the RIGHT (196 §5) and the clip is what stops them
     painting off the composition, so an inset clip would cut the arriving
     block at a line 36px inside the screen instead of at its edge. The open
     card's own gutter is ?cardopenmargin=, measured in cards.js, not this. */
  right: 0;
  bottom: var(--cards-bottom, 50%);
}

/* ⚠️ THE FRONT CARD'S BODY IS PAINTED ON MOBILE TOO, AND THIS BLOCK IS WHERE A
   RULE USED TO BE THAT SAID OTHERWISE (198 §2). 196 §4 had mobile drop the
   --card-front term so no card showed body copy at rest; 198 §2 reverses it,
   and the reversal is not a change of mind — it is an argument expiring. The
   8px figure that justified removing it was computed for a SCALED-DOWN DESKTOP
   BLOCK (196 §2), and we did not build that: mobile got its own proportions and
   the body type never shrank. So there is no mobile rule here at all any more,
   and .card__body's own `opacity: max(var(--card-front, 1), var(--card-open, 0))`
   governs both platforms — front card shows body, back cards show bar and title.

   ⚠️ C37 IS INVERTED, NOT DELETED (198 §6). It now asserts the front card's body
   IS visible at rest on mobile. A check left asserting the old behaviour would
   have been the fourth stale instrument this session. */
