/* ============================================================
   Landing intro — the full-height opener that introduces the
   person before the page introduces the tools.

   It is the only section on the site that claims a whole screen,
   and that claim is the reason it exists: everything below it is
   a list of things, and a list opens badly. What it must not do
   is claim a whole screen and then look like the page ended, so
   the scroll cue at the bottom is load-bearing rather than
   decorative — see .scroll-cue.
   ============================================================ */

.intro{
  /* One screen, less the header above it. The header is sticky, not fixed, so
     it occupies flow at the top of the page; a plain 100vh here would push the
     intro's own bottom edge — and with it the scroll cue — exactly --nav-h
     below the fold, which is the one place a "there is more down here" hint
     must never be. */
  /* --bar-h is the phone tab bar, and 0px everywhere there isn't one, so this
     one expression covers both. Without it the section would be exactly one
     bar too tall on a phone and the scroll cue at its foot — the whole reason
     the section is allowed to claim a screen — would sit behind the bar. */
  min-height:calc(100vh - var(--nav-h) - var(--bar-h));
  /* svh is the *small* viewport height: the height with the mobile browser's
     address bar showing. vh on a phone is the large height, so a vh-sized
     section is taller than the visible screen until the user scrolls and the
     bar retracts — the cue would start life hidden behind the URL bar. Sizing
     to the small height means the section always fits, and the extra space
     when the bar retracts is just breathing room. Declared second so engines
     that know svh take it and the rest keep the line above. */
  min-height:calc(100svh - var(--nav-h) - var(--bar-h));

  /* Two rows: the copy takes the free space and centres in it, the cue sits on
     the bottom edge. Centring the copy in a *row* rather than in the section
     keeps it optically centred regardless of how tall the cue is. */
  display:grid;
  grid-template-rows:1fr auto;
  align-items:center;
  padding-top:40px;
  padding-bottom:28px;
}

/* Copy on the left, the mark on the right. Before the mark moved up here the
   copy was a single column against a full-width container, which on a wide
   screen is a block of text in the left third and a lot of nothing beside it —
   the layout read as unfinished rather than spacious. The mark gives the right
   half something to be, and the pair reads as one composition.
   The mark's column is capped so the extra width on a large display goes to
   the headline, which can use it, rather than to a bigger circle, which
   cannot. */
.intro-body{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,380px);
  gap:clamp(32px, 5vw, 72px);
  align-items:center;
}
.intro-copy{ min-width:0; }

/* ---------- the mark ----------
   Moved here from the tools hero below. It was competing with this section for
   the role of "the first thing you see", and the section that introduces the
   person is the one that should carry the identity. */
.intro-mark{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
}
/* A soft wash behind the disc so it sits in the page instead of on it. Sized
   off the container and blurred well past its own edge, it reads as light
   coming off the mark rather than as a second circle drawn behind it — which
   is what a tighter, harder gradient would look like. */
.intro-mark::before{
  content:"";
  position:absolute;
  width:78%;
  aspect-ratio:1;
  border-radius:50%;
  background:radial-gradient(circle,
    color-mix(in srgb, var(--role-nav) 22%, transparent) 0%,
    transparent 66%);
  filter:blur(34px);
  pointer-events:none;
}
@supports not (background: color-mix(in srgb, red 10%, blue)){
  .intro-mark::before{ background:radial-gradient(circle, rgba(90,169,255,0.2), transparent 66%); }
}

/* The mark sits in a ring. The logo file is padded — the glyph fills only the
   middle of it — so the image is scaled past the ring's edge to bring the glyph
   up to a readable size, and the ring clips the overflow. The image is taken
   out of flow so it can be oversized without stretching the circle into an
   ellipse; `aspect-ratio` then has nothing to fight with. */
.gizmo{
  position:relative; /* also lifts the disc above the wash behind it */
  display:block;
  overflow:hidden;
  width:clamp(200px, 19vw, 300px);
  aspect-ratio:1;
  border:1px solid var(--line);
  border-radius:50%;
  background:var(--surface);
}
.gizmo img{
  position:absolute;
  top:50%;
  left:50%;
  width:124%;
  height:auto;
  transform:translate(-50%, -50%);
}

/* Four words, so it can take the site's full display size — a short headline is
   what earns a large one. The earlier three-line version had to be set smaller
   just to fit, which is the wrong way round: the size should follow from how
   much there is to say, not the other way about. */
.intro-title{
  font-size:var(--t-display);
  line-height:1.05;
  letter-spacing:-0.024em;
  margin:0 0 16px;
}

/* The claim, now at reading size rather than headline size. It is a sentence
   with a real clause in it — "that don't break when 1000s of players join at
   once" is the specific part, and specifics read as prose, not as display type.
   Set large it would have been four lines of very big text competing with the
   greeting above it; set here it supports the greeting instead.
   The measure is capped near 52 characters so it breaks into two or three even
   lines rather than one long ribbon on a wide screen. */
.intro-lead{
  color:var(--muted);
  /* The middle term carries a rem as well as a vw, and that is the whole
     point of it. Written as a bare `1.5vw` this line was pinned to the
     viewport: a reader who had raised their browser's default text size got a
     lead paragraph that did not move, while the eyebrow above it and the
     figures below it both grew. Adding the rem term puts the reader's own
     setting back into the sum without changing what the line does across
     widths. Display type below stays viewport-driven on purpose — see the h1. */
  font-size:clamp(1.0625rem, 0.95rem + 0.35vw, 1.1875rem);
  line-height:1.5;
  max-width:52ch;
  margin:0;
}

/* ---------- the figure rail ----------
   Each entry is one sentence broken across three lines, and only the middle
   line is sized to be seen at a glance. That is the whole structure: the
   lead-in and the tail are deliberately small, because an entry where all
   three lines compete is just a paragraph.

   One hairline runs across the top of the group and there is no box around
   any single entry. Boxing them said these are three separate objects; they
   are three readings of one record, and the shared rule says exactly that
   with one line instead of twelve edges. It also keeps the section quiet —
   three filled panels turned the intro back into a component gallery, which
   is the thing the copy above it is trying not to be.

   Read as a row they answer the three questions the greeting leaves open:
   how long, how much, and what now. */
.intro-cards{
  display:grid;
  grid-template-columns:repeat(3, minmax(0,1fr));
  /* Four rows, shared by all three entries — see the subgrid block below.
     The row gap is 0 because the spacing between the four lines is set by
     their own margins, which is where it was before this became a grid. */
  grid-template-rows:repeat(4, auto);
  gap:0 clamp(24px, 3.2vw, 52px);
  list-style:none;
  margin:30px 0 0;
  /* --hit-y is added back on top of the 24px the rule actually wants. Each
     entry's hit area is inset by that much with a negative margin (see
     .intro-card-inner), so without the compensation the group would ride up
     under the hairline by exactly one inset. */
  padding:calc(24px + var(--hit-y)) 0 0;
  border-top:1px solid var(--line);
  max-width:620px;

  /* How far each entry's pressable area extends past its text. Kept here
     because .intro-cards has to subtract the vertical half from its own
     padding, and the two numbers must never drift apart. */
  --hit-x:12px;
  --hit-y:10px;
}
.intro-card{ min-width:0; }

/* The clickable box is the whole entry, not just the link line at its foot —
   a figure with a link under it is a target the size of a sentence, and this
   makes it the size of the block you were already reading.
   It is inset by a negative margin rather than laid out with padding so the
   text keeps its original position: the first entry still lines up with the
   headline's left edge, and the hover fill is the only thing that extends
   past it. The inert third entry carries the same inset so all three sit on
   one baseline. */
.intro-card-inner{
  display:block;
  margin:calc(-1 * var(--hit-y)) calc(-1 * var(--hit-x));
  padding:var(--hit-y) var(--hit-x);
  border-radius:10px;
  color:inherit;
  text-decoration:none;
  -webkit-tap-highlight-color:transparent;
  transition:background var(--dur-1) var(--ease-out);
}
.intro-card-link:hover{ background:rgba(255,255,255,0.04); }

/* ---------- one baseline across three columns ----------
   The rail's whole claim is that these are three readings of one record rather
   than three panels, and the thing that says so is that the four lines line up
   across all three. They did not. Each entry was an independent block, so the
   moment one column's figure wrapped to two lines and its neighbour's did not,
   every line below it in that column dropped by the difference: measured at
   375px the three link lines sat at 576, 580 and 596, and at 1280px the third
   sat 29px below the other two. Read across, the row came apart into three
   fragments — which is the one thing this arrangement cannot afford, because
   the fragments are all it has instead of borders.

   Subgrid is the fix rather than fixed heights: the four rows are declared once
   on .intro-cards and each entry adopts them, so a row is exactly as tall as
   the tallest thing in it across all three columns, and the copy can change
   length without anyone re-measuring anything. Two levels of it, because the
   <a> that carries the hit area sits between the <li> and the four lines and
   has to pass the tracks through rather than start its own.

   It sits below .intro-card-inner rather than beside .intro-card above,
   because `display:grid` here and `display:block` there are the same
   specificity and the last one written wins.

   Guarded, because a browser without subgrid would read `span 4` against the
   four declared rows and distribute each entry across them evenly — four equal
   tracks regardless of what is in them, which is worse than what it replaced.
   Without support the rule does not apply at all and the entries go back to
   being independent blocks, which is where they started. */
@supports (grid-template-rows: subgrid){
  .intro-card,
  .intro-card-inner{
    display:grid;
    grid-row:span 4;
    grid-template-rows:subgrid;
    /* One column, and it may not exceed its share. Without the minmax the
       single implicit track is `auto`, which sizes to max-content and lets the
       longest line set the entry's width — the third entry came out 15px wider
       than its column and broke the very alignment the rows had just fixed. */
    grid-template-columns:minmax(0,1fr);
  }
  /* Each line sits at the top of its own row rather than stretching to fill
     it. It only shows on the row the figure owns — where "4 years" is one line
     in a track sized for the two "81,521 props" needs — and top is the correct
     edge there: the number hangs from the line above it, which is the first
     half of the sentence it finishes. */
  .intro-card-inner > span{ align-self:start; }
}

/* Direct children only. The figure line wraps its own <span> around the counted
   year, and a blanket `.intro-card span` turned that inline number into a block
   — which broke "4 years" onto two lines while the other two entries stayed on
   one, and left the row taller than any of its contents needed. */
.intro-card-inner > span{ display:block; }
/* The two supporting lines share a size and a colour so neither pulls rank on
   the other; they are the two halves of one sentence with the figure between
   them. */
.intro-card .c-pre,
.intro-card .c-post{
  font-size:var(--t-small);
  line-height:1.4;
  color:var(--muted);
}
.intro-card .c-pre{ margin-bottom:6px; }
.intro-card .c-post{ margin-top:6px; }

/* Space Grotesk with tabular figures: the display face gives the number
   presence at a size body text could not carry, and fixed-width figures keep
   "6" and "81,521" on the same rhythm instead of drifting against each other.
   Tightened tracking because it is set large — the same size-specific rule the
   headings follow. */
.intro-card .c-key{
  font-family:var(--font-display);
  font-size:clamp(1.25rem, 1.1rem + 0.7vw, 1.625rem);
  font-weight:600;
  line-height:1.1;
  letter-spacing:-0.018em;
  font-variant-numeric:tabular-nums;
  color:var(--text);
  /* "81,521 props" must not split its number from its unit across a line */
  text-wrap:balance;
}
/* The figure inside the figure line carries .fig (base.css) like every other
   claimed number on the site — the number in the claim colour, the unit beside
   it in the text colour. Nothing is needed here: the shared rule already sets
   both the colour and the weight, and this line's own 600 was going to be
   there regardless. It is noted rather than styled so the next person looking
   for where "81,521" gets its red does not go hunting in this file. */

/* ---------- the link line ----------
   The affordance, and the reason the entries are not merely "clickable if you
   happen to try". It is written the way links have looked since the web began
   — accent colour, underlined, with the direction it goes in — because the
   audience for this page is someone skimming candidates, not someone learning
   an interface. The arrow distinguishes the two destinations: → stays on the
   site, ↗ leaves it.

   The colour is the same on all three: green, the page's proof colour. This
   rail is the one place on the site where every line is an offer of evidence —
   a timeline, a write-up, a list — and the row reads as one set when it is one
   colour, rather than as two internal links and an odd one out. Which trip it
   is, is still said: → stays on the site, ↗ leaves it.

   The line carries `.link` (base.css) and takes the underline recipe from
   there. What it cannot take is the colour, because `.link` reads `target` off
   the element and here the element is a <span> — the anchor is the whole
   entry, three levels up, since the target is the size of the block you were
   already reading rather than the size of one sentence. So the rule below sets
   the same two custom properties itself, and the recipe follows. That
   indirection is the reason `--link-ink` exists rather than the rule simply
   hard-coding two colours. */
.intro-card .c-cta{
  --link-ink:var(--role-proof-hi);
  --link-ink-hi:#96E0A4;
  margin-top:10px;
  font-size:var(--t-small);
  line-height:1.4;
}
/* `.link`'s own fallback keys off `[target]`, which only one of the three
   anchors carries and which is on the ancestor besides, so it would leave two
   of these underlined in blue on an engine without color-mix. */
@supports not (background: color-mix(in srgb, red 10%, blue)){
  .intro-card .c-cta{ text-decoration-color:rgba(110,208,127,0.45); }
}
/* The arrow moves the way the link goes — one step, on the hover of the whole
   entry, so the movement confirms that the block and not just the words is the
   target. Inline-block because a transform does nothing to an inline box. */
.intro-card .c-cta-arrow{
  display:inline-block;
  transition:transform var(--dur-1) var(--ease-out);
}
/* The hover belongs to the whole entry, not to the words — the block is the
   target, so the block's hover is what lights the link. `.link:hover` would
   only fire on the span itself, which is the thing this rail deliberately is
   not. Same two properties, driven from one level up. */
.intro-card-link:hover .c-cta{
  color:var(--link-ink-hi);
  text-decoration-color:currentColor;
}
.intro-card-link:hover .c-cta-arrow{ transform:translateX(3px); }
/* ↗ leads away from the page, so its step is diagonal — the same distance,
   pointed where the link actually goes. */
.intro-card-link[target="_blank"]:hover .c-cta-arrow{ transform:translate(2px,-2px); }

/* ---------- scroll cue ----------
   A full-height section's cost is that it hides the fact that anything follows
   it. This is the repayment: a labelled target, not a bare chevron. "Tools"
   says what is down there, which is the difference between a hint and an
   instruction to scroll — and it is a real anchor, so it also works as a
   control for anyone who would rather click than scroll.

   It is placed at the bottom edge because that is the direction it refers to;
   the label and the arrow together point at the thing they lead to. */
.scroll-cue{
  justify-self:center;
  display:inline-flex;
  flex-direction:column;
  align-items:center;
  gap:8px;
  padding:8px 14px 6px;
  border-radius:10px;
  text-decoration:none;
  color:var(--muted);
  font-size:var(--t-micro);
  letter-spacing:0.16em;
  text-transform:uppercase;
  -webkit-tap-highlight-color:transparent;
  transition:color var(--dur-1) var(--ease-out),
             opacity var(--dur-2) var(--ease-out);
}
.scroll-cue:hover{ color:var(--text); }

.scroll-cue-arrow{
  width:18px;
  height:18px;
  display:block;
  /* A slow, small drift downward. It is not decoration: a static arrow reads as
     a label, a moving one reads as a direction. The travel is 4px — enough to
     register at the edge of vision, small enough that it never competes with
     the headline for attention.

     ~0.5Hz on purpose. A loop closer to one cycle every five seconds sits in
     the range that reads as drifting rather than pulsing and is the range
     motion-sensitive users report problems with; reduced-motion stops it
     outright via the blanket rule in motion.css. */
  animation:cue-drift 1.9s var(--ease-out) infinite;
  transition:transform var(--dur-1) var(--ease-out);
}
@keyframes cue-drift{
  0%, 100%{ transform:translateY(0); }
  50%     { transform:translateY(4px); }
}
/* On hover the drift gives way to a committed step down — the interface
   answering the pointer with the same movement it was hinting at. */
.scroll-cue:hover .scroll-cue-arrow{
  animation:none;
  transform:translateY(3px);
}

/* Once the page has moved, the cue has been obeyed and has nothing left to say;
   leaving it on screen would be the interface still asking for something it
   already got. `.is-scrolled` is set on <html> by site-header.js — the same
   state the header's scroll edge uses, so both react to one fact about the
   page rather than each watching the scroll separately.
   `visibility` follows the fade so the faded-out cue also leaves the tab order
   instead of being a focusable target sitting invisibly on the page. */
.is-scrolled .scroll-cue{
  opacity:0;
  visibility:hidden;
  transition:opacity var(--dur-2) var(--ease-in),
             visibility 0s linear var(--dur-2);
}

/* ---------- responsive ---------- */

/* Between phone and desktop the split survives — it just needs a smaller mark.
   Stacking here instead would be the wrong trade: side by side, the two
   columns share the screen's height, and stacked they add up. On a 880x700
   laptop that sum ran past the fold and took the scroll cue with it, which is
   the one thing this section cannot afford — a full-height section whose "more
   below" hint is itself below the fold is worse than no hint at all. So the
   stack is held back to the width where two columns genuinely stop fitting. */
@media (max-width: 900px){
  .intro-body{ grid-template-columns:minmax(0,1fr) minmax(0,220px); }
  .gizmo{ width:clamp(150px, 22vw, 220px); }
}

/* Below this the headline has no room to be a headline beside the mark, so the
   mark goes above the copy — it is the identity, so it stays the first thing
   seen, and it aligns to the text's left edge rather than centring, which
   keeps one vertical line down the whole section. */
@media (max-width: 720px){
  /* One column, so everything centres on one axis.
     Left-aligned text under a centred mark reads as a layout that lost an
     argument — the mark sits on the page's centre line and the copy on its
     left edge, and nothing lines up with anything. Centred, the section has a
     single spine running from the eyebrow through the figures to the scroll
     cue, which was already centred at the foot. The copy is short enough for
     this: centred text costs reading speed when there are many lines with
     ragged left edges to find, and here the longest block is three. */
  .intro-body{
    grid-template-columns:1fr;
    gap:clamp(18px, 4vw, 30px);
    justify-items:center;
  }
  .intro-copy{ text-align:center; }
  .intro-mark{ order:-1; align-self:center; }
  .gizmo{ width:clamp(110px, 26vw, 150px); }
  .intro-mark::before{ width:150%; filter:blur(28px); }

  .intro{ padding-top:20px; padding-bottom:20px; }
  .intro-title{ margin-bottom:14px; }
  /* The measure cap stays; it just centres in the column now instead of
     hanging off the left. */
  .intro-lead{ margin-inline:auto; }

  /* The row stays a row. Stacking three entries would add roughly 200px of
     height to a screen that has none to give, and push the scroll cue under
     the fold — the one thing this section cannot do. So they get narrower and
     quieter instead: a smaller figure and a step down on the two supporting
     lines, so three columns still hold real words. */
  .intro-cards{
    margin:22px auto 0;
    /* the same "+ inset" the desktop rule makes, against a smaller base */
    padding-top:calc(18px + var(--hit-y));
    gap:0 clamp(16px, 4.5vw, 22px);
    /* The columns are ~100px wide here, so the hit area gives back most of its
       horizontal reach. The gap floor is what the reach is set against: at
       10px of gap and 5px of inset either side, two neighbouring targets ended
       up 1px apart — three destinations touching each other on the screen this
       page is mostly read on, two of which leave the site. A thumb landing
       near a boundary opened whichever it happened to graze.
       16px of gap against 4px of inset leaves 8px of dead space between them,
       which costs each column three pixels of width and is worth it. */
    --hit-x:4px;
    --hit-y:8px;
  }
  .intro-card .c-pre,
  .intro-card .c-post{ font-size:var(--t-mini); line-height:1.35; }
  .intro-card .c-key{ font-size:var(--t-h4); }
  /* "See the timeline" wraps to two lines in a 100px column and costs the
     scroll cue the height it needs, so the article drops out and the line
     reads "See timeline" / "Read post" — same promise, one line. */
  .intro-card .c-cta{ margin-top:8px; font-size:var(--t-micro); }
  .intro-card .c-cta-opt{ display:none; }
}

/* ---------- when the screen is short ----------
   The section's whole bargain is: take a full screen, and pay for it with a
   cue that proves there is more below. On a short screen the bargain cannot
   be kept — the copy alone is taller than the screen, so the cue ends up
   under the fold, or under the tab bar, which is worse than not promising
   anything. Stacked, this content wants about 560px; with the header above
   it and the tab bar below, a phone needs roughly 680px of height before the
   promise is affordable.

   There are two short screens and they want opposite things, so they are two
   rules. This first one is the part they share: give up the full-height claim
   and drop the cue rather than squeeze both. */
@media (max-height: 600px){
  .intro{ min-height:0; padding-top:32px; padding-bottom:32px; }
  .scroll-cue{ display:none; }
}

/* Short because it is a phone on its side. Here the width is the resource
   there is plenty of, so the mark goes back beside the copy — stacked, it
   would push the text off the bottom on its own.
   Keyed to the orientation rather than to a width: a 320px-wide phone held
   upright is also under 600px tall, and the last thing it needs is a
   two-column layout. */
@media (max-height: 600px) and (orientation: landscape){
  .intro-body{ grid-template-columns:minmax(0,1fr) auto; align-items:center; }
  .intro-mark{ order:0; }
  .gizmo{ width:clamp(110px, 16vw, 150px); }
}

/* Tall enough to keep the bargain, but only just — the common 640px and
   667px phones, where the stack fits and the cue lands with nothing to
   spare. Everything here is a small subtraction rather than a change of
   layout: the mark loses a quarter of its diameter and each gap gives back a
   few pixels, which together is about sixty — the difference between a cue
   sitting behind the tab bar and a cue sitting clear above it. */
@media (max-width: 620px) and (min-height: 601px) and (max-height: 720px){
  .intro{ padding-top:14px; padding-bottom:14px; }
  .intro-body{ gap:14px; }
  .gizmo{ width:clamp(84px, 22vw, 110px); }
  .intro-title{ margin-bottom:10px; }
  .intro-cards{
    margin-top:16px;
    padding-top:calc(14px + var(--hit-y));
  }
  .scroll-cue{ gap:6px; padding:4px 14px; }
}
