/* ============================================================
   Components — sits on top of design-system.css
   ============================================================ */

/* noise: generated, so there is no binary asset to ship */
.noise {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
}

.section { position: relative; }
.u-hide-md { display: none; }

/* ═══════════ 1 · NAV ═══════════ */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 90;
  color: var(--c-black);
  mix-blend-mode: difference;
  filter: invert(1);
}
.nav__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 24px; padding-block: 16px;
}
.nav__logo { white-space: nowrap; display: flex; align-items: center; }
/* the wordmark replaces the old text node, so it takes its optical size from
   .h-l's 1rem rather than a fixed px height. currentColor keeps it in step
   with .nav's difference blend + invert. */
.nav__logo-mark { height: 1.15em; width: auto; display: block; }
.nav__list { display: flex; gap: clamp(22px, 4.4vw, 70px); }
.nav-link { position: relative; padding-block: 2px; }
/* underline removed — the letter swap is the hover state now */
.nav-link__underline { display: none; }
.nav-list__bg {
  position: fixed; inset: 0; background: var(--c-black);
  clip-path: inset(0 0 100% 0); transition: clip-path .6s cubic-bezier(.87,0,.13,1);
  z-index: -1;
}
.nav[data-nav-status="open"] .nav-list__bg { clip-path: inset(0 0 0 0); }

@media (max-width: 900px) {
  .u-hide-md { display: block; }
  .nav__list {
    position: fixed; inset: 0; z-index: 5;
    flex-direction: column; justify-content: center;
    padding: var(--pad); gap: 4px;
    pointer-events: none; opacity: 0;
    transition: opacity .4s ease;
  }
  .nav__list .nav-link { font-size: 2rem; font-weight: 700; letter-spacing: -.03em; }
  .nav[data-nav-status="open"] .nav__list { pointer-events: auto; opacity: 1; }
  .nav-toggle { position: relative; z-index: 6; }

  /* A `filter` makes an element the containing block for its position:fixed
     descendants. .nav carries invert(1) for the difference blend, so both the
     drawer and its backdrop were resolving `inset: 0` against the 50px header
     bar instead of the viewport — the backdrop showed as a black band across
     the top and the link column overflowed its box, leaving only the last two
     items on screen.

     The blend exists to keep the closed bar legible over whatever section sits
     behind it. Once the drawer is open its own backdrop supplies the contrast,
     so the effect has no job to do: drop it while open and the overlay lands
     against the viewport as intended. Colour then has to be stated outright,
     since it was the invert that used to make black type read on black. */
  .nav[data-nav-status="open"] {
    mix-blend-mode: normal;
    filter: none;
    color: var(--c-white);
    /* A transform traps fixed descendants the same way a filter does, and the
       hero intro tweens .nav from yPercent -200 (main.js). It clears itself
       when the tween ends, but a tap on Menu during those first seconds would
       re-trap the drawer — and GSAP writes transform inline, so only
       !important can out-rank it. */
    transform: none !important;
  }
}

/* ═══════════ 2 · HERO ═══════════ */
.hero {
  min-height: 100vh;
  min-height: 100svh;          /* avoids the mobile URL-bar jump */
  display: flex;
  flex-direction: column;
}
/* the two upper spacers absorb the leftover viewport height, so the wordmark
   centres and the now-playing row settles toward the bottom. Their design-system
   values stay as the floor, which keeps the rhythm on short screens. */
/* NB: the hero's children are all <div>, so :nth-of-type counts every div —
   the two upper spacers are div 1 and div 3, not 1 and 2. */
.hero > .spacer:nth-of-type(1) { flex: 2.7 0 var(--h); }
.hero > .spacer:nth-of-type(3) { flex: 1 0 var(--h); }

.hero__title {
  display: flex; align-items: center; justify-content: center;
  gap: .1em; flex-wrap: wrap; text-align: center;
  /* own scale: .h-d caps at 8.5rem, which leaves a 1920 canvas half empty.
     ~10vw keeps the lockup near full-bleed from 1280 up; it wraps below that. */
  font-size: clamp(2.25rem, 10vw, 13rem);
}
/* Hero intro (ported from paulkalkbrenner.net). Three layers, as there:
   .hero__window  — the inline slot that holds the layout space. NO overflow
                    hidden: the hold grows past it to 105vw and must show.
                    z-index 1 so the oversized still covers the wordmark.
   .hero__img-hold — absolutely positioned, so GSAP can size it to 105vw/105vh
                    without the <h1> reflowing. This is the animated element.
   .hero__img-el  — one per photo, stacked. */
.hero__window {
  display: inline-flex; position: relative; z-index: 1;
  width: 1.55em; height: .74em;           /* clipped to the letterform height */
  flex: none; align-self: center;
}
.hero__img-hold {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: inline-block; background: var(--c-gray);
  will-change: width, height, transform;
}
.hero__img-el { position: absolute; inset: 0; z-index: 0; }
.hero__window .img { width: 100%; height: 100%; object-fit: cover; display: block; opacity: 0; }
/* :first-child now matches every .img (one per wrapper), so qualify by wrapper */
.hero__img-el:first-child .img { opacity: 1; }
.hero__word { display: inline-block; }

/* The timeline reveals this at t=0 with the still already oversized and the
   words already off-screen (GSAP fromTo renders its "from" state immediately). */
[data-home-hero] { opacity: 0; }

.hero__bottom { align-items: end; row-gap: 24px; }
.hero__np { display: flex; align-items: center; gap: 12px; }
.hero__np-text { display: flex; flex-direction: column; gap: 2px; }

/* intro paragraph sits 2px above the shared .p-l scale */
.hero__intro { font-size: 0.9375rem; }

.equalizer { display: flex; align-items: flex-end; gap: 2px; height: 18px; flex: none; }
.equalizer i {
  width: 2px; height: 100%; background: currentColor; display: block;
  transform: scaleY(.2); transform-origin: bottom;
}

.sound-toggle { height: 1.3em; }
.sound-toggle__track {
  display: flex; flex-direction: column;
  transition: transform .45s cubic-bezier(.16,1,.3,1);
}
.sound-toggle__track > span { height: 1.3em; line-height: 1.3em; white-space: nowrap; }
.sound-toggle[aria-pressed="true"] .sound-toggle__track { transform: translateY(-1.3em); }

/* ═══════════ 3 · EXPERIENCE GRID ═══════════ */
/* No block padding at all: the spacers either side of the marquee carry the
   rhythm, so the gap above it matches the gap below. */
.experience {
  overflow: hidden; padding-block: 26px 0;   /* just enough for the rule markers */
  --exp-rule: rgba(0, 0, 0, .55);   /* one shade for every rule in the lattice */
}

/* positioned ancestor for .exp-lines — caps the lattice at the last row */
.exp-stage {
  position: relative;
  --lines: 5;      /* track count */
  --bleed: 25%;    /* overhang each side so the layer can drift without a gap */
}

/* Wider than the stage and offset left, so the layer can translate with the
   scroll without ever exposing a bare edge inside .experience's overflow clip. */
.exp-lines {
  position: absolute; top: 0; bottom: 0;
  /* Padding moved into the offset: it used to shrink the content box and make
     the rule tracks a few px narrower than the word tracks. */
  left: calc(var(--bleed) * -1 + var(--pad));
  /* 450% wide / 15 tracks = 30% of the stage per track — identical to the word
     grid's 150% / 5. Wide enough that the layer can travel the full sweep
     without its edge coming into view. */
  width: 450%;
  display: grid; grid-template-columns: repeat(var(--lines), minmax(0, 1fr));
  pointer-events: none;
  will-change: transform;
}
.exp-lines span {
  position: relative;
  border-left: 1px solid var(--exp-rule);
}
/* the 01/02/03 markers, sitting just above the lattice like a ruler */
.exp-lines span[data-label]::before {
  content: attr(data-label);
  position: absolute;
  left: 5px; top: -19px;
  font-family: 'Space Mono', ui-monospace, monospace;
  font-size: 11px; font-weight: 700; letter-spacing: .02em;
  color: var(--fg);
}
.exp-lines span:last-child { border-right: 1px solid var(--exp-rule); }

/* same inset as .exp-lines, so the words sit on the column rules.
   border-top + the row borders draw the horizontal half of the lattice. */
.exp-content {
  position: relative; z-index: 1;
  border-top: 1px solid var(--exp-rule);
}
/* .exp-row is static so its rule doesn't slide with the drift; __inner is the
   thing that translates. Words are placed by column and overflow their cell on
   purpose — tracks are minmax(0,1fr) so a long word can't stretch the grid. */
.exp-row { border-bottom: 1px solid var(--exp-rule); }
.exp-row__inner {
  /* identical box to .exp-lines, so --col lands a word exactly on a rule */
  position: relative;
  left: calc(var(--bleed) * -1 + var(--pad));
  width: calc(100% + var(--bleed) * 2);
  display: grid;
  /* Fixed track widths, not fr. Music sits on col 6, which creates an implicit
     column in that row only — with fr tracks the row then split its width six
     ways instead of five, making its tracks narrower than every other row's. */
  grid-template-columns: repeat(var(--lines), calc(100% / var(--lines)));
  grid-auto-columns: calc(100% / var(--lines));
  align-items: center;
  font-size: clamp(3rem, 11.5vw, 12rem);
  /* tight: the rules should sit close to the type, near-touching between rows */
  font-weight: 700; line-height: .88; letter-spacing: -.035em;
  will-change: transform;
}
.exp-word {
  grid-column: var(--col);        /* start at --col, span 1 — no track overlap */
  display: flex; align-items: center;
  gap: .15em;                     /* dot -> word; tighter than the word -> word gap */
  white-space: nowrap;
  /* pull back half a dot so the dot's CENTRE lands on the rule, not its left edge */
  margin-left: -.39em;
}
.exp-content__dot {
  /* ~88% of the row's 0.88em line box, leaving a small even gap top and bottom */
  width: .78em; height: .78em; border-radius: 50%;
  background: currentColor; flex: none; display: block;
}

/* ═══════════ 4 · PLATFORM DOCK ═══════════ */
.dock-w { display: flex; justify-content: center; padding-inline: var(--pad); }

.dock {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 20px;
  background: rgba(0, 0, 0, .045);
  border: 1px solid rgba(0, 0, 0, .09);
  box-shadow: 0 12px 34px -20px rgba(0, 0, 0, .5);
  backdrop-filter: blur(12px);
}

.dock__item {
  position: relative;
  display: block;
  flex: none;
  /* JS writes --s (magnification). Anchored to the bottom so items grow upward
     out of the dock rather than pushing through it, like the macOS dock. */
  --s: 1;
  width: calc(46px * var(--s));
  transition: width .18s cubic-bezier(.16, 1, .3, 1);
}
.dock[data-idle] .dock__item { transition: width .32s cubic-bezier(.16, 1, .3, 1); }

.dock__icon {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 24%;
  background: var(--c, #444);
  box-shadow: 0 6px 14px -8px rgba(0, 0, 0, .7);
}
.dock__icon svg { width: 62%; height: 62%; display: block; color: var(--a, #fff); }
.dock__icon { color: var(--a, #fff); border: 1px solid color-mix(in srgb, var(--a, #fff) 40%, transparent); }
/* Simple Icons ships the Adobe marks as complete app TILES — the rounded
   square with the letters knocked out of it — not as bare glyphs. So a tile
   fills the whole box and the two colours swap roles: the box paints the
   letter colour (--a) and the path paints the tile (--c), which is what makes
   the knockout read as the real icon. Glyph marks (Blender) keep the default
   treatment: dark --c box, accent glyph at 62%. */
.dock__icon.is--tile { background: var(--a); border: 0; overflow: hidden; }
/* full-colour art (Procreate) paints its own gradients, so it ignores --a
   and sits a little larger than a monochrome glyph */
.dock__icon.is--art svg { width: 74%; height: 74%; }
/* the tile art is 24x23.4 in a 24x24 box, so it is cropped to its real
   bounds and sliced to fill — otherwise --a leaks as a band top and bottom */
.dock__icon.is--tile svg { width: 101%; height: 101%; color: var(--c); }
/* 101% + overflow:hidden kills the subpixel hairline of --a that rounding
   leaves along an edge at some scales (the dock magnifies on hover) */

/* placeholder lettermark — swap the whole .dock__icon contents for a real asset */
.dock__mono {
  font-family: 'Inter Tight', sans-serif;
  font-weight: 700;
  font-size: calc(46px * var(--s, 1) * .40);
  letter-spacing: -.03em;
  color: var(--a, #fff);
  line-height: 1;
}

.dock__label {
  position: absolute;
  left: 50%; bottom: calc(100% + 12px);
  transform: translateX(-50%) translateY(4px);
  padding: 5px 9px;
  border-radius: 7px;
  background: var(--c-black);
  color: var(--c-white);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease, transform .2s ease;
}
.dock__item:hover .dock__label,
.dock__item:focus-visible .dock__label {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@keyframes dock-bounce {
  0%   { transform: translateY(0); }
  35%  { transform: translateY(-26%); }
  60%  { transform: translateY(0); }
  80%  { transform: translateY(-9%); }
  100% { transform: translateY(0); }
}
.dock__icon.is--bouncing { animation: dock-bounce .55s cubic-bezier(.16, 1, .3, 1); }

@media (hover: none) {
  .dock { flex-wrap: wrap; justify-content: center; }
  .dock__item { --s: 1 !important; width: 46px; }
}

/* ═══════════ 5 · PIXELATED TRANSITION ═══════════ */
.pixelated-scroll-transition {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 42vh; display: grid; pointer-events: none; z-index: 2;
}
.pixelated-scroll-transition i { display: block; transform: scale(0); will-change: transform; }

/* ═══════════ 6 · RECORDINGS ═══════════ */
.section-top { row-gap: 20px; align-items: start; }

.album-tabs { display: flex; gap: 10px; align-self: start; }
.album-tab {
  padding: 6px 12px; border: 1px solid var(--line);
  transition: background-color .3s ease, color .3s ease;
}
.album-tab.is--active { background: var(--fg); color: var(--bg); }

.album { row-gap: 32px; align-items: start; }
.album__visual, .album__info { position: relative; }

.album-visual { position: relative; opacity: 0; visibility: hidden; }
.album-visual.is--active { opacity: 1; visibility: visible; position: relative; }
.album__visual .album-visual:not(.is--active) { position: absolute; inset: 0; }
.album-visual__inset {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 38%; aspect-ratio: 1; overflow: hidden;
}

.album-info { opacity: 0; visibility: hidden; }
.album-info.is--active { opacity: 1; visibility: visible; }
.album__info .album-info:not(.is--active) { position: absolute; inset: 0; }
.album-info__meta { display: flex; gap: 40px; }
.album-info__meta dt { margin-bottom: 4px; }
.album-info__meta dd { margin: 0; }  /* UA default indents dd by 40px */
.album-info__actions { display: flex; flex-wrap: wrap; gap: 8px; }

/* Info column runs the full height of the visual beside it, with the three
   groups spread down it: title at the top, note in the middle, actions on the
   baseline. The two `margin-top: auto` items split the free space evenly. */
@media (min-width: 768px) {
  .album { align-items: stretch; }
  .album__info { display: flex; flex-direction: column; }
  .album-info { flex: 1 1 auto; }
  .album-info.is--active { display: flex; flex-direction: column; }
  .album-info__note { margin-top: auto; }
  .album-info__actions.is--duo { margin-top: auto; }
}

/* Paired call-to-action: two tall cells sharing a single hairline frame, so the
   pair reads as one block rather than two loose pills. */
.album-info__actions.is--duo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  border: 1px solid var(--fg);
}
.album-info__actions.is--duo .button {
  border: 0;
  min-height: 116px;
  padding: 20px 16px;
  text-align: center;
  letter-spacing: .06em;
}
.album-info__actions.is--duo .button + .button { border-left: 1px solid var(--fg); }

/* Standfirst above the pair: a bold lead-in, then a muted paragraph. Carries the
   card's body copy now, so it sits a step above .p-r rather than below it. */
.album-info__note {
  max-width: 42ch;
  font-size: 0.9375rem;   /* 14px -> 15px */
  line-height: 1.45;
}
.album-info__note strong { display: block; }
.album-info__note p + p { margin-top: 8px; }

@media (max-width: 767px) {
  .album-info__actions.is--duo { grid-template-columns: 1fr; }
  .album-info__actions.is--duo .button { min-height: 88px; }
  .album-info__actions.is--duo .button + .button {
    border-left: 0;
    border-top: 1px solid var(--fg);
  }
}

/* ═══════════ 7 · TOUR DATES ═══════════ */
.tour-dates__head { padding-block: 10px; border-bottom: 1px solid var(--line); }

.tour-dates__item { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); }
.tour-dates__item-bg {
  position: absolute; inset: 0; background: var(--fg);
  transform: translateY(101%); transition: transform .5s cubic-bezier(.16,1,.3,1);
}
.tour-dates__item-noise {
  transform: translateY(101%); transition: transform .5s cubic-bezier(.16,1,.3,1);
  opacity: 0;
}
.tour-dates__item:hover .tour-dates__item-bg { transform: translateY(0); }
.tour-dates__item:hover .tour-dates__item-noise { transform: translateY(0); opacity: .12; }

.tour-dates__link { position: relative; z-index: 1; padding-block: 18px; align-items: center; transition: color .35s ease; }
.tour-dates__item:hover .tour-dates__link { color: var(--bg); }
.tour-dates__tix {
  display: inline-block; font-style: normal;
  transform: translateY(120%); transition: transform .45s cubic-bezier(.16,1,.3,1);
}
.tour-dates__item:hover .tour-dates__tix { transform: translateY(0); }

/* reveal group */
[data-reveal-group] > * { opacity: 0; }

/* ═══════════ 8 · GALLERY / DROPPING STACK ═══════════ */
.dropping-stack { position: relative; aspect-ratio: 1.2 / 1; touch-action: pan-y; }
.dropping-stack__card {
  position: absolute; inset: 0; overflow: hidden;
  will-change: transform; background: var(--c-gray);
}
.dropping-stack__card .img { width: 100%; height: 100%; }

.stack-side { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; align-self: end; }
.stack-nav { display: flex; gap: 8px; margin-left: auto; }

/* attribute-driven so it works on .overlay-nr too, which is display:block */
[data-odometer-element] { display: inline-flex; overflow: hidden; line-height: 1; }
.odometer__digit { display: block; overflow: hidden; height: 1em; }
.odometer__reel { display: flex; flex-direction: column; will-change: transform; }
.odometer__reel span { height: 1em; line-height: 1em; display: block; }

/* ═══════════ 9 · VIDEO ARCHIVE ═══════════ */
.archive { row-gap: 32px; align-items: end; }
.archive__count { display: flex; align-items: baseline; }
.archive__count [data-odometer-element] { line-height: .88; }
.archive__stage { cursor: pointer; }
/* The stage isn't parallaxed, so .media-inner's -12%/124% oversize buys nothing
   here and just crops 24% of the frame away — the phone lost its bottom bar. */
.archive__stage .media-inner { inset: 0; height: 100%; }
/* the video is a true 16:9; matching the box to it means no crop at all */
.archive__stage.is--video { --ratio: 16 / 9; }
.archive__stage .img { transition: opacity .35s ease; }
.archive__stage.is--swapping .img { opacity: .2; }

/* the tour preview fades the same way the archive stage does */
#tourPreview { transition: opacity .35s ease; }
.media-w.is--swapping #tourPreview { opacity: .25; }
.archive__info { display: flex; justify-content: space-between; gap: 16px; }
.archive__info-end { display: flex; align-items: baseline; gap: 20px; }

/* .button gives it the letter-swap hover for free — but not the chrome that
   comes with it, so the border, padding and fill all come back off. */
.archive__sound {
  padding: 0; border: 0; background: none;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; opacity: .55;
  transition: opacity .3s ease;
}
.archive__sound:hover { background: none; color: inherit; opacity: 1; }
.archive__sound[aria-pressed="true"] { opacity: 1; }

.archive-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; }
.archive-strip::-webkit-scrollbar { height: 4px; }
.archive-strip::-webkit-scrollbar-thumb { background: var(--c-light-40); }
.archive-thumb {
  position: relative; flex: none; width: clamp(120px, 12vw, 180px);
  aspect-ratio: 16 / 9; overflow: hidden; opacity: .4;
  transition: opacity .3s ease;
}
.archive-thumb .img { filter: grayscale(1); }
.archive-thumb span { position: absolute; left: 6px; bottom: 6px; }
.archive-thumb:hover { opacity: .75; }
.archive-thumb.is--active { opacity: 1; outline: 1px solid var(--fg); outline-offset: -1px; }
.archive-thumb.is--active .img { filter: none; }

/* ═══════════ 10 · LIFE WORKS ═══════════ */
.lifeworks__overlay {
  position: absolute; top: 0; left: 0; right: 0;
  padding-block: clamp(24px, 5vh, 60px);
  display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap;
  color: var(--c-white); z-index: 2;
}
.lifeworks__copy { max-width: 34ch; }
.lifeworks .overlay-nr {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  text-align: center; color: var(--c-white); pointer-events: none;
}
.lifeworks [data-odometer-element] { justify-content: center; }

/* ═══════════ 11 · NEWSLETTER ═══════════ */
.marquee { overflow: hidden; }
.marquee__track { display: flex; width: max-content; will-change: transform; }
.marquee__item { flex: none; width: clamp(160px, 18vw, 280px); padding-right: 8px; }
.marquee__item .img { aspect-ratio: 10 / 7; }

.nl-form { display: flex; flex-direction: column; gap: 8px; max-width: 460px; }
.nl-form .w-input {
  padding: 14px 16px; border: 1px solid var(--line);
  background: transparent; color: inherit; font-family: inherit;
}
.nl-form .w-input::placeholder { color: var(--c-dark-40); }
.nl-form .w-input:focus { outline: none; border-color: var(--fg); }
.nl-form .button { align-self: flex-start; cursor: pointer; }

/* ═══════════ 12 · FOOTER ═══════════ */
.footer { position: relative; }
.footer-overlay {
  position: absolute; inset: 0; background: var(--c-black);
  pointer-events: none; opacity: 0; z-index: 3;
}
.footer__col { display: flex; flex-direction: column; gap: 6px; }
.footer__bar { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.footer__spin { display: block; width: 24px; height: 24px; display: grid; place-items: center; }
.footer__spin .square { width: 14px; height: 14px; }

/* ═══════════ cursor ═══════════ */
/* An always-on filled dot replaces the native pointer. .cursor carries
   mix-blend-mode: difference, so the fill inverts whatever sits beneath it —
   growing the dot over a link or a line of type is what flips that type's
   colour. Three sizes: resting, over anything interactive, and the play label. */
.cursor { opacity: 0; transition: opacity .25s ease; }
.cursor.is--ready { opacity: 1; }
.cursor-bubble { width: 12px; height: 12px; transition: width .3s ease, height .3s ease; }
.cursor.is--hover .cursor-bubble { width: 44px; height: 44px; }
.cursor.is--active .cursor-bubble { width: 76px; height: 76px; }
.cursor-bubble span { opacity: 0; transition: opacity .2s ease; }
.cursor.is--active .cursor-bubble span { opacity: 1; }

/* The dot IS the pointer now, so suppress the native one. !important because
   .button / .archive__stage / .nl-form .button each set cursor: pointer and
   out-specify a plain descendant selector. Text fields keep their caret. */
/* .has-dot is set by initCursor itself, so if the script fails to load or the
   device has no fine pointer the native cursor is never taken away. */
@media (hover: hover) and (pointer: fine) {
  .has-dot body, .has-dot body * { cursor: none !important; }
  .has-dot input, .has-dot textarea, .has-dot select { cursor: auto !important; }
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal-group] > * { opacity: 1 !important; }
  .exp-row { transform: none !important; }
}

/* ═══════════ letter swap (hover) ═══════════
   Port of the RandomLetterSwap idea to vanilla CSS/JS: each character gets a
   stacked duplicate; on hover the original rolls out and the copy rolls in,
   staggered in a shuffled order so the swap reads as random. */
.ls { display: inline-flex; position: relative; }
.ls__sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.ls__char { position: relative; display: inline-flex; white-space: pre; overflow: hidden; }
.ls__a, .ls__b {
  display: block;
  transition: transform .55s cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--d, 0s);
}
.ls__b { position: absolute; left: 0; top: 0; transform: translateY(-100%); }

[data-ls-host]:hover .ls__a,
[data-ls-host]:focus-visible .ls__a { transform: translateY(100%); }
[data-ls-host]:hover .ls__b,
[data-ls-host]:focus-visible .ls__b { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .ls__a, .ls__b { transition: none; }
  [data-ls-host]:hover .ls__a { transform: none; }
  [data-ls-host]:hover .ls__b { transform: translateY(-100%); }
}

a:hover, a:focus-visible { text-decoration: none; }

/* ── about page ───────────────────────────────────────────────────────── */
/* The page runs down the centre rather than on the 8-column grid: there is
   one voice here and nothing to set it against, so the measure does the
   framing instead of the columns. */
.about-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.about-portrait { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 14px; }
/* Scales with the viewport like the title beneath it, rather than sitting at
   one fixed size. Capped at 160px: the source is 400px square, so this still
   has 2.5x density to spend on a retina screen. */
.about-portrait img {
  width: clamp(120px, 9vw, 160px);
  height: clamp(120px, 9vw, 160px);
  border-radius: 50%;
  object-fit: cover;
}

/* Smaller than the home page's display size: this line is a statement of
   what the practice is, not the site's front door, and at full .h-d scale it
   filled the viewport on its own. */
.about-title {
  max-width: 24ch;   /* wide enough that each written line stays one line */
  margin-inline: auto;
  font-size: clamp(2rem, 4.6vw, 4.25rem);
  line-height: 1.05;
  letter-spacing: -.03em;
}

/* Prose sits in a single centred column at a comfortable measure, left
   aligned so the ragged edge doesn't fight the centred display type above. */
.about-prose {
  max-width: 62ch;
  margin-inline: auto;
}
.about-prose > * + * { margin-top: 18px; }

.about-links { display: flex; flex-wrap: wrap; gap: 24px; }

/* Experience runs as ruled rows in the same language as the hero meta and the
   tour dates: role and dates on one line, the org beneath. No cards, no logos
   — the page states everything else with a hairline and a caps label. */
.xp-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  counter-reset: xp;
}
.xp {
  padding: 20px 0;
  border-top: 1px solid var(--line);
}
.xp:last-child { border-bottom: 1px solid var(--line); }

.xp__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
}
.xp__role { margin: 0; font-weight: 700; }
.xp__when { flex: none; white-space: nowrap; }
.xp__org { margin: 6px 0 0; color: var(--c-dark-40); }
.xp__note { margin: 4px 0 0; color: var(--c-dark-40); }

@media (max-width: 600px) {
  .xp__head { flex-direction: column; gap: 4px; }
}
