/* ==========================================================================
   Odell — portfolio stylesheet · base

   Load 2 of 8 — see tokens.css for the full map and for
   why the load order carries meaning.

   02 Reset & base · 03 Utilities

   The box model, the document defaults, and the handful of helper
   classes the markup leans on.
   ========================================================================== */

/* ==========================================================================
   02  RESET & BASE
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Keeps anchored section headings clear of the sticky header. */
  scroll-padding-top: calc(var(--header-h) + 1rem);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

h1,
h2,
h3,
h4 {
  margin: 0;
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-weight: 700;
  text-wrap: balance;
}

p {
  margin: 0;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

img,
svg {
  display: block;
  max-width: 100%;
}

button {
  font: inherit;
  color: inherit;
}

code {
  font-family: var(--mono);
  font-size: 0.9em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1em 0.36em;
}

a {
  color: var(--accent);
}

/* Visible focus for keyboard users only; pointer clicks stay clean. */
:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: 4px;
}

:focus:not(:focus-visible) {
  outline: none;
}

::selection {
  background: var(--accent-soft);
}

/* ==========================================================================
   03  UTILITIES
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The icon sprite is referenced by <use>, never rendered itself. */
.sprite {
  display: none;
}

.icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  /* Filled and stroked symbols both inherit colour from here. */
  color: currentColor;
}

.skip-link {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 200;
  padding: 0.6rem 1rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-200%);
  transition: transform 0.18s var(--ease);
}

.skip-link:focus {
  transform: translateY(0);
}

/* ---- Language switching -------------------------------------------------
   English is the document default, so a visitor without JavaScript sees one
   clean language rather than both at once. The script flips <html lang> and
   these rules follow. */
html[lang="en"] .lang-es {
  display: none;
}

html[lang="es"] .lang-en {
  display: none;
}

/* ---- The switch ---------------------------------------------------------
   The copy does not swap in place. The page leaves, the language flips while
   nothing is on screen, and the page returns. <html> carries `lang-out` for
   the first half and `lang-in` for the second, and main.js performs the swap
   between them — so the script still owns every language decision, and there
   is still only one place that decides what language the document is in.

   Two classes rather than one because the halves want opposite timings.
   Everything leaves together on a quick ease-in; it returns on a slower
   ease-out staggered by section, so the page reads as re-lettering itself
   rather than as a jump cut. `:nth-of-type` finds the sections, so nothing
   has to be tagged in the markup — but a new section does still need its own
   delay below, and leaving one out is quiet rather than loud. An unstaggered
   section does not fail to animate; it animates first, so the bottom of the
   page lights up before the top. That is exactly what the rating section did
   when it was added, and it is why the list runs to seven rather than
   stopping at the six the page had.

   Only opacity and transform are animated. Both are composited, neither
   triggers layout, and the stagger is small enough that seven sections are
   never individually expensive. Under reduced motion the global rule in
   section 17 pins these durations to nothing, and main.js skips the sequence
   outright so not even the wait before the swap is felt.

   The dev entry is named alongside the footer in both halves. It is not
   bilingual and has nothing to relabel, but it sits in the footer's own band
   and is covered by neither `#main>section` nor `.site-footer` — left out
   here it would be the one thing still lit while the page around it empties,
   which reads as a bug rather than as a thing that survived the switch. */
html.lang-out #main>section,
html.lang-out .site-footer,
html.lang-out .dev-entry {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 140ms ease-in, transform 140ms ease-in;
}

html.lang-in #main>section,
html.lang-in .site-footer,
html.lang-in .dev-entry {
  opacity: 1;
  transform: none;
  transition: opacity 300ms var(--ease), transform 300ms var(--ease);
}

html.lang-in #main>section:nth-of-type(2) {
  transition-delay: 35ms;
}

html.lang-in #main>section:nth-of-type(3) {
  transition-delay: 70ms;
}

html.lang-in #main>section:nth-of-type(4) {
  transition-delay: 105ms;
}

html.lang-in #main>section:nth-of-type(5) {
  transition-delay: 140ms;
}

html.lang-in #main>section:nth-of-type(6) {
  transition-delay: 175ms;
}

html.lang-in #main>section:nth-of-type(7) {
  transition-delay: 210ms;
}

html.lang-in .site-footer,
html.lang-in .dev-entry {
  transition-delay: 245ms;
}

/* The navigation is the one exception to the header holding still. Those five
   words are the only other bilingual text in the frame, so swapping them in
   place would put a hard cut in the one part of the page that is still fully
   lit — the rest of the header is dark and the page behind it is fading, so
   the eye has nowhere else to be. The brand and the two tool buttons do hold,
   which keeps the header reading as a frame whose words re-letter rather than
   as a thing that blinks. Out slightly quicker than the sections, so the
   words are gone before the swap lands. */
html.lang-out .nav-link {
  opacity: 0;
  transition: opacity 120ms ease-in;
}

html.lang-in .nav-link {
  opacity: 1;
  transition: opacity 260ms var(--ease) 60ms;
}

/* The button's own label arrives by turning into place rather than by being
   relabelled. main.js performs the swap while `lang-out` has the page hidden,
   so by the time this starts the text is already the new one — the turn
   begins edge-on, which is exactly where a swap cannot be seen. The header
   is deliberately not faded, so this is the one piece of the transition the
   visitor is looking at when it happens. */
html.lang-in #langToggleText {
  animation: lang-flip 420ms var(--ease);
}

@keyframes lang-flip {
  from {
    transform: rotateY(-88deg);
  }

  to {
    transform: rotateY(0deg);
  }
}

/* ---- The morph ----------------------------------------------------------
   The second half of the same click, running underneath the first. The page
   still leaves and returns; what changes is that the copy re-letters itself on
   the way, a character at a time, instead of arriving already changed.

   The churn is written into both spans of a pair, not just the visible one, so
   this rule is applied to both. That is safe: only one of the two is ever laid
   out, and `display` here cannot reveal the hidden one because
   `html[lang] .lang-*` — two classes and an attribute — outranks a single
   class, and this declaration has no `!important` to beat it with.

   `overflow-x: clip` rather than `hidden` is deliberate. `hidden` would force
   the other axis to `auto`, which can shave descenders off; `clip` leaves
   `overflow-y` visible, so a box that is briefly narrower than its text never
   cuts the bottom off a g or a y. What is clipped is churning noise, so
   nothing legible is ever hidden by it. `white-space: nowrap` keeps a box that
   is briefly too narrow from wrapping the string onto a second line and moving
   everything below it — the churn is the longer of the two strings, so this
   is the common case, not the rare one.

   No duration here. main.js sets it inline from the same number it schedules
   the settling against, so the box cannot finish moving at a different time
   from the text — the drift that `returnMs()` exists to prevent on the other
   half of the switch. */
.lang-scramble {
  display: inline-block;
  white-space: nowrap;
  overflow-x: clip;
  transition-property: width;
  transition-timing-function: var(--ease);
}

/* Copy that wraps in either language is held still by the element around it
   instead, and neither span is touched at all. Holding a paragraph on one line
   would clip it to its first line and collapse everything under it; and giving
   the spans any display other than `inline` moves the baseline of every line
   box they sit in, which grows each of those paragraphs by whole lines for as
   long as the effect runs. So the parent is pinned by height and the spans keep
   the layout they already had.

   Both axes are clipped here rather than just the horizontal one: what
   overflows a pinned box is whole lines of churn, and they are the part that
   must not be seen pushing the page around. `clip` and not `hidden` for the
   same reason as above — `hidden` would make this a scroll container, and a
   paragraph that briefly held more lines than it was pinned to would scroll
   rather than simply being cut off. */
.lang-scramble-box {
  overflow: clip;
  transition-property: height;
  transition-timing-function: var(--ease);
}

