/* ==========================================================================
   base.css — design tokens, reset, typography, shared primitives.
   Component and section styles live in site.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Fonts. Self-hosted latin subsets of two variable faces:
   Archivo carries a width axis (62–125) which is what makes the hero
   wordmark possible without shipping a second display font.
   -------------------------------------------------------------------------- */
/* format('woff2'), not format('woff2-variations'): the -variations keyword is
   deprecated and browsers disagree about it. A format() string a browser does
   not recognise makes it skip that src entirely — which means no webfont at
   all, in that browser only. Plain 'woff2' is understood everywhere, and the
   variable axes below work regardless of what the format string says. */
@font-face {
  font-family: 'Archivo';
  src: url('/assets/fonts/archivo-latin-var.woff2') format('woff2');
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('/assets/fonts/jetbrains-mono-latin-var.woff2') format('woff2');
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   Tokens
   -------------------------------------------------------------------------- */
/* Registering the accent as a real colour is what lets it *transition* when
   JS swaps it at the play → build boundary. Browsers without @property just
   get an instant switch, which is a perfectly good fallback. */
@property --tint {
  syntax: '<color>';
  inherits: true;
  initial-value: #cdfb45;
}

:root {
  /* Palette. Two accents doing actual work: acid = the playing half,
     plasma = the building half. The WebGL scene and the page tint both
     interpolate between them as you scroll, so colour carries the narrative. */
  /* Four steps of ink, and the bottom two are pinned to a contrast floor
     rather than to taste. --ink-faint carries real text — the tree's
     invitation, the GitHub panel's stat labels, the colophon — at 9-11 px,
     which is nowhere near large enough for WCAG's 3:1 large-text allowance.
     It was #5d5d68, or 3.08:1 on --bg, and every one of those lines failed AA.
     #7a7a88 is 4.73:1 on --bg and 4.59:1 on --bg-raise, so it passes against
     both surfaces it is ever painted on, and the four steps still read as four
     steps: 17.7 → 7.2 → 4.7. Anything dimmer than this is decoration, and
     decoration does not get to hold words. */
  --bg:        #08080a;
  --bg-raise:  #0d0d11;
  --ink:       #f3f1ec;
  --ink-dim:   #9a9aa6;
  --ink-faint: #7a7a88;
  --acid:      #cdfb45;
  /* Nudged up from #7d5cff for the same reason: the GitHub panel re-points
     --tint at it, so this is a *text* colour there, and 4.48:1 missed AA by
     two hundredths. #836bff is 5.09:1 on the panel and reads as the same
     purple. Changing it here changes it in the WebGL scenes too, which is the
     point of keeping one token. */
  --plasma:    #836bff;

  /* The orchard. `macieira` is Portuguese for apple tree, so these three are
     brand colours rather than illustration colours: the logo, the fruit in
     the hero and the particle field all read from them. --leaf is the acid
     accent on purpose — a neon-green leaf against the red is what keeps the
     tree inside this palette instead of turning the page into a nature doc. */
  --apple:      #e4392f;
  --apple-deep: #96201a;
  --leaf:       var(--acid);
  --bark:       #8b7b69;

  --line:        rgb(255 255 255 / 0.10);
  --line-strong: rgb(255 255 255 / 0.20);

  /* Live tint: reveals.js writes this on <html> as sections come into view. */
  --tint: var(--acid);
  transition: --tint 0.9s var(--ease-out);

  /* Rhythm */
  --gutter: clamp(1.25rem, 4.2vw, 5rem);
  --measure: 1680px;
  --section-y: clamp(6rem, 16vh, 13rem);
  --radius: 14px;

  /* Type */
  --font-display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --step-label: clamp(0.66rem, 0.72vw, 0.75rem);
  --step-body: clamp(1rem, 0.95rem + 0.25vw, 1.18rem);
  --step-h2: clamp(1.9rem, 1.1rem + 3.4vw, 4.25rem);
  --step-h3: clamp(1.15rem, 1rem + 0.8vw, 1.6rem);

  /* Motion */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}

/* --------------------------------------------------------------------------
   Reset
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
}

/* Native smooth scroll only matters when Lenis is *not* running (JS off) —
   otherwise the two fight each other. */
html:where(.no-js) { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--step-body);
  font-variation-settings: 'wdth' 100;
  line-height: 1.55;
  min-height: 100svh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, canvas { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; }
a { color: inherit; text-decoration: none; }
ul { padding: 0; list-style: none; }
dl, dd { margin: 0; }

:focus-visible {
  outline: 2px solid var(--tint);
  outline-offset: 3px;
  border-radius: 4px;
}

/* <main> only takes focus because the skip link sends it there (tabindex="-1"
   in the markup). Ringing it would draw a 2 px box around the entire page,
   which reads as a rendering bug rather than as feedback — and the jump to the
   top of the content is the feedback. Everything *inside* it still rings. */
main:focus-visible { outline: none; }

::selection { background: var(--tint); color: #0a0a0c; }

/* --------------------------------------------------------------------------
   Scrollbar
   --------------------------------------------------------------------------
   The native bar is *removed*, not styled, and js/modules/scrollbar.js draws
   the replacement (see .scrollbar in site.css). That is the only way to get a
   scrollbar that looks the same in every browser: `scrollbar-width` and
   `scrollbar-color` are all Firefox will ever offer, the ::-webkit-scrollbar
   pseudo-elements are what Chromium and Safari offer, the two do not overlap,
   and in Chromium — which has both — setting the standard property makes it
   ignore the pseudo-elements. No combination of them draws one bar.

   Taking it away is gated on the same promise as the reveals: `js` is set
   before first paint by the probe in index.html, and `boot-failed` releases
   everything if the module graph never arrives. Remove that guard and a page
   whose JS failed has no scrollbar at all.
   -------------------------------------------------------------------------- */
html.js:not(.boot-failed) { scrollbar-width: none; }
html.js:not(.boot-failed)::-webkit-scrollbar { display: none; }

/* --------------------------------------------------------------------------
   Lenis. These rules are required by the library itself (its dist/lenis.css),
   inlined here so the page ships one stylesheet fewer.
   -------------------------------------------------------------------------- */
html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* --------------------------------------------------------------------------
   Typography primitives
   -------------------------------------------------------------------------- */
.mono {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 400;
  letter-spacing: 0.02em;
}

.label {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.h2 {
  font-size: var(--step-h2);
  font-weight: 600;
  font-variation-settings: 'wdth' 96;
  line-height: 1.02;
  letter-spacing: -0.025em;
  text-wrap: balance;
  max-width: 18ch;
}

.prose {
  color: var(--ink-dim);
  max-width: 46ch;
  text-wrap: pretty;
}

.dim { color: var(--ink-dim); }
.sep { color: var(--ink-faint); }

.icon { width: 20px; height: 20px; fill: currentColor; flex: none; }
.icon--sm { width: 13px; height: 13px; }

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* Read, never seen.
   For the labels that a sighted reader gets from layout and a screen reader
   gets from nothing — "Local time in Portugal" in front of the clock digits,
   "Email" in front of an address. This is deliberately not `aria-label`: that
   attribute is only honoured on elements with a role that supports naming, and
   a bare <span> is a `generic`, where support is inconsistent across engines.
   Real text in the document works everywhere and survives translation, because
   it can carry its own data-pt like every other string on the page. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Small pulsing status dot, reused by the hero kicker and the GitHub panel. */
.dot {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--tint);
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--tint) 60%, transparent);
  animation: pulse 2.6s var(--ease-inout) infinite;
}
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--tint) 55%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in oklab, var(--tint) 0%, transparent); }
}

.skip-link {
  position: fixed;
  top: 0.75rem; left: 0.75rem;
  z-index: 100;
  padding: 0.7rem 1rem;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  border-radius: 8px;
  transform: translateY(-160%);
  transition: transform 0.25s var(--ease-out);
}
.skip-link:focus-visible { transform: none; }

/* --------------------------------------------------------------------------
   Reveal contract
   --------------------------------------------------------------------------
   Elements only start hidden when JS is running.
   Without JS, or if the module fails to boot
   (html.boot-failed, set by the failsafe in index.html), everything is
   simply visible — no animation library is load-bearing for reading the page.
   -------------------------------------------------------------------------- */
html.js:not(.boot-failed) [data-reveal] {
  opacity: 0;
  transform: translateY(28px);
}
html.js:not(.boot-failed) [data-reveal-lines] > .line > .line__inner {
  transform: translateY(105%);
}
html.js:not(.boot-failed) [data-hero] {
  opacity: 0;
}

/* The tree's own hidden state.
   Only the *fills* and the fruit start hidden — never the branches. The wood
   is stroked in the markup and stays that way, so anime.js redrawing it is
   pure decoration on a picture that is already there. The fills and the
   apples are released by the hero timeline in js/modules/hero.js, which is on
   the critical path; nothing here waits on the lazily-imported anime.js. */
html.js:not(.boot-failed) .monogram__body,
html.js:not(.boot-failed) .monogram__leaf {
  fill-opacity: 0;
}
html.js:not(.boot-failed) .orchard__glow { opacity: 0; }
/* Uniform because every branch declares pathLength="1" — see index.html. */
html.js:not(.boot-failed) [data-branch] {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
html.js:not(.boot-failed) .apple {
  opacity: 0;
  transform: scale(0.3);
}

/* Line-mask wrappers created by the splitter in js/modules/reveals.js */
.line { display: block; overflow: hidden; padding-bottom: 0.06em; }
.line__inner { display: block; will-change: transform; }

/* --------------------------------------------------------------------------
   Reduced motion
   --------------------------------------------------------------------------
   Reduce, not remove — and the distinction is the whole policy, so it is
   written down in one place here and mirrored in js/modules/env.js.

   What stays: the smooth scroll and the tree. Those are the page, not effects
   applied to it, and a visitor who gets neither is looking at a different
   site. Both are also *reactive* — they answer a wheel or a click and stop
   when you do — which is the category the preference is least concerned with.

   What goes: everything that moves on its own and never stops. Motion nobody
   asked for and nobody can stop is the thing the preference is actually for,
   and it is also WCAG 2.2.2 (Level A) once it runs past five seconds beside
   other content. So the marquee, the pulsing status dots, the scroll cue and
   the idle bob in the canopy all come to rest, and the entrance reveals stop
   travelling — they are released outright here, because an element that never
   gets its animation must never be left holding the animation's start state.
   js/modules/reveals.js and js/modules/hero.js skip the matching tweens, or
   the two halves would fight and the fade would flash.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html.js:not(.boot-failed) [data-reveal],
  html.js:not(.boot-failed) [data-hero] {
    opacity: 1;
    transform: none;
  }
  html.js:not(.boot-failed) [data-reveal-lines] > .line > .line__inner {
    transform: none;
  }
  html.js:not(.boot-failed) .monogram__body,
  html.js:not(.boot-failed) .monogram__leaf {
    fill-opacity: 1;
  }
  html.js:not(.boot-failed) .orchard__glow { opacity: 1; }
  html.js:not(.boot-failed) [data-branch] { stroke-dashoffset: 0; }
  html.js:not(.boot-failed) .apple { opacity: 1; transform: none; }

  /* Decorative loops, all of them. */
  .dot { animation: none; }
}

/* Per-character wrappers for the hero wordmark. The word box keeps the line
   from breaking between two character masks — see splitChars() in split.js. */
.word { display: inline-block; white-space: nowrap; }
.char { display: inline-block; will-change: transform; }
.char__inner { display: inline-block; }
