/* ============================================================
   site.css — layout and components. Colours and faces come
   from tokens.css; nothing here hard-codes a hex.
   ============================================================ */

*{box-sizing:border-box;}

html{
  scroll-padding-block:6rem;   /* 2.4.11 — a card tabbed to never lands jammed against an edge */
}

body{
  margin:0;
  background:var(--ground);
  color:var(--ink);
  font-family:var(--ui);
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

a{color:var(--accent);}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:3px;border-radius:2px;}

.label{
  font-family:var(--label);
  text-transform:uppercase;
  letter-spacing:.28em;
  font-size:.78rem;
  color:var(--muted);
}

/* --- debug chrome: only with ?debug on the URL --------------- */
[data-debug-only]{display:none;}
html.debug [data-debug-only]{display:flex;}

/* ============================================================
   HERO
   The stage reserves the scroll distance. The layer is fixed and
   driven by js/hero.js; it is not laid out by CSS.
   ============================================================ */

.stage{position:relative;}

.stage-frame{
  position:fixed;top:0;left:0;right:0;height:100vh;
  z-index:3;
  flex-direction:column;justify-content:space-between;
  padding:var(--step);
  pointer-events:none;
  transition:opacity .3s ease;
}

.hero-layer{
  position:fixed;top:0;left:0;width:100%;
  display:flex;justify-content:flex-start;
  pointer-events:none;
  z-index:1;
  will-change:transform;
}

.wordmark{
  font-family:var(--display);
  line-height:.78;
  letter-spacing:-.01em;
  white-space:nowrap;
  font-size:100px;              /* replaced at runtime by hero.js fit() */
  color:var(--n1);
  transform-origin:0 50%;       /* scale from the left edge so gutters hold */
  will-change:transform,color;
}

.roles{
  position:absolute;top:0;left:0;
  font-family:var(--body);
  color:var(--ink);
  line-height:1.14;
  text-align:right;
  white-space:nowrap;
  will-change:transform;
}
.role{will-change:transform,opacity;}

/* zero-size baseline probes — they ride the line box and cost no layout */
.bl{display:inline-block;width:0;height:0;vertical-align:baseline;}

.cue{
  position:fixed;left:50%;bottom:1.6rem;transform:translateX(-50%);
  z-index:3;display:flex;align-items:center;gap:.6rem;
  pointer-events:none;
  transition:opacity .4s ease;
}
.cue .tick{width:1px;height:26px;background:var(--rule);}

.readout{
  position:fixed;top:var(--step);right:var(--step);
  z-index:3;
  align-items:center;gap:.55rem;
  padding:.4rem .6rem .4rem .45rem;
  border:1px solid var(--rule);border-radius:2px;
  background:color-mix(in srgb,var(--ground) 82%,transparent);
  backdrop-filter:blur(6px);
  font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;
  font-variant-numeric:tabular-nums;
  color:var(--muted);
  pointer-events:none;
  transition:opacity .4s ease;
}
.readout .dot{width:14px;height:14px;border-radius:1px;background:var(--n1);}
.readout b{font-weight:500;color:var(--ink);}
.readout .aa{padding:.1rem .3rem;border:1px solid currentColor;border-radius:1px;font-size:.62rem;}

/* ============================================================
   SHEETS — every section after the hero scrolls over the layer
   ============================================================ */

.sheet{position:relative;z-index:2;background:var(--ground);}
.wrap{max-width:64rem;margin:0 auto;padding:calc(var(--step)*2) var(--step);}

h2{
  font-family:var(--display-alt);
  font-weight:400;font-size:clamp(2rem,6vw,3.4rem);
  line-height:.92;margin:0;text-wrap:balance;letter-spacing:-.01em;
}
p{margin:0;line-height:1.65;color:var(--muted);font-size:1rem;max-width:65ch;}

.placeholder{display:flex;flex-direction:column;gap:1rem;}

/* --- footer -----------------------------------------------
   Grounded in the green the wordmark opens on, so the page closes where it
   started. --focus-ring is overridden here: the site's black ring is 1.50:1 on
   this ground and would simply vanish.
   ---------------------------------------------------------- */
footer{
  background:var(--footer-ground);
  color:var(--footer-ink);
  --focus-ring:var(--footer-ink);
}
footer{
  position:relative;
  overflow:hidden;                     /* clips the oversized ghost wordmark */
  /* The bird is absolute, so it no longer sets the footer's height the way it did as
     a flex item — without a floor the band collapses to the links and the bird
     shrinks with it. This is what gives the bird room to be big. */
  min-height:clamp(160px,18vw,230px);
  /* content rides the top of the band; the ghost and the bird are anchored to the
     footer itself, so neither moves with it */
  display:flex;align-items:flex-start;
}
/* Purely decorative: the surname set oversized and run off the bottom edge, so it
   reads as ground rather than as a line of copy. Sized to span the band and cropped
   by footer{overflow:hidden} — the ascenders and x-height stay, which is enough to
   read it. aria-hidden, and no contrast bar applies: it is texture, not text. */
.footer-ghost{
  /* Anchored to the footer, not the wrap, so moving the content does not drag it.
     The left offset reproduces the wrap's own content edge — max-width 96rem, centred,
     plus its side padding — so it stays on the links' edge at any width. */
  position:absolute;
  left:max(var(--step), calc((100% - 96rem)/2 + var(--step)));
  bottom:-.26em;
  font-family:var(--display);
  font-size:clamp(3.5rem,17vw,16rem);
  line-height:.78;letter-spacing:.05em;
  color:color-mix(in srgb,var(--footer-ink) var(--footer-ghost),transparent);
  pointer-events:none;user-select:none;white-space:nowrap;
}
footer .wrap{
  position:relative;                          /* the real content rides above the ghost */
  width:100%;
  padding-top:var(--step);padding-bottom:var(--step);
  display:flex;flex-wrap:wrap;gap:clamp(1.2rem,3vw,2.4rem);
  justify-content:space-between;align-items:flex-end;
}
.footer-main{display:flex;flex-direction:column;gap:clamp(.6rem,1.3vw,.9rem);}
.footer-links{
  display:flex;flex-wrap:wrap;gap:clamp(1rem,2.6vw,2.2rem);
  font-family:var(--body-text);
  font-size:clamp(1.05rem,1.5vw,1.25rem);
  letter-spacing:.022em;
}
.footer-links a{
  color:var(--footer-ink);
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-underline-offset:.28em;
  text-decoration-color:color-mix(in srgb,var(--footer-ink) 45%,transparent);
  transition:text-decoration-color 180ms ease;
}
.footer-links a:hover,.footer-links a:focus-visible{text-decoration-color:currentColor;}
.footer-copy{
  margin:0;
  font-family:var(--display);
  font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;
  color:color-mix(in srgb,var(--footer-ink) 72%,transparent);
}
.footer-bird{
  position:absolute;right:var(--step);bottom:0;
  /* Full band height, so its centre is the footer's centre and the links line up on
     it. Safe because the artwork carries 45px of transparent margin above the head
     and only 6px below the feet — the head clears the top edge on its own. */
  height:100%;width:auto;
  display:block;pointer-events:none;
}
@media (forced-colors:active){
  footer{background:Canvas;color:CanvasText;}
}

@media (prefers-reduced-motion:reduce){
  .cue,.readout,.stage-frame{transition:none;}
}

/* ============================================================
   SECTION TWO — the statement
   ============================================================ */

.wrap--wide{max-width:96rem;}
.wrap--tight-top{padding-top:calc(var(--step)*0.75);}
.wrap--tight-bottom{padding-bottom:0;}   /* the 60px gap is set on .work-pin */

.statement{
  position:relative;                 /* the marker canvas anchors to this */
  font-family:var(--statement);
  color:var(--ink);
  font-size:clamp(1.3rem,2.55vw,2.35rem);
  line-height:1.24;
  max-width:88%;                     /* runs most of the wrap's width; % of the container
                                         (not vw) so it stays proportional at every screen
                                         size instead of drifting relative to .wrap--wide's
                                         96rem cap the way a raw vw figure does */
  text-wrap:pretty;                  /* asks supporting browsers to balance the last line
                                         rather than leave a lone short word behind */
}

@media (max-width:44rem){
  .statement{max-width:100%;}       /* the two-thirds measure stops helping on phones */
}

.statement-ink{position:relative;z-index:1;}

.statement .name{
  font-family:var(--statement-strong);
}

.mark-canvas{
  position:absolute;inset:0;
  width:100%;height:100%;
  z-index:0;
  pointer-events:none;
}

/* ============================================================
   SECTION THREE — the work banner

   A three-line pattern wipes in from the left, then the middle
   stripe expands into a menu carrying a ticker of project names.
   The stage reserves scroll room; the pin holds the banner still
   while the choreography plays.
   ============================================================ */

.work-stage{
  position:relative;
  height:85vh;                     /* the sweep finishes at .82 of this, so the
                                      hold after the banner opens is ~15vh. Was
                                      145vh, which held for a third of a page.
                                      Shares in work.js are scaled to match. */
  padding-top:80px;                /* the banner arrives 80px under the bio */
}
.work-pin{
  position:sticky;
  top:200px;                       /* ...rides up with the page, then docks
                                      here, leaving the heading room above.
                                      No overflow:hidden — it would clip the
                                      heading, which sits outside this box. */
}

.stripes{position:relative;width:100%;}

.stripes-clip{
  /* the wipe: JS drives the inset from 100% to 0 */
  clip-path:inset(0 100% 0 0);
  will-change:clip-path;
}

.stripe{width:100%;}
.stripe--top{height:13px;background:var(--stripe-top);}
.stripe--bot{height:13px;background:var(--stripe-bot);}
.stripe--mid{
  background:var(--stripe-mid);
  height:14px;                     /* closed; JS expands it */
  display:flex;align-items:center;
  overflow:hidden;
  will-change:height;
}

.ticker{width:100%;overflow:hidden;opacity:0;will-change:opacity;}
.ticker-track{
  display:flex;
  align-items:center;
  gap:100px;                       /* the words sit 100px apart */
  width:max-content;
  will-change:transform;
}
.ticker-star{
  display:flex;align-items:center;
  color:var(--stripe-text);
  flex:0 0 auto;
}
.ticker-star .star{
  width:calc(var(--tick-size) * 1.18);
  height:calc(var(--tick-size) * 1.18);
  display:block;
  fill:currentColor;
}

.ticker-item{
  font-family:var(--display-alt);  /* Harry Fat */
  color:var(--stripe-text);
  font-size:var(--tick-size);
  line-height:1;
  white-space:nowrap;
  letter-spacing:.035em;           /* Harry Fat is tight; moving text needs air */
}

.work-head{
  position:absolute;
  left:50%;
  bottom:100%;                     /* JS sets margin-bottom so the baseline
                                      lands on the stripe, glyphs just touching */
  transform:translateX(-50%);
  margin-top:0;
  font-family:var(--display-alt);
  font-weight:400;
  /* one Fibonacci step above the ticker, capped so it cannot
     outgrow a narrow screen (the heading does not wrap) */
  font-size:min(calc(var(--tick-size) * var(--fib-step)),8.5vw);
  line-height:1;
  letter-spacing:-.01em;
  white-space:nowrap;
  color:var(--stripe-top);         /* matches the stripe it rests on */
  opacity:0;                       /* JS reveals it as the bar opens */
  will-change:opacity,transform;
}

@media (prefers-reduced-motion:reduce){
  .stripes-clip{will-change:auto;}
}

/* ============================================================
   SECTION FOUR — the work timeline

   Three columns: an info card, a rail that fills as you scroll,
   and a staggered field of preview cards. Cards reveal on entry
   via IntersectionObserver; only the rail is scroll-driven.
   ============================================================ */

.timeline{margin-top:28px;}          /* sits close under the banner */

.tl-group{
  display:grid;
  grid-template-columns:minmax(0,0.9fr) var(--rail-col) minmax(0,2.3fr);
  /* +20px on each side of the rail: it needed clearing off the info cards, and the
     previews needed the same air on the other side. The clamp still carries the
     responsive base; the 20px is the deliberate breathing room on top of it. */
  column-gap:var(--tl-gap);
  align-items:start;
  padding-bottom:clamp(2rem,5vw,4.5rem);
}

/* --- the rail --------------------------------------------- */
.tl-rail{
  position:relative;
  align-self:stretch;
  display:flex;flex-direction:column;align-items:center;
  gap:.6rem;
}
.tl-icon{
  flex:0 0 auto;
  width:clamp(20px,2.3vw,26px);height:clamp(20px,2.3vw,26px);
  color:var(--rail);
}
.tl-icon svg{width:100%;height:100%;display:block;fill:currentColor;}
.tl-line{
  position:relative;flex:1 1 auto;width:2px;
  background:color-mix(in srgb,var(--rail) 22%,transparent);
  border-radius:1px;
}
.tl-fill{
  position:absolute;left:0;top:0;width:100%;height:0;
  background:var(--rail);border-radius:1px;
  will-change:height;
}

/* --- info cards (left) ------------------------------------
   Deep grounds rather than the pale tints: they hold their own
   beside the white preview cards, and the cream body copy clears
   AA comfortably (6.2-6.6:1) instead of scraping the bar.

   Body copy is Knockout 30, the narrowest width in the family. Size
   and leading are pushed up to give the condensed letterforms as much
   room as they can get.
   ---------------------------------------------------------- */
.info-card{
  background:var(--card-bg);
  color:var(--card-ink);
  border-radius:clamp(16px,1.9vw,26px);
  padding:clamp(1.5rem,2.6vw,2.2rem) clamp(1.2rem,2.2vw,1.8rem) clamp(1.6rem,2.9vw,2.3rem);
}
.info-card h3{
  margin:0 0 .5em;
  font-family:var(--display-alt);      /* Harry Fat */
  font-weight:400;
  font-size:clamp(1.9rem,3.7vw,3rem);
  line-height:.88;
  letter-spacing:0;                    /* matches the site's other display type */
  color:var(--card-title);
}
.info-card h3::after{
  content:"";
  display:block;
  width:2.1em;height:2px;
  margin-top:.42em;
  background:currentColor;
  opacity:.5;
  border-radius:1px;
}
.info-card p{
  margin:0;
  color:var(--card-ink);
  font-family:var(--body-text);
  font-size:clamp(1.05rem,1.55vw,1.28rem);
  line-height:1.55;
  letter-spacing:.022em;               /* condensed type needs the tracking */
  max-width:30ch;
}

/* --- preview cards (right) -------------------------------- */
.tl-previews{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(.9rem,1.8vw,1.5rem);
  align-content:start;
}
.preview-card{
  grid-column:var(--col,1);
  margin-left:var(--shift,0);
  display:flex;align-items:center;gap:.5rem;
  min-height:clamp(96px,11vw,150px);
  padding:0 0 0 clamp(.9rem,1.8vw,1.5rem);
  background:var(--card-ground);
  border:3px solid var(--edge);
  border-radius:clamp(16px,1.8vw,26px);
  overflow:hidden;
}
/* --- case-study stubs -------------------------------------
   Each preview card needs a real destination with a matching <h1>, so a
   screen-reader user hearing "linked data, link" arrives somewhere named
   "linked data". Content follows; the structure is what matters now.
   ---------------------------------------------------------- */
.case-title{
  font-family:var(--display-alt);
  font-weight:400;
  font-size:clamp(2.4rem,7vw,5rem);
  line-height:.95;letter-spacing:0;
  margin:.4em 0 .2em;
  color:var(--ink);
}
.case-meta{
  font-family:var(--display);
  text-transform:lowercase;
  letter-spacing:.16em;
  color:var(--muted);
  margin:0 0 2rem;
}

/* --- cards as links ---------------------------------------
   The whole card is one native <a>, so keyboard operation, Enter,
   the pointer cursor and no-JS behaviour all come for free. Hover and
   focus-visible share one rule: a keyboard user should get exactly what a
   mouse user gets, plus a ring on top to say where focus actually is.
   ---------------------------------------------------------- */
a.preview-card{
  text-decoration:none;
  color:inherit;
  box-shadow:var(--shadow-card);
  transition:color 180ms ease, border-color 180ms ease,
             box-shadow 180ms ease, text-decoration-color 180ms ease;
}
a.preview-card:hover,
a.preview-card:focus-visible{
  --edge-strong:color-mix(in srgb, var(--edge) 78%, var(--shade));
  border-color:var(--edge-strong);
  box-shadow:var(--shadow-card-lift);
}
a.preview-card:hover h4,
a.preview-card:focus-visible h4{
  color:var(--edge-strong);
  text-decoration:underline;
  text-decoration-color:currentColor;
  text-decoration-thickness:.055em;
  text-underline-offset:.14em;   /* skip-ink stays on, so the y in "analytical" is not struck */
}
a.preview-card:focus-visible{
  outline:3px solid var(--focus-ring);
  outline-offset:3px;
}
@media (prefers-reduced-motion:reduce){
  a.preview-card{transition-duration:1ms;}   /* the state still changes, it just stops animating */
}
@media (forced-colors:active){
  a.preview-card:focus-visible{outline-color:Highlight;}
  a.preview-card{border-color:CanvasText;}
}

.preview-card h4{
  margin:0;flex:0 0 auto;max-width:8ch;
  font-family:var(--display-alt);
  font-weight:400;
  font-size:clamp(1rem,1.9vw,1.7rem);
  line-height:.95;letter-spacing:0;
  color:var(--edge);
}
.preview-card .shot{
  flex:1 1 auto;align-self:stretch;
  position:relative;overflow:var(--shot-clip,hidden);
  display:flex;align-items:var(--shot-align,center);justify-content:flex-end;
}
.preview-card .shot img{
  width:var(--shot-w,112%);max-width:none;display:block;
  transform:rotate(var(--shot-rot,-2.5deg)) translate(var(--shot-x,6%),var(--shot-y,0%));
  transform-origin:center;
  /* Most exports carry the comp's shadow baked into their alpha. analytical methods
     does not — its image fills its Figma frame exactly, so the frame clipped the
     shadow away on export. --shot-shadow adds it back for those cards only; setting
     it on one that already has a baked shadow would double it. */
  filter:var(--shot-shadow,none);
}
/* shown only when the screenshot has not been dropped in yet */
.preview-card .shot.missing::after{
  content:"screenshot";
  position:absolute;inset:12% 8% 12% 0;
  border:2px dashed color-mix(in srgb,var(--edge) 35%,transparent);
  border-radius:10px;
  display:grid;place-items:center;
  font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;
  color:color-mix(in srgb,var(--edge) 55%,transparent);
}
.preview-card .shot.missing img{display:none;}

/* --- wide preview card ------------------------------------
   Breaks out of the two-column grid, but is sized by proportion
   rather than filling the zone — full width read far too large.
   Cards in this variant all match, and stagger with --shift.
   ---------------------------------------------------------- */
.preview-card--wide{
  grid-column:1 / -1;
  width:clamp(17rem,64%,32rem);
  aspect-ratio:3.45;               /* holds the comp's proportion at any width */
  min-height:0;
  margin-left:var(--shift,0);
  border-width:4px;
  padding-left:clamp(1.2rem,2.4vw,2.1rem);
  gap:clamp(.7rem,1.5vw,1.2rem);
}
.preview-card--wide h4{
  /* Long names ("analytical methods") would otherwise eat 79% of the card and leave
     the shot no room. Cap the measure so they wrap instead; the short ones are well
     under it and are unaffected. */
  max-width:56%;
  font-size:clamp(2rem,4.3vw,3.8rem);   /* comp sets the heading at 43% of the card */
  /* align-items:center centres the LINE BOX, not the ink. At line-height .95 with
     no descenders in "linked data" / "bulk actions", the box carries empty descent
     that pushes the glyphs high by .117em. Nudge down so the ink reads centred. */
  transform:translateY(.117em);
}
/* The comp lets the cluster run off the BOTTOM edge and keeps air above it and
   to its right — it is not centred in the slot. Centring clips the author name
   bars away, which are the part that says what the work is. */
/* Every comp frames its shot differently — linked data tucks a portrait cluster
   top-right with a gutter, bulk actions runs a landscape screenshot off the right
   and bottom. So the framing is four knobs with defaults, overridden per card on the
   style attribute alongside --edge / --shift. --shot-clip:visible lets an image run
   wider than its slot; the card's own overflow:hidden still trims it to the rounded
   border, which is the only edge the comps cut against.
   These are the wide cards' defaults; the narrow ones keep the base values above. */
.preview-card--wide{
  --shot-align:flex-start;
  --shot-w:77%;
  --shot-x:-26%;
  --shot-y:5%;
}
/* --- the appearing animation ------------------------------ */
.reveal{
  opacity:0;transform:translateY(18px);
  transition:opacity .6s cubic-bezier(.22,.61,.36,1),
             transform .6s cubic-bezier(.22,.61,.36,1);
  transition-delay:var(--delay,0ms);
  will-change:opacity,transform;
}
.reveal.is-in{opacity:1;transform:none;}

@media (prefers-reduced-motion:reduce){
  .reveal{opacity:1;transform:none;transition:none;}
}

@media (max-width:52rem){
  .tl-group{grid-template-columns:var(--rail-col-m) minmax(0,1fr);}
  .tl-group .info-card{grid-column:2;margin-bottom:1rem;}
  .tl-rail{grid-column:1;grid-row:1 / span 2;}
  .tl-previews{grid-column:2;grid-template-columns:minmax(0,1fr);}
  .preview-card{grid-column:1;margin-left:0;}
  /* .preview-card--wide's own width is clamp(17rem,64%,32rem) — sized for a
     desktop card sitting in a wide column. At mobile widths 64% of the
     (much narrower) column is well under the 17rem floor, so the floor
     wins and stays fixed regardless of viewport, while .info-card (which
     has no width of its own, just its grid column) keeps growing with the
     screen. The two drift apart — 10px at 390px, 50px by 430px. Both
     should just fill the shared content column. */
  .preview-card--wide{width:100%;}
  /* the desktop clamp() floors (heading, padding, leading) still read as
     desktop-sized on a phone-width card; shrink them together here */
  .info-card{padding:20px 20px 24px;}
  .info-card h3{font-size:1.5rem;margin:0 0 .35em;}
  .info-card h3::after{margin-top:.3em;}
  .info-card p{max-width:none;font-size:.95rem;line-height:1.35;}
}

/* --- about me ---------------------------------------------
   The orange bar here is the craft media rail carrying on downward — same
   colour, same x. About borrows the timeline's grid so that alignment is
   structural: the rule sits on the rail's centre line and both words start
   on the rule's own left edge, so all three share one gridline at every width.

   Scroll-scrubbed, but the FINISHED state is the default: --about-progress is 1
   in CSS, so with no JS, blocked JS, or before the script runs, the heading is
   already in place. about.js only ever lowers it. The transforms are further
   gated on .js-scroll-active so nothing can strand text off-canvas.
   ---------------------------------------------------------- */
#about{overflow-x:clip;padding-bottom:var(--about-runway);}
/* The rule is the craft media rail carrying on, so the stacked paddings between the
   two are collapsed — otherwise 200px of ground opens up and it reads as a second,
   unrelated bar rather than the same one continuing. */
#timeline .wrap{padding-bottom:0;}
#timeline .tl-group:last-of-type{padding-bottom:0;}
#about .wrap{padding-top:0;}  /* clip, not hidden: hidden would make a scroll container */
.about{
  --about-progress:1;
  display:grid;
  grid-template-columns:minmax(0,0.9fr) var(--rail-col) minmax(0,2.3fr);
  column-gap:var(--tl-gap);
}
.about-photos{
  grid-column:1;
  align-self:start;
  display:flex;flex-direction:column;
  gap:clamp(.9rem,2vw,1.6rem);
  /* Start the stack level with "about", not with the section: the rule reserves its
     full layout height whatever it has drawn, so that height plus its gap is exactly
     how far down the heading begins. */
  padding-top:calc(var(--about-rule-h) + var(--about-rule-gap) + var(--polaroid-nudge));
}
.polaroid{
  background:var(--panel);
  padding:var(--polaroid-pad) var(--polaroid-pad) var(--polaroid-chin);
  box-shadow:var(--polaroid-shadow);
  width:100%;
  margin:0;
  /* a tilt each way, so the pair reads as dropped on a desk rather than laid out on a grid */
  transform:rotate(var(--tilt,-3deg));
}
.polaroid:nth-child(even){--tilt:2.5deg;}
/* Desktop only: the upper frame sits right, the lower one keeps the column's left
   edge, so the pair reads as two prints dropped down rather than a column. Narrower
   than this there is no room to spend and they go back to a plain stack. */
@media (min-width:60rem){
  .polaroid{width:var(--polaroid-w);}
  .about-photos .polaroid:nth-child(odd){margin-left:var(--polaroid-stagger);}
}
.polaroid img{
  display:block;width:100%;
  aspect-ratio:1;object-fit:cover;   /* one crop for both, whatever each photo's native shape */
  background:color-mix(in srgb,var(--ink) 8%,transparent);  /* an empty frame still reads as a frame */
}
@media (prefers-reduced-motion:reduce){
  .polaroid{transform:none;}
}

.about-col{
  grid-column:2 / -1;

  /* half the rail column, less half the rule, lands the rule on the rail's centre */
  padding-left:calc(var(--rail-col)/2 - var(--about-rule-w)/2);
}
.about-rule{
  display:block;
  width:var(--about-rule-w);
  height:var(--about-rule-h);
  margin-bottom:var(--about-rule-gap);
  background:var(--about-rule-ink);
  transform-origin:top center;
}
.about-head{
  margin:0;
  font-family:var(--display-alt);
  font-weight:400;
  /* floor matches .preview-card--wide h4's own floor (2rem) — "linked data",
     "bulk actions" etc. — so "about" reads at the same size on a phone */
  font-size:clamp(2rem,4.6vw,3.2rem);
  line-height:.92;
  letter-spacing:0;
}
/* Decorative, so it is a pseudo-element — an <hr> would carry separator semantics
   this does not deserve. Same ink as the bio it introduces. */
.about-head::after{
  content:"";
  display:block;
  width:2.6em;height:2px;
  margin:.42em 0 0;
  background:var(--about-bio-ink);
  border-radius:1px;
}
.about-bio{
  margin:0;
  max-width:52ch;
  font-family:var(--body-text);        /* Knockout 30, as the info cards use */
  font-size:clamp(1.15rem,1.68vw,1.4rem);
  line-height:1.38;
  letter-spacing:.022em;               /* condensed type needs the tracking */
  color:var(--about-bio-ink);
  /* keeps a lone word off the last line at any width — better than a hard-coded
     &nbsp;, which only fixes the one viewport it was measured at */
  text-wrap:pretty;
}
.about-bio + .about-bio{margin-top:clamp(.7rem,1.5vw,1.1rem);}
/* The row starts at the paragraphs, not at the heading, so aligning the print to
   flex-start puts its top on the first paragraph's top by construction — no offset
   to recompute when the heading size or the rule length changes. The gap under the
   heading lives here rather than on the first paragraph, which would otherwise push
   the text down and leave the print aligned to a margin edge instead of to type. */
.about-body{
  display:flex;align-items:flex-start;
  gap:clamp(1.5rem,4vw,4rem);
  margin-top:clamp(1rem,2.2vw,1.6rem);
}
.about-copy{flex:1 1 auto;min-width:0;}
.polaroid--side{
  flex:0 0 auto;
  width:var(--polaroid-w);
  --tilt:3.5deg;
  /* the tilt lifts the frame's corner above its own box; pull it back onto the
     paragraph's first line, same correction the pair gets against "about" */
  margin-top:var(--polaroid-nudge);
}
@media (max-width:60rem){
  /* no room to sit beside the copy — fall in under it */
  .about-body{display:block;}
  /* below the desktop stagger, .polaroid's base rule is still width:100% — on a
     narrow screen that's the full column, ballooning the pair well past the size
     of .polaroid--side just below. One size for all three, same as the side print.
     % of its own column, capped at 260px — not vw: between 600px and 60rem #about
     is still a 3-column grid, and .about-photos' 0.9fr track can be well under
     260px there (it's ~179px around 800px wide), so a flat vw figure overflowed
     the track into the rail/copy column. % always respects the actual column, and
     still lands on the 260px cap once .about goes single-column below 600px and
     .about-photos and .about-copy converge on the same full-measure width. */
  .polaroid{width:min(100%,260px);}
  .polaroid--side{margin-top:clamp(1.2rem,4vw,2rem);}
}

.about-word{display:block;width:max-content;}
.about-word--a{color:var(--about-word-a);}
.about-word--b{color:var(--about-word-b);}

/* No transition on these: a transition on a property rewritten every frame
   fights the scroll and the words trail the cursor. */
.js-scroll-active .about-rule{
  transform:scaleY(calc(var(--about-rule-min) + (1 - var(--about-rule-min)) * var(--about-progress)));
}
.js-scroll-active .about-word--a{
  transform:translate3d(calc((1 - var(--about-progress)) * var(--about-travel-a,0px) * -1),0,0);
}
.js-scroll-active .about-word--b{
  transform:translate3d(calc((1 - var(--about-progress)) * var(--about-travel-b,0px)),0,0);
}

@media (max-width:52rem){
  /* Below this, #about switches from the desktop 3-column grid (photos / rail /
     copy, side by side) to the timeline's own 2-column mobile pattern (rail /
     everything else) — the same breakpoint .tl-group already collapses at, so
     the rail never has a stretch where one section is mid-transition and the
     other isn't.
     .about-rule lives inside .about-col in the markup, but the rule needs to
     sit in its own column next to .about-photos, not trapped in a box that
     only starts after the photos. display:contents removes .about-col's own
     box and promotes its children (.about-rule, .about-head, .about-body) to
     be direct grid items of .about, placeable on their own. */
  .about{
    display:grid;
    grid-template-columns:var(--rail-col-m) minmax(0,1fr);
    column-gap:var(--tl-gap);
    row-gap:clamp(1rem,3vw,1.6rem);
  }
  /* #timeline and #about collapse their padding to zero at the wrap level (see
     below) so the rail has no seam to cross. That's correct for the rail, but
     it also means the photos, which sit right next to it, land flush against
     the last preview card with nothing to separate them. Space it back out
     here instead of at the section boundary: .about-rule spans this row via
     grid-row:1/3 with an auto height, so growing this row also grows the
     rule to fill it — the line simply runs a bit further before the photos
     start, rather than a gap opening up in it. */
  .about-photos{grid-column:2;grid-row:1;padding-top:clamp(3rem,8vw,4rem);}
  .about-col{display:contents;}
  .about-rule{
    grid-column:1;
    grid-row:1 / 3;              /* spans the photos row and the heading row */
    justify-self:center;         /* centres the 2px bar in the rail column, same x as .tl-line */
    height:auto;                 /* an explicit height blocks a grid item's default
                                     stretch — auto lets it fill exactly the two rows
                                     it spans, so it always matches the photos' real
                                     height instead of a guessed pixel figure */
    margin-bottom:0;             /* would otherwise shrink the stretch short of row-line 3 */
  }
  .about-head{grid-column:2;grid-row:2;}
  .about-body{grid-column:2;grid-row:3;}
  /* Two full-width squares read as one long scroll of photo before any text
     shows, and at full width they sat on top of the rail above. Shrunk to fit
     the column right of the rail, overlapped rather than side by side: plain
     width/margin, nothing for flex-shrink to get wrong on a real phone. */
  .about-photos .polaroid{width:62%;}
  .about-photos .polaroid:nth-child(odd){margin-left:26%;}
  .about-photos .polaroid:nth-child(even){margin-top:-30%;position:relative;z-index:1;}
}
@media (max-width:600px){
  .js-scroll-active .about-word--a,
  .js-scroll-active .about-word--b{transform:none;}
}
@media (prefers-reduced-motion:reduce){
  .about-rule,.about-word{transform:none !important;}
}
@media (forced-colors:active){
  .about-rule{background:CanvasText;forced-color-adjust:none;}
}

/* the skip link's target takes focus programmatically; it needs no ring of its own */
main:focus{outline:none;}
