/* ==========================================================================
   site.css — chrome, sections and components.
   Tokens, reset and primitives live in base.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Backdrop
   --------------------------------------------------------------------------
   Three layers, fixed behind everything:
     1. .backdrop__glow  — CSS-only gradient wash that follows --tint.
                           This alone is the full fallback when WebGL is out.
     2. .backdrop__canvas — the three.js particle field, faded in by the
                            scene module once it has actually rendered a frame.
     3. .backdrop__grain — static film grain to kill gradient banding.
   -------------------------------------------------------------------------- */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.backdrop__glow {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(46% 42% at 22% 18%, color-mix(in oklab, var(--tint) 22%, transparent) 0%, transparent 70%),
    radial-gradient(40% 45% at 82% 72%, color-mix(in oklab, var(--plasma) 20%, transparent) 0%, transparent 72%),
    radial-gradient(70% 60% at 50% 120%, color-mix(in oklab, var(--tint) 10%, transparent) 0%, transparent 70%);
  filter: blur(30px) saturate(120%);
  opacity: 0.55;
  transition: opacity 0.8s var(--ease-out);
}

.backdrop__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;                       /* raised by js/scene once the first frame is up */
  transition: opacity 1.2s var(--ease-out);
}
.backdrop__canvas.is-live { opacity: 1; }

/* With a live particle field the CSS wash steps back so it doesn't wash it out. */
html.scene-live .backdrop__glow { opacity: 0.32; }

.backdrop__grain {
  position: absolute;
  inset: 0;
  opacity: 0.05;
  mix-blend-mode: overlay;
  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='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Everything above the backdrop. */
.masthead, main, .footer, .toast, .progress { position: relative; z-index: 1; }

/* --------------------------------------------------------------------------
   Scroll progress
   -------------------------------------------------------------------------- */
.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 40;
  background: var(--line);
}
.progress__bar {
  display: block;
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--acid), var(--plasma));
}

/* --------------------------------------------------------------------------
   Scrollbar
   --------------------------------------------------------------------------
   A short bar in the middle of the right edge rather than a full-height rail:
   a fixed length of travel, so it looks the same on a laptop and on a 4K
   monitor, and it stays out of the way of the masthead and the footer instead
   of running behind both. The native one is removed in base.css.

   It sleeps by default and js/modules/scrollbar.js wakes it while the page is
   moving. Only the opacity animates — the thumb's position is a transform
   written per frame, and nothing here may fight it for that property.
   -------------------------------------------------------------------------- */
.scrollbar {
  position: fixed;
  z-index: 45;
  top: 50%;
  right: clamp(9px, 1vw, 16px);
  width: 4px;
  height: clamp(112px, 17vh, 190px);
  transform: translateY(-50%);       /* centred without repeating the clamp */
  border-radius: 999px;
  background: var(--line);           /* the track */
  cursor: pointer;
  touch-action: none;                /* a drag on the bar is not a page scroll */
  opacity: 0;
  transition: opacity 0.5s var(--ease-out), width 0.25s var(--ease-out);
}
.scrollbar[hidden] { display: none; }

.scrollbar.is-awake,
.scrollbar:hover,
.scrollbar.is-dragging { opacity: 1; }

/* Nothing to scroll: no bar, and no hover state to bring one back. */
.scrollbar.is-inert { opacity: 0; pointer-events: none; }

/* Fattens under the pointer, from the centre line out, so the thumb stays put
   while the target gets easier to hit. */
.scrollbar:hover,
.scrollbar.is-dragging { width: 6px; }

.scrollbar__thumb {
  display: block;
  width: 100%;
  border-radius: inherit;
  background: var(--ink);
  /* height and translate3d are written by the module */
}
.scrollbar.is-dragging .scrollbar__thumb { background: var(--tint); }

/* Touch already has its own inertia and no pointer to grab with, so the bar
   stays as a read-out there and never eats a swipe. */
@media (hover: none) {
  .scrollbar { pointer-events: none; }
}

/* --------------------------------------------------------------------------
   Masthead
   -------------------------------------------------------------------------- */
.masthead {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: clamp(0.9rem, 1.6vw, 1.4rem) var(--gutter);
  /* Fade the page out behind the bar rather than using a hard background. */
  background: linear-gradient(to bottom, color-mix(in oklab, var(--bg) 88%, transparent), transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  mask-image: linear-gradient(to bottom, #000 55%, transparent);
}

.masthead__brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
/* The apple mark. Stroke and fill are the same colour, so the anime.js
   draw-on reads as the outline of the fruit rather than as an outline *around*
   it, and the fill arriving afterwards looks like it is being poured in. */
.monogram { width: 27px; height: 27px; overflow: visible; }
.monogram__body { fill: var(--apple); stroke: var(--apple); }
.monogram__stem { stroke: var(--bark); }
.monogram__leaf { fill: var(--leaf); }
.masthead__brand:hover .monogram { animation: apple-nudge 0.7s var(--ease-out); }
@keyframes apple-nudge {
  30% { transform: rotate(-7deg) scale(1.06); }
  65% { transform: rotate(4deg)  scale(1.02); }
}

.masthead__wordmark { font-size: 0.98rem; }

.masthead__nav {
  display: flex;
  /* Five tabs since `background` joined them, so the low end of the gap is
     tighter than it was — five at the old 1rem floor collided with the
     language switch before the breakpoint below could catch it. */
  gap: clamp(0.7rem, 1.8vw, 2rem);
  font-family: var(--font-mono);
  font-size: var(--step-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.masthead__nav a {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  padding-block: 0.25rem;
  transition: color 0.3s var(--ease-out);
}
.masthead__nav a:hover,
.masthead__nav a.is-current { color: var(--ink); }
.masthead__nav .idx { color: var(--ink-faint); font-size: 0.85em; }
.masthead__nav a.is-current .idx { color: var(--tint); }

/* Below this the bar can't hold five items without them colliding. The
   sections are one scroll apart anyway, and the skip link still jumps. */
@media (max-width: 820px) {
  .masthead__nav { display: none; }
}

/* Language switch ----------------------------------------------------------
   Two buttons and a chip that slides between them. The chip is a separate
   element rather than a background on the pressed button so it can *travel*:
   a background swap is a cut, and this is the one control on the page that
   should look like it moved.

   Which side it sits on comes from html[data-lang], written by
   js/modules/lang.js — the control is never told its own state, and the whole
   thing survives being toggled from anywhere else. */
.lang {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 3px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: color-mix(in oklab, var(--bg-raise) 70%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* Ships hidden — see the note in index.html. `display: inline-flex` above
   would otherwise outrank the user-agent's [hidden] rule. */
.lang[hidden] { display: none; }

.lang__thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc(50% - 3px);
  border-radius: 999px;
  background: var(--tint);
  pointer-events: none;
  transition: transform 0.5s var(--ease-out), background-color 0.4s var(--ease-out);
}
html[data-lang='pt'] .lang__thumb { transform: translateX(100%); }

.lang__btn {
  position: relative;                /* over the chip */
  z-index: 1;
  padding: 0.3rem 0.55rem;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.14em;
  color: var(--ink-dim);
  cursor: pointer;
  transition: color 0.35s var(--ease-out);
}
.lang__btn:hover { color: var(--ink); }
/* Against the chip, and the chip is the accent — so this is the one place on
   the page where type goes dark. */
.lang__btn[aria-pressed='true'],
.lang__btn[aria-pressed='true']:hover { color: #0a0a0c; }

.masthead__end { display: flex; align-items: center; gap: clamp(0.5rem, 1vw, 0.75rem); }

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.72rem 1.1rem;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  font-size: 0.92rem;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.35s var(--ease-out), color 0.35s var(--ease-out),
              border-color 0.35s var(--ease-out), transform 0.35s var(--ease-out);
}
.btn .icon { width: 17px; height: 17px; }
.btn:hover { border-color: var(--tint); }

.btn--ghost:hover { background: color-mix(in oklab, var(--tint) 12%, transparent); }

.btn--solid {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
  font-weight: 600;
}
.btn--solid:hover { background: var(--tint); border-color: var(--tint); color: #0a0a0c; }

.masthead__gh { padding: 0.6rem 0.95rem; }
/* The label comes off the screen and stays in the accessibility tree — the
   same trap the apple tags fell into. Both icons in this link are aria-hidden,
   so `display: none` on the one piece of text left it as a focusable link with
   no accessible name at all on every phone. */
@media (max-width: 720px) {
  .masthead__gh span {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .masthead__gh { padding: 0.6rem; }
  .masthead__gh .icon--sm { display: none; }
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */
.hero {
  min-height: 100svh;
  display: flex;
  align-items: center;
  /* Enough to clear the fixed masthead, and no more — the tree needs the
     height far more than the top of the page needs the air. */
  padding: calc(var(--gutter) * 0.9 + 2.6rem) var(--gutter) clamp(1.25rem, 3vh, 2.5rem);
}
.hero__inner { width: 100%; max-width: var(--measure); margin-inline: auto; }

/* Tree on the left, everything you can read on the right. The tree column is
   given slightly more room because it is the thing being looked at. */
.hero__stage {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.5rem, 3vw, 3.5rem);
  margin-top: clamp(0.5rem, 2vh, 1.5rem);
}
.hero__copy { display: grid; justify-items: start; gap: clamp(1rem, 2.2vh, 1.75rem); }

/* Cue on the left, metadata on the right, both under the stage. */
.hero__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem clamp(1rem, 3vw, 3rem);
  margin-top: clamp(1.25rem, 3vh, 2.25rem);
}

.hero__kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.7rem;
  font-family: var(--font-mono);
  font-size: var(--step-label);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* The wordmark. Archivo's width axis is pushed wide so it reads as a poster
   rather than as body copy scaled up. It shares the fold with the tree now,
   so it is sized against its own column instead of the whole viewport. */
.hero__name {
  font-size: clamp(2.75rem, 7.9vw, 9rem);
  font-weight: 800;
  font-variation-settings: 'wdth' 118;
  line-height: 0.76;
  letter-spacing: -0.055em;
  margin-left: -0.055em;                 /* optical alignment of the 'R' stem */
  color: var(--ink);
}

.hero__lede {
  max-width: 42ch;
  color: var(--ink-dim);
  text-wrap: pretty;
  padding-top: clamp(0.75rem, 1.5vh, 1.25rem);
  border-top: 1px solid var(--line);
}
/* Emphasis on the sentence, not on whatever happens to fit the first line. */
.hero__lede-lead { color: var(--ink); }

/* The two things a visitor might want to do, said in verbs, directly under the
   sentence that says who I am — above the fold, above the tree, and nowhere
   near the bottom of the page. Everything actionable used to live in the
   footer behind a handle ("rvdi", "@MacieiraPT"), which is a label, not an
   invitation: it names a thing instead of offering one. */
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.hero__cue {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: var(--step-label);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.hero__cue-line {
  width: 54px;
  height: 1px;
  background: linear-gradient(90deg, var(--tint), transparent);
  transform-origin: 0 50%;
  animation: cue 2.8s var(--ease-inout) infinite;
}
@keyframes cue {
  0%, 100% { transform: scaleX(0.4); opacity: 0.5; }
  50%      { transform: scaleX(1); opacity: 1; }
}

/* --------------------------------------------------------------------------
   The orchard — the interactive half of the hero
   --------------------------------------------------------------------------
   Two layers in one box: an <svg> that draws the tree, and a <ul> of real
   buttons positioned on top of it. The SVG's aspect ratio is pinned to its
   viewBox so the percentage coordinates on each apple land on the same branch
   tip at every width — the whole reason the fruit isn't drawn inside the SVG.
   -------------------------------------------------------------------------- */
.orchard {
  position: relative;
  /* width + aspect-ratio, never height + aspect-ratio: the apples are placed
     as percentages of this box, so it has to match the viewBox exactly or
     every one of them drifts off its branch. The viewport-height term is
     therefore expressed as a *max-width* — 460/520 of the height budget —
     which keeps the whole tree on screen without touching the ratio. */
  width: 100%;
  max-width: min(820px, 68svh);
  margin-inline: auto;
  aspect-ratio: 460 / 520;
}
.orchard__art,
.orchard__canvas,
.orchard__glow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.orchard__art { transition: opacity 0.7s var(--ease-out); }

.orchard__wood {
  stroke: color-mix(in oklab, var(--bark) 72%, transparent);
}

/* The light around the tree. It belongs to neither tree: the SVG and the
   canvas trade places underneath it, and it carries the composition through
   the handover — and on its own, when there is no WebGL at all. */
/* Centred and symmetrical, because the tree in front of it now turns. An
   earlier version had offset blobs matched to a fixed three-quarter view; the
   moment the camera could orbit, they stopped lining up with anything. */
.orchard__glow {
  pointer-events: none;
  background:
    radial-gradient(46% 34% at 50% 42%, color-mix(in oklab, var(--leaf) 16%, transparent), transparent 72%),
    radial-gradient(30% 8% at 50% 78%, color-mix(in oklab, var(--leaf) 10%, transparent), transparent 80%);
}

.orchard__canvas {
  opacity: 0;
  transition: opacity 0.9s var(--ease-out);
}
.orchard__canvas.is-live { opacity: 1; }

.orchard__apples {
  position: absolute;
  inset: 0;
  margin: 0;
  transition: opacity 0.5s var(--ease-out);
}

/* There used to be an `.is-pending` state here that hid the SVG tree and the
   fruit from the first paint, on the theory that the flat drawing is a
   fallback rather than a loading state and should never be shown on the way to
   the real one. That reasoning was right about what the SVG *is* and wrong
   about what the visitor sees: Three.js is 137 kB gzipped, and until it lands
   the front page's main object was an empty glow. A blank hole with no
   explanation is a loading screen with the indicator removed — the worst half
   of the pattern.
   Now the SVG tree draws immediately and `.is-3d` cross-fades it out when the
   renderer has a frame to show (the transition on .orchard__art below). Slow
   connection: a complete tree, then a better one. No connection to the chunk
   at all: a complete tree, forever, which is what it was always for. */

/* The <li> holds the position, the <button> holds the motion. Keeping the
   centring translate off the button is what lets GSAP own `scale` and
   `rotate` on it without having to re-declare the offset every frame. */
.orchard__apple {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%);
}

.apple {
  --apple-size: clamp(30px, 5.4vw, 46px);
  display: grid;
  justify-items: center;
  gap: 0.45rem;
  padding: 0;
  cursor: pointer;
  /* Fruit hangs from a branch: rotation reads right around the stem. */
  transform-origin: 50% 8%;
  touch-action: manipulation;
}
.apple:focus-visible { outline: none; }

.apple__fruit {
  position: relative;
  display: block;
  width: var(--apple-size);
  height: var(--apple-size);
  transition: filter 0.35s var(--ease-out);
  filter: drop-shadow(0 6px 14px color-mix(in oklab, var(--apple-deep) 55%, transparent));
}
.apple__fruit svg { width: 100%; height: 100%; overflow: visible; }

/* Hit target, padded out without inflating the artwork.
   The padding is normally 9px, but grows when the fruit itself is small so
   the target never drops below 44px across. That matters most in 3D: the
   apple's size there is whatever the projection makes it, which on a phone is
   about 25px — a perfectly good apple and a hopeless button. */
.apple__fruit::after {
  content: '';
  position: absolute;
  inset: calc(-1 * max(9px, (44px - var(--apple-size)) / 2));
  border-radius: 50%;
}

/* Focus ring drawn on the fruit rather than the button box, so it traces the
   apple instead of the tag underneath it. */
.apple:focus-visible .apple__fruit::after {
  outline: 2px solid var(--leaf);
  outline-offset: 2px;
}

.apple__tag {
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: color-mix(in oklab, var(--bg) 82%, transparent);
  color: var(--ink-dim);
  white-space: nowrap;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: 0.58rem;
  /* Opacity, not display/visibility: the label stays in the accessibility
     tree, so the button always has a name even while the tag is invisible. */
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.apple:hover .apple__tag,
.apple:focus-visible .apple__tag,
.apple[aria-expanded='true'] .apple__tag {
  opacity: 1;
  transform: none;
}

.apple:hover .apple__fruit,
.apple:focus-visible .apple__fruit {
  filter: drop-shadow(0 8px 22px color-mix(in oklab, var(--apple) 65%, transparent));
}

/* The picked apple. A ring rather than a scale, because scale is GSAP's
   property here and CSS must not fight it for the transform. */
.apple[aria-expanded='true'] .apple__fruit::after {
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--leaf) 55%, transparent);
}
.apple[aria-expanded='true'] .apple__tag {
  border-color: color-mix(in oklab, var(--leaf) 45%, transparent);
  color: var(--ink);
}

/* Below this the tags collide with each other and with the fruit, so they come
   off the screen — but `display: none` took them out of the accessibility tree
   with it, and the tag is the *only* text in the button. Seven buttons whose
   accessible name was the empty string, on every phone. The fruit is
   aria-hidden and there is no aria-label to fall back on, so a screen reader
   announced seven identical "button"s and a keyboard user tabbed through seven
   unlabelled stops.
   Clipping instead of hiding keeps every name exactly where it was. */
@media (max-width: 620px) {
  .apple__tag {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
    /* The reveal transition above animates opacity and transform; neither can
       bring a 1 px clipped box back, but both would still run on hover. */
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   3D mode
   --------------------------------------------------------------------------
   Set by js/modules/tree.js the frame js/scene/orchard.js first draws. The
   buttons stop being placed by the percentages in the markup and start being
   moved by the renderer; the flat artwork goes with them, leaving each button
   as a transparent hit area with a label under it.

   Nothing here is a fallback for anything: without this class the page is the
   SVG tree, complete and interactive, which is what a browser with no WebGL
   keeps forever.
   -------------------------------------------------------------------------- */
.orchard.is-3d .orchard__art { opacity: 0; }

.orchard.is-3d .orchard__apple {
  /* Projected pixels, applied as a transform so moving seven buttons every
     frame costs compositing rather than layout. */
  left: 0;
  top: 0;
  transform: translate3d(var(--px, 0px), var(--py, 0px), 0) translate(-50%, -50%);
  /* --reveal is 1 when the apple faces the camera and 0 once it has turned
     behind the trunk. js/modules/tree.js drops pointer-events with it, so a
     fruit you can't see is never a hit target you can't see either. */
  opacity: var(--reveal, 1);
}

/* The tree turns under the pointer. Grabbing is on the whole box, not just the
   canvas, so a drag that starts over an apple turns the tree rather than
   dead-ending on a button (the click that would follow is swallowed). */
.orchard.is-3d {
  cursor: grab;
  /* pan-y hands vertical gestures to the page and keeps horizontal ones, so a
     phone can still scroll past a tree that fills most of the screen. Pitch is
     mouse-only as a result, which is the right trade. */
  touch-action: pan-y;
}
.orchard.is-3d.is-turning { cursor: grabbing; }

/* --apple-size is written to .orchard on resize, from the fruit's projected
   diameter, so the hit area matches what is on screen. */
.orchard.is-3d .apple { --apple-size: var(--apple-3d, 44px); position: relative; }

/* The mesh draws the apple; the DOM keeps the hit area and the focus ring. */
.orchard.is-3d .apple__fruit { filter: none; }
.orchard.is-3d .apple__fruit > svg { visibility: hidden; }

/* Taking the tag out of flow leaves the fruit as the button's only in-flow
   child, so the centring transform above lands the *fruit* on the projected
   point rather than the fruit-plus-label box. */
.orchard.is-3d .apple__tag {
  position: absolute;
  top: calc(100% + 0.45rem);
  left: 50%;
  transform: translate(-50%, -4px);
}
.orchard.is-3d .apple:hover .apple__tag,
.orchard.is-3d .apple:focus-visible .apple__tag,
.orchard.is-3d .apple[aria-expanded='true'] .apple__tag {
  transform: translate(-50%, 0);
}

/* --------------------------------------------------------------------------
   The basket — what a picked apple says
   -------------------------------------------------------------------------- */
.basket {
  width: 100%;
  padding: clamp(1.1rem, 2vw, 1.6rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--bg-raise) 72%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* A standing hint under the card, not an empty state above it. */
.basket__prompt {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.basket__prompt[hidden] { display: none; }
.basket__prompt-mark { width: 24px; height: 24px; flex: none; }
.basket__prompt-mark svg { width: 100%; height: 100%; }

/* With JS the deck holds one card at a time and the height would jump on
   every pick; a floor keeps the column still, and centring stops the one-line
   invitation from sitting in the top corner of an otherwise empty panel.
   Without JS the seven facts stack and neither rule ever comes into play. */
html.js .basket__deck {
  display: grid;
  align-content: center;
  min-height: 9.5rem;
}

/* `display: grid` on a class outranks the user-agent's `[hidden]` rule, so
   the attribute has to be honoured explicitly or js/modules/tree.js would set
   `hidden` on cards that stay perfectly visible. */
.fact { display: grid; justify-items: start; gap: 0.6rem; }
.fact[hidden] { display: none; }
/* No JS: the deck is a list, so the cards need separating from each other. */
.fact + .fact {
  margin-top: 1.4rem;
  padding-top: 1.4rem;
  border-top: 1px solid var(--line);
}
html.js .fact + .fact { margin: 0; padding: 0; border-top: 0; }

.fact__label { color: var(--tint); letter-spacing: 0.2em; text-transform: uppercase; }
.fact__title {
  font-size: clamp(1.2rem, 1rem + 1vw, 1.85rem);
  font-weight: 600;
  font-variation-settings: 'wdth' 96;
  line-height: 1.08;
  letter-spacing: -0.025em;
  text-wrap: balance;
}
.fact__body { color: var(--ink-dim); font-size: 0.98rem; max-width: 42ch; text-wrap: pretty; }
.fact__action { margin-top: 0.35rem; padding: 0.6rem 0.95rem; font-size: 0.86rem; }

/* One column: name, then the tree, then what the tree has to say — so a pick
   never pushes the apple you just tapped off the screen.
   `display: contents` dissolves the copy wrapper into the stage grid, which
   is what lets the basket sort *below* the tree while the markup (and so the
   tab order) still reads name → apples → facts. */
@media (max-width: 980px) {
  .hero__stage { grid-template-columns: 1fr; gap: clamp(1.25rem, 3vh, 2rem); }
  .hero__copy { display: contents; }
  .hero__name { order: 1; font-size: clamp(2.5rem, 17.5vw, 11rem); }
  .hero__lede { order: 1; }
  /* Same order as the name and the lede, so source order keeps the three of
     them together: the buttons must not end up below the tree on the one
     layout where the tree is tallest. */
  .hero__actions { order: 1; }
  .orchard { order: 2; max-width: min(560px, 42svh); }
  .basket { order: 3; }
}

/* --------------------------------------------------------------------------
   Sections
   -------------------------------------------------------------------------- */
.section {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--section-y) var(--gutter);
}
.section__head { margin-bottom: clamp(2.5rem, 6vh, 5rem); }
.section__head .label { margin-bottom: 1.25rem; }

/* Cards -------------------------------------------------------------------- */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: clamp(0.75rem, 1.4vw, 1.25rem);
}
.card {
  padding: clamp(1.4rem, 2.2vw, 2rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--bg-raise) 70%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
/* No hover state, deliberately. These three are statements, not links: there
   is nothing behind them to open. A card that lifts under the pointer is a
   card that says "click me", and the only thing worse than a weak link is a
   thing that behaves like a link and isn't one. Hover belongs to .link-row,
   .btn, .repo and the real work cards below — everything you can actually
   press. */
.card__label {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--tint);
  margin-bottom: 1rem;
}
.card__body { color: var(--ink-dim); font-size: 1rem; text-wrap: pretty; }
.card--accent { border-color: color-mix(in oklab, var(--tint) 35%, transparent); }

/* The background section reuses .card rather than growing a parallel set of
   boxes; these three are the only pieces it needs on top. */
.card__title {
  font-size: var(--step-h3);
  font-weight: 600;
  font-variation-settings: 'wdth' 96;
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin-bottom: 0.7rem;
  text-wrap: balance;
}
.card__action { margin-top: 1.1rem; padding: 0.6rem 0.95rem; font-size: 0.86rem; }
.card__meta {
  margin-top: 0.9rem;
  font-family: var(--font-mono);
  font-size: var(--step-label);
  color: var(--ink-faint);
  letter-spacing: 0.02em;
}

/* Marquee ------------------------------------------------------------------
   Full-bleed strip whose position and skew are driven by Lenis' scroll
   velocity in js/modules/reveals.js. */
.marquee {
  overflow: hidden;
  border-block: 1px solid var(--line);
  padding-block: clamp(0.6rem, 1.4vh, 1.1rem);
  user-select: none;
}
.marquee__track {
  display: flex;
  align-items: center;
  gap: 2.5rem;
  width: max-content;
  will-change: transform;
  font-size: clamp(1.75rem, 5.5vw, 4.5rem);
  font-weight: 700;
  font-variation-settings: 'wdth' 88;
  letter-spacing: -0.02em;
  line-height: 1;
  text-transform: lowercase;
  color: transparent;
  /* Its own value rather than --line-strong, which is a *border* token at 0.20
     and reads as 1.7:1 against the page — fine for a hairline, too faint for
     letterforms this size. 0.36 clears the 3:1 that large text owes and the
     strip still reads as an outline rather than as type. */
  -webkit-text-stroke: 1px rgb(255 255 255 / 0.36);
}
.marquee__dot {
  -webkit-text-stroke: 0;
  color: var(--tint);
  font-size: 0.35em;
}

/* --------------------------------------------------------------------------
   Link rows
   -------------------------------------------------------------------------- */
.links { border-top: 1px solid var(--line); }
.link-row { border-bottom: 1px solid var(--line); }

.link-row__main {
  display: grid;
  grid-template-columns: 3.5rem 1.75rem minmax(0, 1fr) auto 2.5rem;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.75rem);
  width: 100%;
  padding: clamp(1.1rem, 2.4vw, 1.9rem) clamp(0.5rem, 1.5vw, 1.25rem);
  text-align: left;
  cursor: pointer;
  position: relative;
  isolation: isolate;
  transition: padding-left 0.45s var(--ease-out), color 0.45s var(--ease-out);
}
/* Accent wash that wipes in from the left on hover. */
.link-row__main::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(90deg, color-mix(in oklab, var(--tint) 14%, transparent), transparent 65%);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.5s var(--ease-out);
}
.link-row__main:hover::before,
.link-row__main:focus-visible::before { transform: scaleX(1); }
.link-row__main:hover { padding-left: clamp(1rem, 2.5vw, 2rem); }

.link-row__idx { color: var(--ink-faint); }
.link-row__icon { width: 22px; height: 22px; color: var(--ink-dim); transition: color 0.4s var(--ease-out); }
.link-row__main:hover .link-row__icon { color: var(--tint); }

.link-row__name {
  font-size: clamp(1.5rem, 3.6vw, 2.9rem);
  font-weight: 600;
  font-variation-settings: 'wdth' 92;
  letter-spacing: -0.03em;
  line-height: 1.05;
}
.link-row__handle { color: var(--ink-dim); }

.link-row__go {
  width: 22px; height: 22px;
  justify-self: end;
  color: var(--ink-faint);
  transition: color 0.4s var(--ease-out), transform 0.4s var(--ease-out);
}
.link-row__main:hover .link-row__go { color: var(--tint); transform: translate(3px, -3px); }
.link-row__main:hover .link-row__go--copy { transform: translateY(-2px); }

/* Row briefly confirms a copy in place, on top of the toast. */
.link-row__main.is-copied .link-row__handle { color: var(--tint); }

@media (max-width: 780px) {
  .link-row__main { grid-template-columns: 1.75rem minmax(0, 1fr) 1.5rem; row-gap: 0.35rem; }
  .link-row__idx { display: none; }
  .link-row__handle { grid-column: 2; font-size: 0.78rem; }
  .link-row__go { width: 18px; height: 18px; }
}

/* --------------------------------------------------------------------------
   Seam (play → build) — the anime.js corner
   -------------------------------------------------------------------------- */
.seam {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 1.25rem;
  padding: clamp(3rem, 9vh, 7rem) var(--gutter);
  color: var(--line-strong);
}
.seam__line { width: 100%; height: 40px; }

.seam__toggle {
  display: inline-flex;
  align-items: center;
  gap: 1rem;
  padding: 0.75rem 1.25rem 0.75rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: color-mix(in oklab, var(--bg-raise) 80%, transparent);
  cursor: pointer;
  transition: border-color 0.4s var(--ease-out), background-color 0.4s var(--ease-out);
}
.seam__toggle:hover { border-color: var(--tint); }

.seam__glyph { width: 30px; height: 30px; fill: var(--tint); transition: fill 0.6s var(--ease-out); }
/* Set by js/modules/details.js when the morph runs, rather than read from
   aria-pressed. This stopped being a toggle: it was a <button> whose entire
   function was to change the shape of its own 30 px icon — the most
   button-shaped thing on the page, mid-scroll, promising an action and
   delivering a redraw. It is now the link to the source of the page you are
   on, which is what the apple → GitHub morph was always illustrating. */
.seam__toggle.is-gh .seam__glyph { fill: var(--plasma); }
.seam__toggle .icon--sm { color: var(--ink-faint); transition: transform 0.4s var(--ease-out); }
.seam__toggle:hover .icon--sm { transform: translate(3px, -3px); }

.seam__caption { display: grid; justify-items: start; gap: 0.2rem; }
.seam__state {
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.seam__hint { color: var(--ink-faint); letter-spacing: 0.14em; text-transform: uppercase; font-size: 0.6rem; }

/* --------------------------------------------------------------------------
   Dev section
   -------------------------------------------------------------------------- */
.dev {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}
.dev__rail-inner { display: grid; gap: 1.5rem; justify-items: start; }
.dev__rail-inner .label { margin: 0; }
.dev__body { display: grid; gap: clamp(2rem, 4vw, 3.5rem); }

@media (max-width: 980px) {
  .dev { grid-template-columns: 1fr; }
}

/* GitHub panel ------------------------------------------------------------- */
.gh {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--bg-raise) 78%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  overflow: hidden;
  /* The dev half of the page leans plasma, so the panel opts out of --tint. */
  --tint: var(--plasma);
}
.gh > * + * { border-top: 1px solid var(--line); }
.gh__head, .gh__activity, .gh__repos { padding: clamp(1.1rem, 2vw, 1.5rem); }

.gh__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.gh__id { display: flex; align-items: center; gap: 0.9rem; }
.gh__avatar {
  width: 46px; height: 46px;
  border-radius: 50%;
  flex: none;
  background:
    linear-gradient(135deg, color-mix(in oklab, var(--plasma) 40%, transparent), color-mix(in oklab, var(--acid) 25%, transparent));
  border: 1px solid var(--line-strong);
  background-size: cover;
  background-position: center;
}
.gh__title { font-size: var(--step-h3); font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
.gh__sub { color: var(--ink-faint); }

.gh__live { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--ink-dim); }
.gh[data-gh-state='error'] .gh__live { color: var(--ink-faint); }
.gh[data-gh-state='error'] .dot { background: var(--ink-faint); animation: none; }
.gh[data-gh-state='live'] .gh__live { color: var(--tint); }

.gh__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gh__stats > div { padding: clamp(1rem, 2vw, 1.4rem); }
.gh__stats > div + div { border-left: 1px solid var(--line); }
.gh__stats dt {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.5rem;
}
.gh__stats dd {
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  font-weight: 700;
  font-variation-settings: 'wdth' 92;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.gh__activity-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}

/* Activity bars, one per day. Height is set inline from the API data. */
.spark {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 56px;
}
.spark__bar {
  flex: 1;
  min-height: 2px;
  height: 8%;                       /* grown to the real value by github.js */
  border-radius: 2px;
  background: color-mix(in oklab, var(--tint) 70%, transparent);
  transform-origin: 50% 100%;
  transition: height 0.7s var(--ease-out);
}
.spark__bar[data-empty='true'] { background: var(--line); }

.gh__repos { display: grid; gap: 0.15rem; }
.gh__empty { color: var(--ink-dim); font-size: 0.95rem; }

.repo {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.5rem 1rem;
  padding: 0.85rem 0.6rem;
  margin-inline: -0.6rem;
  border-radius: 8px;
  transition: background-color 0.3s var(--ease-out);
}
.repo:hover { background: color-mix(in oklab, var(--tint) 10%, transparent); }
.repo__name { font-weight: 600; letter-spacing: -0.01em; }
.repo__name::after { content: ' ↗'; color: var(--ink-faint); font-size: 0.85em; }
.repo__meta { display: flex; align-items: center; gap: 0.9rem; color: var(--ink-faint); }
.repo__desc { grid-column: 1 / -1; color: var(--ink-dim); font-size: 0.92rem; margin-top: -0.2rem; }
.repo__lang { display: inline-flex; align-items: center; gap: 0.4rem; }
.repo__lang::before {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--lang-color, var(--tint));
}

.gh__note {
  padding: 0 clamp(1.1rem, 2vw, 1.5rem) clamp(1.1rem, 2vw, 1.5rem);
  color: var(--ink-faint);
}
.gh__note:empty { display: none; }

/* Selected work ------------------------------------------------------------ */
.work { display: grid; gap: 1.25rem; }
.work__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 0.75rem;
}
.work-card {
  display: grid;
  align-content: start;
  gap: 0.6rem;
  min-height: 150px;
  padding: 1.1rem;
  border-radius: var(--radius);
  border: 1px dashed var(--line);
}
.work-card--ghost { color: var(--ink-faint); }
.work-card__slot { color: var(--ink-faint); letter-spacing: 0.16em; text-transform: uppercase; font-size: 0.6rem; }
.work-card__text { font-size: 0.95rem; }
/* Real entries from js/data/projects.js get a solid border. */
.work-card--real {
  border-style: solid;
  color: inherit;
  transition: border-color 0.35s var(--ease-out), transform 0.35s var(--ease-out);
}
/* The hover is on the anchor, not on the class. js/modules/work.js only builds
   an <a> when the entry has a `url` — an entry without one stays an <article>,
   and giving *that* a lift would be the .card mistake one file down: a card
   that rises under the pointer and has nowhere to go. Focus gets the same
   treatment so the keyboard sees what the mouse sees. */
a.work-card--real:hover,
a.work-card--real:focus-visible { border-color: var(--plasma); transform: translateY(-3px); }
.work-card__title { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; }
.work-card__tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: auto; }
.work-card__tag {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink-dim);
}

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */
.footer {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--section-y) var(--gutter) clamp(2rem, 5vh, 3rem);
}
.footer__top { display: grid; gap: 1.5rem; justify-items: start; }
.footer__big {
  font-size: clamp(1.6rem, 4.5vw, 3.6rem);
  font-weight: 600;
  font-variation-settings: 'wdth' 96;
  line-height: 1.05;
  letter-spacing: -0.03em;
  max-width: 20ch;
  text-wrap: balance;
}
.footer__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.5rem; }

.footer__email { color: var(--ink-dim); letter-spacing: 0.04em; }
.footer__email a { border-bottom: 1px solid var(--line-strong); }
.footer__email a:hover { color: var(--ink); border-color: var(--tint); }

/* Stickers ----------------------------------------------------------------- */
.stickers {
  position: relative;
  height: clamp(160px, 22vh, 230px);
  margin-top: clamp(2.5rem, 6vh, 4rem);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.stickers__hint {
  position: absolute;
  right: 1rem;
  bottom: 0.85rem;
  color: var(--ink-faint);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  font-size: 0.6rem;
}
.sticker {
  position: absolute;
  display: grid;
  place-items: center;
  min-width: 74px;
  height: 74px;
  padding-inline: 0.9rem;
  border-radius: 50%;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: #0a0a0c;
  cursor: grab;
  user-select: none;
  touch-action: none;                /* the draggable owns the gesture */
  will-change: transform;
  box-shadow: 0 12px 30px -12px rgb(0 0 0 / 0.7);
}
.sticker:active { cursor: grabbing; }
.sticker__apple { width: 42px; height: 42px; }
.sticker--1 { left: 6%;  top: 22%; background: var(--ink); }
/* Dark type like its two neighbours. Pale ink on the plasma disc was 3.4:1 —
   the odd one out here, and the only sticker you had to squint at. */
.sticker--2 { left: 32%; top: 46%; background: var(--plasma); }
.sticker--3 { left: 58%; top: 18%; background: var(--acid); }

/* Colophon ----------------------------------------------------------------- */
.colophon {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: clamp(2.5rem, 6vh, 4rem);
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
}
.colophon a { border-bottom: 1px solid var(--line-strong); }
.colophon a:hover { color: var(--ink); border-color: var(--tint); }

/* --------------------------------------------------------------------------
   Toast
   -------------------------------------------------------------------------- */
.toast {
  position: fixed;
  left: 50%;
  bottom: clamp(1rem, 4vh, 2.5rem);
  z-index: 50;
  padding: 0.7rem 1.1rem;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: var(--ink);
  color: var(--bg);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 12px);
  pointer-events: none;
  transition: opacity 0.3s var(--ease-out), transform 0.4s var(--ease-out);
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.toast:empty { display: none; }

/* --------------------------------------------------------------------------
   Reduced motion
   --------------------------------------------------------------------------
   The component half of the policy written down in base.css: the loops that
   run on their own stop, and the ones that answer a pointer stay. The marquee
   and the canopy bob are GSAP's, so they are switched off in
   js/modules/reveals.js and js/modules/tree.js instead — CSS cannot reach a
   tween.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  /* Ran forever, at the one spot on the page the eye is being asked to leave. */
  .hero__cue-line { animation: none; opacity: 0.9; transform: scaleX(1); }
  .masthead__brand:hover .monogram { animation: none; }
}
