/* ============================================================
   FAN — alternative homepage, scroll-scrubbed.
   The hero never moves: name, kicker and chair hold their
   homepage positions. Scrolling scrubs the cards' pop: they leave
   the ball horizontally, bend up / level / down into a vertical
   stack, overshoot and settle. The keyframes below ARE the path;
   the scroll position is the playhead. fan.js sets --p (0..1) and
   each card's paused animation is offset into it by a negative
   animation-delay — the standard scrub trick, so the curve and
   bounce live in keyframes but obey the scroll.

   One shared keyframe set does all three paths: each card carries
   its own --tx/--ty/--rot target and the keyframes are written in
   calc() fractions of those, so the curve and bounce are tuned in
   exactly one place.

   House rules kept: no radius, no shadow, hairlines, mono labels,
   gaps from --sp-*.
   ============================================================ */

body.case-page.fan main { padding: 0; }   /* no slack below the stage: the pinned hero must never slide */
/* fan.css is only loaded by this page, so styling html here is safe.
   The page scrolls 120vh to drive the fan while LOOKING static, so the
   rubber-band at either end reads as a glitch — suppress it. */
html { overscroll-behavior-y: none; }

.fan-stage {
  position: relative; height: 460vh;   /* the scroll distance the pop plays over */
  /* The figure boxes are allowed to hang past the viewport edge (the right
     column sits 1.5 --gap-x out, and its --fig-w square clears the edge at
     most widths), but that must never become sideways scroll. clip, not
     hidden: hidden would make the stage a scroll container and unstick the
     pinned hero; clip only trims the paint and the scrollable overflow. */
  overflow-x: clip;
}
/* The hero composes proportionally rather than sitting on fixed margins.
   Horizontally the 12-column grid does the work, so name and chair separate
   as the viewport grows — but only up to 1600, after which the grid centres.
   Past that they drift far enough apart to stop reading as one composition.
   Vertically both are centred on the row and offset from each other by
   tokens, so the pair always sits around the middle of the screen. */
body.case-page.fan .fan-pin {   /* outspecify .wrap, which sets max-width:none */
  /* Full viewport height, with the nav's band taken out of the CONTENT box
     rather than out of the element. .fan-figures is inset:0 in here and clips
     vertically at its own edges, so a pin starting below the nav put the clip
     line exactly under it. Behind an opaque nav that was invisible; behind a
     see-through one it is a hard slice across the figures as they ride past.
     Spanning the full height moves the clip to the screen edge where it
     belongs, and the padding keeps align-items:center composing over the same
     area as before, so the hero itself does not move. */
  position: sticky; top: 0;
  height: 100vh;
  padding-top: var(--nav-h);
  box-sizing: border-box;
  display: grid; grid-template-columns: repeat(12, 1fr);
  column-gap: var(--sp-24);
  align-items: center;
  max-width: 1600px; margin-inline: auto;
  /* lift the whole composition off the midline — a centred hero sits a touch
     low optically, since the nav already occupies the top of the frame */
  padding-bottom: var(--sp-88);
}

/* ---- identity: exact homepage rest values, fully static ---- */
.fan-id {
  grid-column: 2 / 7; grid-row: 1; align-self: center;
  width: fit-content;
  /* centred, then lifted: margin-bottom on a centred item shifts it up by
     half its value, so 128 + 128 puts it 128 above the midline */
  margin-bottom: calc(var(--sp-128) + var(--sp-128));
}
.fan-id .wordmark {
  font-family: var(--font-hand); font-weight: 400;
  font-size: 40px; line-height: 1.2; letter-spacing: .05em;
  color: var(--fg); margin: 0 0 var(--sp-16);
}
.fan-id .home-kicker {
  font-family: var(--font-mono); font-size: 14px; line-height: 16px;
  letter-spacing: -0.04em; text-transform: uppercase; color: var(--label); margin: 0;
}
/* The work-history drop-down. Everything about the reveal comes from case.css
   via the shared .home-id / .home-history classes; the only thing this page has
   to change is that it must not affect layout. .fan-id is centred in its grid
   row, so a list that grew the block in flow would push the name UP by half its
   height as it opened. Taken out of flow, the composition holds still and only
   the list moves. */
.fan-id { position: relative; outline: none; }
.fan-id .home-history {
  position: absolute; top: 100%; left: 0;
  width: max-content; max-width: 100%;
}
.fan-id .wordmark, .fan-id .home-kicker { cursor: default; }

/* ---- landing ----------------------------------------------------------
   The page arrives as just the illustration; the name settles in a beat
   later. Deliberately on the CHILDREN, not on .fan-id: the parent already
   carries a scroll-driven transform for its exit, and an animation on the
   same property would fight it and then snap back when the animation ended.

   backwards fill is what holds them hidden through the delay, so this needs
   no JS and cannot flash the name before the script runs. */
@keyframes fan-name-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
.fan-id .wordmark,
.fan-id .home-kicker {
  animation: fan-name-in 900ms cubic-bezier(.16, .84, .34, 1) backwards;
}
.fan-id .wordmark    { animation-delay: 1000ms; }
.fan-id .home-kicker { animation-delay: 1240ms; }   /* trails the name slightly */

@media (prefers-reduced-motion: reduce) {
  .fan-id .wordmark, .fan-id .home-kicker { animation: none; }
}

/* ---- the anchor: chair at its homepage spot, static ---- */
.fan-anchor {
  grid-column: 8 / 12; grid-row: 1; align-self: center;
  justify-self: start;              /* was centre — sits further left in its columns */
  margin-top: var(--sp-128);        /* 64 below the midline */
  /* 340/1512 of the Figma frame. In vw, not px, so the illustration and the
     figures around it scale to the viewport exactly as they do in the frame —
     the whole composition keeps the design's proportions at any width. */
  width: var(--unit); max-width: 100%;
  position: relative;
}
/* The illustration is split into two layers so the cards can fly BETWEEN
   them: shadow behind (z0), cards (z1), figure and chair in front (z2).
   Both PNGs share the original canvas, so one crop rule aligns them exactly.
   .fan-art keeps position:relative with z-index auto, so it does NOT open a
   stacking context and the three z-indexes still compare with each other. */
.fan-art {
  position: relative;
  width: 100%; aspect-ratio: 1 / 1; overflow: visible;   /* main.png is 1020x1020 */
}
/* The old asset was a tight crop of a larger canvas, hence the offsets it needed.
   The new export is already trimmed, so it just fills its box. */
.fan-art img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain; max-width: none;
}
.fan-shadow { z-index: 0; }
.fan-front  { z-index: 2; }

/* ---- the cards ---- */
/* Origin: the ball — a touch left of and above the anchor's centre, then
   pushed --launch further right so the cards emerge from deeper behind the
   figure. The end of the path takes the same --launch back off, so moving
   this number changes only where the cards come FROM, never where they land. */
.fan-cards {
  --launch: 56px;
  position: absolute; top: 42%; left: calc(42% + var(--launch)); z-index: 1;
  width: 0; height: 0;
}
.fan-card {
  position: absolute; top: 0; left: 0;
  width: 168px; min-height: 88px;
  margin: -44px 0 0 -84px;                     /* centred on the origin */
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: var(--sp-12);
  border: 1px solid var(--border);
  background: var(--bg);
  text-decoration: none; color: inherit;
  /* unlaunched: small and invisible behind the chair (the 0% frame) */
  opacity: 0;
  transform: translate(0, 0) rotate(0deg) scale(.5);
  transition: border-color .15s var(--ease), background-color .15s var(--ease);
  will-change: transform, opacity;
  /* scroll scrub: the animation is PAUSED and --p moves its playhead through
     a negative delay. Duration 1s over a 1.55s span, staggered starts: the
     last card lands at --p ~= 0.84, leaving the final ~16% of the scroll
     inert so the settled fan sits well clear of the boundary bounce. */
  animation: fan-pop 1s linear both paused;
  animation-delay: calc(var(--p, 0) * -1.55s + var(--stagger, 0s));
}
.fan-card:nth-child(1) { --stagger: 0s; }
.fan-card:nth-child(2) { --stagger: .15s; }
.fan-card:nth-child(3) { --stagger: .3s; }
/* The stack: the tilted cards sit 4px forward of the middle one, so the
   corner of the edge NEAREST the middle card lands exactly on the middle
   card's right-edge line — rotation pulls that corner back by
   (w/2)(1-cos5) + (h/2)sin5 = 4.3px, and this puts it back. */
.fan-card:nth-child(1) { --tx: -142px; --ty: -112px; --rot:  5deg; }
.fan-card:nth-child(2) { --tx: -146px; --ty:    0px; --rot:  0deg; }
.fan-card:nth-child(3) { --tx: -142px; --ty:  112px; --rot: -5deg; }

/* The path, shared by all three: leaves horizontally (x runs ahead of y),
   bends into its row, overshoots, settles. The overshoot IS the bounce. */
@keyframes fan-pop {
  0%   { opacity: 0;
         transform: translate(0, 0) rotate(0deg) scale(.5); }
  /* --launch is subtracted back progressively: none at 0% (start furthest
     right), a third of it by 30%, all of it from 75% on. So the extra
     distance is spent on the early travel and the landing is untouched. */
  30%  { opacity: 1;
         transform: translate(calc(var(--tx) * .45 - var(--launch) * .34),
                              calc(var(--ty) * .10))
                    rotate(0deg) scale(.92); }
  75%  { transform: translate(calc(var(--tx) * 1.07 - var(--launch)),
                              calc(var(--ty) * 1.12))
                    rotate(calc(var(--rot) * 1.5)) scale(1.02); }
  100% { opacity: 1;
         transform: translate(calc(var(--tx) - var(--launch)), var(--ty))
                    rotate(var(--rot)) scale(1); }
}
.fan-card:hover, .fan-card:focus-visible {
  border-color: var(--fg);
  background: rgba(31,30,30,.06);              /* ground shifts, never elevation */
}
.fan-card .idx {
  font-family: var(--font-mono); font-size: var(--t-micro); line-height: var(--lh-micro);
  letter-spacing: .06em; text-transform: uppercase; color: var(--sublabel);
  margin: 0; padding: 0; border: 0; font-weight: 400;
}
.fan-card .idx b { color: var(--fg); font-weight: 400; }

/* ---- cursor-attached preview ---- */
.fan-preview {
  position: fixed; top: 0; left: 0; z-index: 90;
  width: 340px; aspect-ratio: 4 / 3;
  pointer-events: none; opacity: 0;
  transform: translate3d(-9999px, -9999px, 0);
  transition: opacity .2s var(--ease);
  will-change: transform, opacity;
}
.fan-preview.is-on { opacity: 1; }
.fan-preview img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain; display: block;
  opacity: 0;                      /* one img per card, stacked; only the hovered one shows */
}
.fan-preview img.is-active { opacity: 1; }

@media (max-width: 1100px) {
  /* the illustration's width is NOT set here: it reads --unit at every size,
     which is what keeps it and the figures on one scale. A fixed px here
     silently broke that below 1100. */
  .fan-card { width: 140px; min-height: 80px; margin: -40px 0 0 -70px; }
  .fan-card:nth-child(1) { --tx: -108px; --ty: -102px; }
  .fan-card:nth-child(2) { --tx: -112px; --ty:    0px; }
  .fan-card:nth-child(3) { --tx: -108px; --ty:  102px; }
  .fan-preview { width: 280px; }
}

/* Narrow: no pop. The spray needs width it does not have, and a cursor
   preview is meaningless on touch. Plain stacked list. */
@media (max-width: 860px) {
  .fan-stage { height: auto; }
  /* Must match `body.case-page.fan .fan-pin`, which sets display:grid. A bare
     .fan-pin loses to it, and the grid then drops the name and the illustration
     into the same cell, one on top of the other. */
  body.case-page.fan .fan-pin {
    position: static; height: auto; display: block;
    padding-top: calc(var(--nav-h) + var(--sp-24));   /* the desktop clearance came
       from the sticky offset, which block layout drops; without it the name sits
       behind the fixed header */
    padding-bottom: var(--sp-128);
  }
  .fan-id { padding: 0; margin: var(--sp-16) 0; }
  /* grid-column resets the desktop 8-12 placement: keep it and the anchor hangs
     off the right edge of a phone screen. Width stays 224 so the stack below
     it is unchanged. */
  .fan-anchor { grid-column: 1 / -1; margin: 0 0 var(--sp-48); width: 224px; }
  .fan-cards { position: static; width: auto; height: auto; }
  .fan-card {
    position: static; width: auto; height: auto; margin: 0;
    animation: none; transform: none; opacity: 1;
    border: 0; border-bottom: 1px solid var(--border);
    padding: var(--sp-24) 0;
  }
  .fan-preview { display: none; }
  /* the act-two transform is declared later in the file at the same specificity,
     so a bare .fan-id loses to it and the name is dragged off the top of a phone
     screen (--q pins to 1 when the stage has no travel to scrub) */
  body.case-page.fan .fan-id,
  body.case-page.fan .fan-anchor,
  body.case-page.fan .fan-art img { transform: none; }
}

/* Reduced motion: no pop, no cursor tracking — cards rest in their final
   positions immediately. */
@media (prefers-reduced-motion: reduce) {
  .fan-stage { height: auto; }
  .fan-pin { position: static; height: auto; }
  .fan-card {
    animation: none; opacity: 1;
    /* the 100% frame, --launch included, or these land right of the animated ones */
    transform: translate(calc(var(--tx) - var(--launch)), var(--ty))
               rotate(var(--rot)) scale(1);
  }
  .fan-preview { display: none; }
}


/* ============================================================
   ACTS TWO AND THREE
   Act two: the figures ride up into their positions around the
   illustration, which holds still. Act three: the illustration
   stops being fixed and travels up WITH them, which is the only
   way the lower half of a composition taller than the hero is
   ever seen.
   Each figure's --x/--y is an offset from the illustration's
   centre, in multiples of --fig-w, taken from the Figma frame.
   fan.js publishes --art-cx/--art-cy/--pin-h (pin-relative px).
   They are decoration: aria-hidden, and never hit-testable.
   ============================================================ */
/* On the stage so BOTH the group and the illustration inherit them: act three
   moves the two by the same distance, and they must read one source for it. */
.fan-stage {
  /* The composition settles smaller than the frame draws it. One knob: the
     figure offsets are all in --fig-w, so shrinking it contracts the whole
     arrangement toward the illustration's centre rather than just the artwork. */
  --comp-scale: 0.84;
  /* The illustration settles a touch smaller than the composition scale: at
     0.84 it read a shade large beside the figures (Josh, 4 Sep 2026). Only the
     settled size — the hero-size chair is right as it is, and --fig-w/--gap-x
     stay on --comp-scale so nothing else moves. */
  --art-settle: calc(var(--comp-scale) * 0.95);

  /* ---- how the composition answers a change of screen size ---------------
     SIZE and SPACING are deliberately not the same unit.

     --unit is the size of a drawing. It tracks the viewport (22.5vw, the
     illustration's share of the Figma frame) but is clamped at both ends: past
     about 1500px the people would be absurdly large for what they are, and
     below ~950 they would swamp a small laptop. .fan-anchor and every figure
     read from this ONE value, so the illustration and the figures can never
     drift apart in scale.

     --gap-x is the horizontal spacing, and is NOT clamped. Spacing keeps
     tracking the viewport after size has stopped, so a wider screen spreads the
     same people further apart instead of simply enlarging them — the
     composition goes on covering the screen rather than growing a fixed shape.
     --spread-x widens the arrangement past what the frame draws.

     Vertical spacing stays on --unit, not on the viewport: the composition's
     height feeds --span-*, --group-from and --act3, so tying it to width would
     make the scroll choreography drift every time the window changed shape. */
  --unit: clamp(190px, 22.5vw, 340px);
  --spread-x: 0.97;
  /* Rows run slightly uphill to the right rather than dead level: every
     figure is lifted by --row-tilt --fig-w per --gap-x it sits right of the
     chair (and dropped by the same to the left). The chair is the pivot, the
     lattice stays even — it is sheared, not scattered. 0 = level rows. */
  --row-tilt: 0.24;
  /* How big the drawings are inside their boxes, on top of each figure's own
     --w. Scales every figure at once without moving a single lattice slot. */
  --fig-scale: 1;
  --fig-w: calc(var(--unit) * var(--comp-scale));
  --gap-x: calc(22.5vw * var(--comp-scale) * var(--spread-x));
  /* the composition's own edges from the art centre, in --fig-w, tilt
     included. KEEP IN STEP WITH THE --x/--y VALUES IN THE MARKUP. */
  --span-top: 0.81;
  --span-bottom: 4.30;
  --edge-pad: var(--sp-24);
  /* The --x/--y marks were taken from the Figma frame, then regularised on
     4 Sep 2026 onto one staggered lattice centred on the illustration: columns
     1.5 --gap-x apart, rows 1.2 --fig-w apart, each row offset half a column
     from the one above (aligned rows at ±0.75/±2.25, offset rows at 0/±1.5).
     The lattice is symmetric about the chair, which sits at 0.599 of the
     width, so the right-hand column runs off the edge on narrower screens —
     accepted (Josh, 4 Sep 2026); .fan-stage clips it so it never scrolls. The illustration occupies the (0, 0) slot, so its two
     row-mates sit at ±1.5. The group is NOT nudged: a fudge here would put
     every figure off its mark. */
  --group-x: 0;

  /* act two: far enough down that the whole group clears the bottom edge */
  --group-from: calc(var(--pin-h, 100vh) - var(--art-cy, 50%)
                     + var(--span-top) * var(--fig-w));
  /* act three: how far the composition hangs below the hero, which is exactly
     how far everything has to travel for its last row to be seen */
  --act3: calc(var(--art-cy, 50%) + var(--span-bottom) * var(--fig-w)
               + var(--edge-pad) - var(--pin-h, 100vh));
}

.fan-figures {
  position: absolute; inset: 0;
  pointer-events: none; z-index: 0;
  /* Clip VERTICALLY only. The figures must be hidden above and below the hero
     (they ride in from underneath, and must not show over the section beneath),
     but they have to be free to spread past .fan-pin, which is capped at
     1600px and centred. overflow:hidden cannot do one axis without the other —
     set one to hidden and the other stops being visible too — so this is
     clip-path with negative side insets, which widens the rect instead. */
  clip-path: inset(0 -100vw 0 -100vw);
  /* Held back until fan.js has measured the illustration. --art-cx/--art-cy do
     not exist before that, which makes the figures' left/top invalid, and an
     invalid length computes to auto — so every figure piles into the layer's
     top-left corner and visibly snaps into place a frame later. They start
     below the fold anyway, so there is nothing to see in the meantime. */
  visibility: hidden;
}
.fan-stage.is-measured .fan-figures { visibility: visible; }
/* The clip box and the moving group MUST be different elements. Put the
   transform on the element that also has overflow:hidden and the clip travels
   with the content, silently cutting off everything below it — and
   getBoundingClientRect still reports the figures as on-screen, so it never
   shows up in layout measurements. Only a screenshot catches it. */
.fan-group {
  position: absolute; inset: 0;
  /* ONE transform for the whole group: the figures hold station relative to
     each other and to the illustration throughout. Act two brings it up to
     rest (--q); act three carries it further (--r). */
  transform: translate(calc(var(--group-x) * var(--fig-w)),
                       calc(var(--group-from) * (1 - var(--q, 0))
                            - var(--act3) * var(--r, 0)));
  will-change: transform;
}
.fan-fig {
  position: absolute;
  left: calc(var(--art-cx, 60%) + var(--x) * var(--gap-x));
  top:  calc(var(--art-cy, 50%) + (var(--y) - var(--x) * var(--row-tilt, 0)) * var(--fig-w));
  /* Every figure gets the SAME container: a --fig-w square. The drawing sits
     inside it (contain, centred) and --w scales the drawing within the box, so
     a figure's size is tuned per image without its slot in the lattice
     changing. 1 = the common size; absent means 1. Landscape assets
     (cooking, telescope, football) fit the box's width, which is what their
     old width-based sizing did, so nothing moved in the refactor. */
  width: var(--fig-w); height: var(--fig-w);
  /* --x/--y address the container's CENTRE.

     The second translate is the parallax. .fan-group carries the whole set in;
     this adds each figure's own share on top, so a --depth of 1.3 starts 30%
     further out and closes the gap 30% faster. It multiplies the distance
     REMAINING to the resting place, which is zero once they arrive — so the
     figures travel at different speeds and still land exactly on the marks.
     It also falls to zero for act three, where the whole composition moves as
     one piece again. */
  transform: translate(-50%, -50%)
             translateY(calc(var(--group-from) * (1 - var(--q, 0))
                             * (var(--depth, 1) - 1)));
}
.fan-fig img {
  display: block;
  width: 100%; height: 100%; max-width: none;
  object-fit: contain;
  transform: scale(calc(var(--w, 1) * var(--fig-scale, 1)));
}

/* Act three: the illustration stops being the fixed point and rides up with
   the group, by the same distance, so their relationship never changes. */
/* --art-nudge puts the illustration's centre at 0.599 of the width, where the
   Figma frame has it. That matters beyond taste: every figure is placed relative
   to this centre, so it decides which edge the composition bleeds over, and the
   grid alone lands it at 0.68. fan.js computes it, because the grid's position
   varies with gutters and max-width and no fixed vw offset is right everywhere.
   Act three's rise rides along in the same transform. */
.fan-anchor {
  transform: translate(var(--art-nudge, 0px),
                       calc(var(--act3) * -1 * var(--r, 0)));
}
/* The shrink belongs on the artwork, NOT on .fan-anchor: the case-study cards
   are inside the anchor and were being scaled down with it. Scaling about the
   art's own centre keeps that centre still, which is what every figure is
   placed from, so the artwork shrinks into the size the figures are drawn at
   without dragging the composition or the cards. */
/* The shrink goes on the LAYERS, not on .fan-art. A transform opens a stacking
   context, so scaling .fan-art itself would trap .fan-shadow and .fan-front
   inside it and the cards could no longer fly between them — they would land
   wholly in front of or behind the illustration. Both layers share one box, so
   scaling them separately by the same factor is identical to scaling the group. */
.fan-art img {
  transform: scale(calc(1 - (1 - var(--art-settle)) * var(--q, 0)));
}

/* The name leaves through the top, but not straight away: it holds station for
   the first --id-dwell of act two so it is still there while the figures are
   arriving, then clears out over what is left. Driving it off raw --q made it
   leave before anything had come in to replace it. The fixed nav is opaque, so
   it slides under the band rather than through it. */
.fan-id {
  /* The name is still through acts one and two, and then holds on for the first
     --id-hold of act three as well, so it is the last thing to leave: the
     illustration and the figures are already travelling before it moves at all.
     Keyed to --r so it stays tied to act three however the act split changes. */
  --id-hold: 0.07;
  --id-go: max(0, min(1, calc((var(--r, 0) - var(--id-hold))
                              / calc(1 - var(--id-hold)))));
  transform: translateY(calc(var(--id-go) * -60vh));
  /* Gone before it reaches the nav. --id-fade-end is the --id-go at which the
     name's top meets the bottom of the nav band, measured in fan.js; the fade
     runs over --id-fade-span before that, so the name finishes leaving just as
     it arrives rather than passing up through the links. The fallback keeps it
     sane in the frames before the measurement runs. */
  --id-fade-span: 0.16;
  opacity: max(0, min(1, calc((var(--id-fade-end, 0.45) - var(--id-go))
                              / var(--id-fade-span))));
  will-change: transform, opacity;
}

/* Reduced motion: no travel. Everything simply sits where it lands. */
/* Reduced motion: hold the settled composition rather than dropping every
   transform, which would strand the illustration at its grid position while the
   figures still expect it on the 0.599 mark. */
@media (prefers-reduced-motion: reduce) {
  .fan-group, .fan-id { transform: none; }
  .fan-anchor { transform: translateX(var(--art-nudge, 0px)); }
  .fan-art img { transform: scale(var(--comp-scale)); }
}
/* ============================================================
   THE SCROLL HINT
   The stage is 460vh of scroll behind a hero that never moves, so a first
   screen with no hint on it reads as a page that has nothing else. House
   voice: an arrow is text, so it is mono, it is ink at 40%, and it is flush
   left — placed in the grid on column 2 so it hangs under the name rather
   than being nudged onto the gutter by hand.

   It fades on the first flick, keyed to --t (the stage's raw progress, which
   fan.js publishes). Anything that lingers on a 460vh stage lingers for a
   very long time, and by then the figures are already arriving.
   ============================================================ */
.fan-scroll {
  /* Centred on the composition rather than hung under the name, and low —
     the pin lifts its content 88 off the bottom edge, and the hint wants to
     be nearer the edge it points at than the work above it. */
  grid-column: 1 / -1; grid-row: 1;
  align-self: end; justify-self: center;
  margin-bottom: calc(var(--sp-40) * -1);
  font-family: var(--font-mono); font-weight: 400;
  font-size: 20px; line-height: 1;       /* the rail step: small, but a thin glyph needs the size to be found */
  color: var(--muted);
  pointer-events: none;                  /* it points at the scroll, it is not a control */
  /* It holds while the cards are out. Act one ends at --t 0.28 with the fan
     open, so the hint stays solid right through that and only leaves as act
     two starts and the figures come in to replace it. */
  opacity: max(0, min(1, calc((0.36 - var(--t, 0)) / 0.08)));
  animation: fan-bob 1.8s ease-in-out infinite;
}
@keyframes fan-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}
/* No hint where there is nothing to hint at: under 860 the homepage is a
   column barely taller than the screen, and under reduced motion the stage
   stops pinning altogether, which leaves the arrow at the foot of a static
   page rather than the foot of the first screen. */
@media (max-width: 860px) { .fan-scroll { display: none; } }
@media (prefers-reduced-motion: reduce) { .fan-scroll { display: none; } }

/* ============================================================
   PHONE COMPOSITION (≤860). Appended LAST on purpose: the act rules above
   share this specificity, and an earlier override loses to them — that is
   exactly how the name ended up invisible (its transform was reset in the
   860 block up top, but its act-two fade, declared later, still painted it
   at opacity 0 once --q pinned to 1). Everything the phone needs to undo
   lives here, after every rule it undoes.
   Order on a phone: name and role · the chair · the three pieces of work,
   full width · the figures as a field you swipe. No choreography.
   ============================================================ */
@media (max-width: 860px) {
  body.case-page.fan .fan-id { opacity: 1; transform: none; }
  /* stretch, not the desktop centre: a centred cross-axis lets an item size to
     its own content, and the field's content is wider than the screen — it
     would lay out 611px wide and hang off both edges instead of scrolling */
  body.case-page.fan .fan-pin { display: flex; flex-direction: column; align-items: stretch; }
  /* align-self:center is the desktop grid's, and on a flex column it means
     "shrink to your content and centre" — which is why the name and the work
     sat in a narrow centred column with the gutters ignored. Left for the
     text, full width for the block that holds the cards. */
  .fan-id { order: 1; align-self: start; }
  .fan-anchor { order: 2; align-self: stretch; scroll-margin-top: calc(var(--nav-h) + var(--sp-24)); }

  /* The anchor holds the chair and the three cards, and on a phone it stacks
     them: chair first with room around it, then the work full width beneath
     (Josh, 6 Sep 2026). It was 224px wide with the cards inside it, which
     capped them at 224 too — a third of the screen, in a column of nothing. */
  .fan-anchor { width: auto; display: flex; flex-direction: column; margin: 0; }
  .fan-art {
    order: 1; width: 224px; align-self: center;
    margin: var(--sp-64) 0;                 /* the graphic gets its own air */
  }
  .fan-cards { order: 2; }

  /* No figures here below 860: they are the content of about.html instead,
     drawn at the chair's own size rather than shrunk into a lattice (Josh,
     6 Sep 2026). index.html holds them in a <template> so a phone does not
     download them at all; this only stops the empty box taking room. */
  .fan-figures { display: none; }

  .fan-stage { overflow-x: visible; }
  .fan-card .idx { font-size: 12px; line-height: 16px; }     /* labels never below 12 on touch */
}

/* ============================================================
   Career summary, set in the figure lattice in place of a drawing.
   Same --fig-w box as a figure, so it holds its slot and parallax
   like one. Mono 12/20 (label size, small line height), sentence case: a
   deliberate exception to "mono is for labels" (Josh, 8 Oct 2026),
   so the paragraph reads with the drawings rather than as page copy.
   ============================================================ */
.fan-blurb {
  /* Centred in its slot, so the paragraph's middle sits on the sheared row
     line like the drawings' (measured: their visual centres are within 2px of
     their slot centres). */
  display: flex; align-items: center;
  /* A shade wider than a drawing's square (Josh, 8 Oct). At exactly 1.0 the
     last line was a lone "week." on wide screens; 1.05 clears it at every
     width from 1110 up except 1410-1440, where text-wrap: pretty catches it. */
  width: calc(var(--fig-w) * 1.05);
}
.fan-blurb p {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--t-label); line-height: var(--lh-small); letter-spacing: -0.04em;
  color: var(--fg);
  text-wrap: pretty;
}
/* Below 1100 the drawings shrink with the screen and the type does not, so a
   one-square column runs taller than its slot and, centred, reaches up into
   the chair (0px clear at 900, measured). A wider column keeps it short. */
@media (max-width: 1100px) {
  .fan-blurb { width: calc(var(--fig-w) * 1.4); }
}
/* The readable copy of the same text. The composition is aria-hidden, so
   this is what assistive tech reads; it is never painted. */
.fan-id .home-summary {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
