/* Shared site-wide components: hero, nav, language toggle, cards, buttons,
   footer, and the bottom-right home-link widget. Loaded on every page.
   The Apps-grid-specific components (tool-card themes, filter chips, view
   badges) live in apps-theme.css, loaded only on my-apps.html. */

/* ---------- Hero / header ---------- */
/* "Aurora" background: a base gradient sweep plus two soft blob layers
   drifting independently along irregular (non-symmetric) waypoints, each on
   its own period. The three periods (13s/8s/6s) share no common factor, so
   the combined pattern takes their LCM (624s, ~10 minutes) to exactly
   repeat, which reads as organic/non-looping in normal viewing rather than
   the metronomic back-and-forth a single simple animation gives. */
.hero {
  position: relative;
  overflow: hidden;
  /* Per-page color theme, overridable by a `.hero--<page>` modifier class
     (see "Per-page hero themes" below) — defaults to the shared brand
     gradient so pages without their own theme are unaffected. */
  --hero-c1: var(--color-brand-start);
  --hero-c2: var(--color-brand-end);
  --hero-blob-a-highlight: rgba(255, 255, 255, 0.32);
  --hero-blob-b-highlight: rgba(180, 150, 255, 0.55);
  background: linear-gradient(120deg, var(--hero-c1), var(--hero-c2), var(--hero-c1));
  background-size: 200% 200%;
  animation: hero-drift-base 13s ease-in-out infinite;
  padding: var(--space-10) var(--space-8) var(--space-11);
  text-align: center;
}

.hero::before,
.hero::after {
  content: '';
  position: absolute;
  inset: -25%;
  z-index: 0;
  pointer-events: none;
}

.hero::before {
  background: radial-gradient(circle at 30% 35%, var(--hero-blob-a-highlight), transparent 45%),
              radial-gradient(circle at 68% 62%, var(--hero-c2), transparent 55%);
  mix-blend-mode: soft-light;
  animation: hero-drift-blob-a 8s ease-in-out infinite;
}

.hero::after {
  background: radial-gradient(circle at 62% 28%, var(--hero-blob-b-highlight), transparent 50%),
              radial-gradient(circle at 22% 72%, var(--hero-c1), transparent 55%);
  mix-blend-mode: overlay;
  animation: hero-drift-blob-b 6s ease-in-out infinite;
}

@keyframes hero-drift-base {
  0%   { background-position: 0% 30%; }
  22%  { background-position: 65% 10%; }
  47%  { background-position: 100% 70%; }
  71%  { background-position: 30% 95%; }
  100% { background-position: 0% 30%; }
}

@keyframes hero-drift-blob-a {
  0%   { transform: translate(0%, 0%) scale(1); }
  28%  { transform: translate(7%, -9%) scale(1.12); }
  54%  { transform: translate(-6%, 5%) scale(0.94); }
  81%  { transform: translate(9%, 6%) scale(1.06); }
  100% { transform: translate(0%, 0%) scale(1); }
}

@keyframes hero-drift-blob-b {
  0%   { transform: translate(0%, 0%) scale(1); }
  33%  { transform: translate(-8%, 6%) scale(1.1); }
  62%  { transform: translate(6%, -7%) scale(0.92); }
  86%  { transform: translate(-4%, -4%) scale(1.05); }
  100% { transform: translate(0%, 0%) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .hero,
  .hero::before,
  .hero::after {
    animation: none;
  }

  .hero {
    background: linear-gradient(135deg, var(--hero-c1), var(--hero-c2));
  }
}

/* ---------- Per-page hero themes ----------
   Each inner page can carry its own hero color story instead of the shared
   brand gradient, by overriding the --hero-* custom properties from a
   `.hero--<page>` modifier class (combined with `.hero--compact`/`.hero--full`
   for sizing). Reuses the same aurora animation machinery above — no extra
   JS, no extra paint cost. */

/* About: a slow-moving "flame" glow in the red/yellow of the Spanish flag
   — red and yellow naturally interpolate through orange, so no third color
   is needed for the fire look. The base gradient is circular rather than
   the shared diagonal sweep, echoing the ripple canvas effect drawn on top
   of it; it still drifts via the same hero-drift-base background-position
   animation, just panning a radial "sun" around instead of a linear band.
   A short, independent flicker animation on each blob (desynced periods,
   like the base drift periods above) adds the irregular pulse of a real
   flame without any JS. */
.hero--about {
  --hero-c1: #aa151b;
  --hero-c2: #f1bf00;
  --hero-blob-a-highlight: rgba(255, 233, 150, 0.6);
  --hero-blob-b-highlight: rgba(255, 120, 0, 0.65);
  background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  background-size: 200% 200%;
}

@media (prefers-reduced-motion: reduce) {
  .hero--about {
    background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  }
}

.hero--about::before {
  animation: hero-drift-blob-a 8s ease-in-out infinite, hero-flame-flicker 2.4s ease-in-out infinite;
}

.hero--about::after {
  animation: hero-drift-blob-b 6s ease-in-out infinite, hero-flame-flicker 3.1s ease-in-out infinite;
}

@keyframes hero-flame-flicker {
  0%, 100% { opacity: 0.85; }
  35%      { opacity: 1; }
  60%      { opacity: 0.78; }
  80%      { opacity: 0.96; }
}

@media (prefers-reduced-motion: reduce) {
  .hero--about::before,
  .hero--about::after {
    animation: none;
  }
}

/* Thanks: the same circular, drifting gradient as About's, in the colors of
   a heart — a warm rose-red core fading out to deep crimson. The glow
   blobs beat like a pulse (a quick "lub-dub" then a rest), done with
   opacity only so it doesn't fight the blobs' own drift animation, which
   owns `transform`. The core is kept saturated rather than pale so the
   white heading and the white fireworks stay legible against it. */
.hero--thanks {
  --hero-c1: #8e0b33;
  --hero-c2: #f2557f;
  --hero-blob-a-highlight: rgba(255, 170, 190, 0.55);
  --hero-blob-b-highlight: rgba(255, 60, 100, 0.6);
  background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  background-size: 200% 200%;
}

.hero--thanks::before {
  animation: hero-drift-blob-a 8s ease-in-out infinite, hero-heartbeat 1.8s ease-in-out infinite;
}

.hero--thanks::after {
  animation: hero-drift-blob-b 6s ease-in-out infinite, hero-heartbeat 1.8s ease-in-out infinite;
}

@keyframes hero-heartbeat {
  0%, 52%, 100% { opacity: 0.78; }
  12%           { opacity: 1; }
  24%           { opacity: 0.82; }
  36%           { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .hero--thanks {
    background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  }

  .hero--thanks::before,
  .hero--thanks::after {
    animation: none;
  }
}

/* Background: the same moving circular gradient as About's flame theme,
   but its light endpoint continuously cross-fades between Japan's white
   and Spain's yellow every 10s — the two flags of this page's "Spain →
   Japan" academic move, alternating. --hero-flag-color is registered via
   @property purely so the browser knows to interpolate it as a color
   (smooth crossfade) rather than jump-cutting between keyframes; --hero-c2
   is aliased to it so the blob highlight (which also reads --hero-c2)
   cycles in sync automatically. The white endpoint is kept a soft
   off-white rather than pure white so the hero's white heading text stays
   legible throughout the drift instead of vanishing when the pan brings
   the light end of the gradient behind it. */
@property --hero-flag-color {
  syntax: '<color>';
  inherits: true;
  initial-value: #fbeceb;
}

@keyframes hero-flag-cycle {
  0%   { --hero-flag-color: #fbeceb; }
  50%  { --hero-flag-color: #f1bf00; }
  100% { --hero-flag-color: #fbeceb; }
}

.hero--background {
  --hero-c1: #bc002d;
  --hero-c2: var(--hero-flag-color);
  --hero-blob-a-highlight: rgba(255, 255, 255, 0.5);
  --hero-blob-b-highlight: rgba(220, 20, 60, 0.55);
  background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  background-size: 200% 200%;
  animation: hero-drift-base 13s ease-in-out infinite, hero-flag-cycle 20s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .hero--background {
    animation: none;
    background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  }
}

/* Work: a three-tone blue gradient sampled from Adrï's brand mark (navy →
   mid-blue → bright blue), sweeping the same diagonal band as the shared
   default gradient but through three real hues via an extra --hero-c3 stop
   instead of interpolating just two. The aurora blob layers still only
   read c1/c2 (see .hero::before/::after above), so their highlights are
   picked to read against navy rather than against the brighter blue. */
.hero--work {
  --hero-c1: #0b2e74;
  --hero-c2: #1d46b9;
  --hero-c3: #2851f6;
  --hero-blob-a-highlight: rgba(255, 255, 255, 0.28);
  --hero-blob-b-highlight: rgba(120, 170, 255, 0.55);
  background: linear-gradient(120deg, var(--hero-c1), var(--hero-c2), var(--hero-c3), var(--hero-c2), var(--hero-c1));
  background-size: 200% 200%;
}

@media (prefers-reduced-motion: reduce) {
  .hero--work {
    background: linear-gradient(135deg, var(--hero-c1), var(--hero-c2), var(--hero-c3));
  }
}

/* Games: forest green and gold, evoking Hyrule's Triforce/Kokiri-forest
   palette (and Tunic's own green-tunic-and-gold-ruins look) — a fitting
   theme for a page recommending games. Just a recolor of the shared
   diagonal sweep, no new gradient shape or extra stop, so unlike About/
   Background/Work above it only needs to override the custom properties
   the base `.hero` rule already reads via var() — that rule's own
   `background`/`animation` declarations pick up these new values on their
   own, nothing to redeclare. */
.hero--games {
  --hero-c1: #1e5631;
  --hero-c2: #d4af37;
  --hero-blob-a-highlight: rgba(255, 224, 130, 0.55);
  --hero-blob-b-highlight: rgba(120, 200, 130, 0.5);
}

/* Trips: a clear daytime sky (deep cerulean sweeping to a lighter horizon
   blue) instead of the shared brand blue/purple — fitting a page about
   travel, and it keeps the white wireframe globe drawn on top legible
   against it. Same moving circular gradient as About's flame theme (a
   panning "sun" rather than the base .hero's diagonal sweep) — reads like
   open sky/sunlight rather than a directional streak, which suits a sky
   theme better than the shared linear gradient shape would. */
.hero--trips {
  --hero-c1: #2b7fd1;
  --hero-c2: #8ecbf0;
  --hero-blob-a-highlight: rgba(255, 255, 255, 0.55);
  --hero-blob-b-highlight: rgba(100, 180, 230, 0.5);
  background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  background-size: 200% 200%;
}

@media (prefers-reduced-motion: reduce) {
  .hero--trips {
    background: radial-gradient(circle at 50% 50%, var(--hero-c2) 0%, var(--hero-c1) 75%);
  }
}

/* My Apps: a near-black-to-dark-forest-green sweep and green-tinted blob
   glow, deliberately muted rather than colorful — the digital-rain canvas
   drawn on top (matrix-bg.js) is the real visual interest here, and a loud
   gradient underneath would fight the falling glyphs instead of setting
   them off. Same shared diagonal sweep shape as the default .hero, just
   recolored, like .hero--games above. */
.hero--myapps {
  --hero-c1: #010a04;
  --hero-c2: #0a2f17;
  --hero-blob-a-highlight: rgba(0, 255, 65, 0.25);
  --hero-blob-b-highlight: rgba(0, 200, 83, 0.35);
}

/* Reflections: an aged-manuscript palette — ivory near-white sweeping
   through pale ochre to a deeper old-parchment gold and back, the same
   three-stop technique as .hero--work above but light throughout (unlike
   every other hero theme, which stays mid-to-dark so the default white
   heading text reads against it). The quill canvas drawn on top
   (quill-bg.js) traces dark sepia ink strokes, which is why this is the
   one hero theme that also overrides the heading/subtitle color below
   instead of using the shared white. */
.hero--reflections {
  --hero-c1: #faf3e0;
  --hero-c2: #eed9a0;
  --hero-c3: #ddbb6e;
  --hero-blob-a-highlight: rgba(255, 255, 245, 0.6);
  --hero-blob-b-highlight: rgba(200, 160, 90, 0.35);
  background: linear-gradient(120deg, var(--hero-c1), var(--hero-c2), var(--hero-c3), var(--hero-c2), var(--hero-c1));
  background-size: 200% 200%;
}

@media (prefers-reduced-motion: reduce) {
  .hero--reflections {
    background: linear-gradient(135deg, var(--hero-c1), var(--hero-c2), var(--hero-c3));
  }
}

/* Fonts: a warm vellum/parchment palette — ivory through peach to a dusty
   terracotta-tan, the same light three-stop technique as .hero--reflections
   above but a distinct rosier hue so the two "light" hero themes (both
   under Art & Language) don't read as duplicates. The glyph canvas drawn on
   top (glyph-bg.js) traces actual letterforms from the page's own custom
   fonts in dark ink, echoing a page of font-design work in progress. */
.hero--fonts {
  --hero-c1: #fdf8f0;
  --hero-c2: #f2ddc7;
  --hero-c3: #e0b48f;
  --hero-blob-a-highlight: rgba(255, 250, 240, 0.6);
  --hero-blob-b-highlight: rgba(200, 130, 90, 0.3);
  background: linear-gradient(120deg, var(--hero-c1), var(--hero-c2), var(--hero-c3), var(--hero-c2), var(--hero-c1));
  background-size: 200% 200%;
}

@media (prefers-reduced-motion: reduce) {
  .hero--fonts {
    background: linear-gradient(135deg, var(--hero-c1), var(--hero-c2), var(--hero-c3));
  }
}

.hero--full {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Per-page hero canvas effects (see network-bg.js / ripple-bg.js /
   trail-bg.js / marco-polo-bg.js / contact-bg.js / thanks-bg.js) — sit above the gradient/blobs, below the actual hero
   content. z-index:1 is load-bearing here: .hero::after is generated
   content, which paints after real DOM children even though it's also
   z-index:0, so without an explicit higher z-index the canvas would render
   *under* that blob. */
.hero-network,
.hero-ripples,
.hero-trails,
.hero-math,
.hero-maze,
.hero-globe,
.hero-matrix,
.hero-quill,
.hero-glyphs,
.hero-digraphs,
.hero-squadro-board,
.hero-mp-routes,
.hero-contact-pulses,
.hero-thanks-fireworks {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .hero-network,
  .hero-ripples,
  .hero-trails,
  .hero-math,
  .hero-maze,
  .hero-globe,
  .hero-matrix,
  .hero-quill,
  .hero-glyphs,
  .hero-digraphs,
  .hero-squadro-board,
  .hero-mp-routes,
  .hero-contact-pulses,
  .hero-thanks-fireworks {
    display: none;
  }
}

.hero--compact {
  padding: var(--space-9) var(--space-8) var(--space-10);
}

.hero .container {
  position: relative;
  z-index: 1;
}

.hero h1 {
  color: #ffffff;
  font-size: var(--font-size-hero);
  text-shadow: 0 2px 10px var(--overlay-black-20);
  margin-bottom: var(--space-8);
}

/* The cursive "Adrï" signature wordmark (KnotFont's "A" glyph), used as the
   landing-page hero title in place of plain text. The glyph's ink is about
   three times wider than its advance width (78px at 130px), so it used to
   spill far outside its own box and rely on nothing clipping it — which
   mobile Safari sometimes does, shearing off the sides. The box is now
   sized to contain the whole signature (2.1em, centred), and the font-size
   only shrinks on very narrow screens so that box never exceeds the page. */
.hero-signature {
  display: inline-block;
  width: 2.1em;
  text-align: center;
  font-size: min(130px, 38vw);
  color: #ffffff;
  text-shadow: 0 2px 10px var(--overlay-black-20);
}

.hero--compact h1 {
  font-size: var(--font-size-hero-compact);
  margin-bottom: var(--space-5);
}

.hero .subtitle {
  color: var(--overlay-white-90);
  font-size: var(--font-size-subtitle);
  font-variant: small-caps;
  letter-spacing: 0.08em;
  max-width: 40em;
  margin: 0 auto;
}

/* Reflections' hero background is light (see .hero--reflections above), so
   the shared white heading/subtitle color would wash out — dark sepia ink
   instead, with a soft pale halo standing in for the usual dark drop
   shadow (a dark shadow would only dim contrast further on a light
   background). */
.hero--reflections h1 {
  color: #3b2a14;
  text-shadow: 0 1px 3px rgba(255, 250, 235, 0.6);
}

.hero--reflections .subtitle {
  color: rgba(59, 41, 20, 0.85);
}

/* Fonts' hero background is light too (see .hero--fonts above), same
   reasoning as Reflections just above — dark ink instead of the shared
   white heading color. */
.hero--fonts h1 {
  color: #3d2b1f;
  text-shadow: 0 1px 3px rgba(255, 250, 240, 0.6);
}

.hero--fonts .subtitle {
  color: rgba(61, 43, 31, 0.85);
}

/* ---------- Language toggle ----------
   Rendered twice on every page (one in the sticky nav bar, one in the
   footer — see i18n.js's renderToggle, which keeps both in sync) — CSS
   alone decides which is visible: nav bar on desktop, footer on mobile.
   Neither ever sits on the dark hero gradient anymore, so this is a plain
   light-context pill everywhere, unlike the earlier white-on-translucent
   styling that only worked over the gradient. */
.lang-toggle {
  display: inline-flex;
  background: var(--overlay-black-06);
  border-radius: var(--radius-pill);
  padding: 3px;
  gap: 2px;
}

.lang-toggle button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--color-text-subtle);
  font-size: var(--font-size-small);
  font-weight: 700;
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.lang-toggle button:hover {
  color: var(--color-text);
}

.lang-toggle button[aria-pressed="true"] {
  background: var(--gradient-brand);
  color: #ffffff;
}

/* Nav-bar instance: hidden by default (mobile uses the footer instead),
   shown ≥769px as a flex sibling of the pill row, right-aligned — reads as
   "primary navigation first, locale/utility switcher after it", the same
   left-to-right priority most multilingual sites use. Fixed to the same
   --nav-capsule-height as the pills/lasso circle next to it — its buttons
   stretch to fill that (the default flex align-items:stretch, since
   nothing overrides it) and stay centered via .lang-toggle button's own
   flex centering above. */
.nav-bar-lang-toggle {
  display: none;
  height: var(--nav-capsule-height);
}

/* Footer instance: the default/mobile placement, "always at the bottom"
   exactly like every other page already had (the landing page's hero
   instance was the one exception this unifies away). */
.footer-lang-toggle {
  display: inline-flex;
  margin: 0 auto var(--space-6);
}

.footer-lang-toggle button {
  padding: 12px 18px;
}

@media screen and (min-width: 769px) {
  /* Only touches desktop widths, where the toggle actually appears here —
     the mobile pill-wrapping layout (a plain block-level .site-nav) is
     untouched below this breakpoint. Treats the pill row + toggle as one
     centered cluster rather than forcing a perfectly page-centered pill
     row with the toggle floated off to one side — simpler, and never at
     risk of the toggle overlapping the pills regardless of how many wrap. */
  .site-nav-bar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-6);
  }

  .site-nav-bar .site-nav {
    margin: 0;
  }

  .nav-bar-lang-toggle {
    display: inline-flex;
    flex-shrink: 0;
  }

  .footer-lang-toggle {
    display: none;
  }
}

/* ---------- Site nav (pill row under the hero) ---------- */
/* Sticky bar the pill nav lives in — pinned to the top of the viewport on
   scroll, with a frosted-glass look so page content sliding underneath it
   stays legible without a heavy, opaque bar. */
.site-nav-bar {
  /* Single source of truth for every capsule-shaped control in the bar
     (pills, dropdown triggers, the lasso home circle, the language
     toggle) so they line up exactly instead of each sizing itself from
     its own padding/line-height, which — given a <button> vs <a> vs a
     wrapped toggle all compute that differently — never quite agreed. */
  --nav-capsule-height: 36px;
  position: sticky;
  top: 0;
  z-index: 20;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--color-border-neutral);
  box-shadow: 0 4px 20px rgba(30, 20, 60, 0.05);
}

.site-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-8);
}

/* Rendered directly on the plain page background (never inside .hero), so
   it needs light-context colors, not the white-on-translucent-white styling
   that would only work over the gradient. Shared by the standalone Contact
   pill and every dropdown trigger, so they all look like the same pill. */
.site-nav a,
.nav-dropdown-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: var(--color-badge-bg);
  color: var(--color-badge-text);
  border: 1px solid var(--color-badge-border);
  height: var(--nav-capsule-height);
  padding: 0 var(--space-7);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-small);
  font-weight: 600;
  font-family: inherit;
  /* Browsers apply their own default line-height to <button> (the dropdown
     triggers) instead of inheriting the page's — normally 1.6 here — so
     without pinning it explicitly, the plain <a> pills (like Contact) end
     up visibly taller than the <button> ones for identical padding. Height
     is fixed above anyway (see --nav-capsule-height), but this still keeps
     the text itself vertically centered within that fixed box. */
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.site-nav a svg,
.nav-dropdown-trigger svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

.site-nav a:hover,
.site-nav a[aria-current="page"],
.nav-dropdown-trigger:hover,
.nav-dropdown.is-open .nav-dropdown-trigger,
.nav-dropdown-trigger.is-current-group {
  background: var(--gradient-brand);
  color: #ffffff;
  border-color: transparent;
}

.site-nav a.is-external {
  opacity: 0.85;
}

/* Home-link logo circle, first in the pill row (the same lasso mark as
   the bottom-right sticker widget — see #bottom/.lasso above — just
   without that widget's own drag/shadow behavior) — a one-click way back
   to the top page from anywhere in the nav, without hunting for the
   corner widget. Higher-specificity selector so it overrides the generic
   pill look .site-nav a gets above instead of a plain circle with pill
   padding/radius. Same --nav-capsule-height as every other control here,
   so it's exactly as tall as the pills next to it, not just visually close. */
.site-nav a.site-nav-home {
  width: var(--nav-capsule-height);
  height: var(--nav-capsule-height);
  padding: 0;
  gap: 0;
  border-radius: 50%;
  overflow: hidden;
  background: none;
  transition: box-shadow var(--transition-fast), transform var(--transition-fast);
}

.site-nav a.site-nav-home:hover {
  background: none;
  border-color: var(--color-badge-border);
  box-shadow: 0 2px 10px rgba(30, 20, 60, 0.18);
  transform: translateY(-1px);
}

.site-nav-home img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- Nav group dropdowns (Personal / Creations / Exploring) ---------- */
.nav-dropdown {
  position: relative;
}

.nav-dropdown-caret {
  width: 11px !important;
  height: 11px !important;
  margin-left: -2px;
  transition: transform var(--transition-fast);
}

.nav-dropdown.is-open .nav-dropdown-caret {
  transform: rotate(180deg);
}

/* The outer element is the actual hover/hit-testing box, and it has NO
   visual gap from the trigger (top: 100%) — instead it covers what would
   be the gap with transparent padding-top, so the cursor is still "over"
   this element (and therefore still over .nav-dropdown) while crossing
   from the pill down into the menu. Without this, that empty gap between
   pill and card is a dead zone where the mouse is over neither element,
   :hover breaks, and the menu closes before the cursor ever reaches it.
   The actual visible card is the inner element, offset below the bridge. */
.nav-dropdown-menu {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  padding-top: 10px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--transition-fast), visibility var(--transition-fast);
  z-index: 25;
}

.nav-dropdown:hover .nav-dropdown-menu,
.nav-dropdown:focus-within .nav-dropdown-menu,
.nav-dropdown.is-open .nav-dropdown-menu {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.nav-dropdown-menu-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 220px;
  background: var(--color-card-bg);
  border: 1px solid var(--color-border-neutral);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card-resting);
  padding: var(--space-4);
  transform: translateY(-6px);
  transition: transform var(--transition-fast);
}

.nav-dropdown:hover .nav-dropdown-menu-inner,
.nav-dropdown:focus-within .nav-dropdown-menu-inner,
.nav-dropdown.is-open .nav-dropdown-menu-inner {
  transform: translateY(0);
}

.nav-dropdown-menu a {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  width: 100%;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: transparent;
  border: none;
  color: var(--color-text);
  font-size: var(--font-size-small);
  font-weight: 600;
  text-decoration: none;
  text-align: left;
  white-space: nowrap;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.nav-dropdown-menu a svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  color: var(--color-badge-text);
}

.nav-dropdown-menu a:hover,
.nav-dropdown-menu a[aria-current="page"] {
  /* `.site-nav a[aria-current="page"]` (for the top-level pills' gradient
     fill) matches these links too and sets color:#fff — same specificity,
     so without an explicit override here that white text silently wins,
     leaving barely-readable white-on-light-lavender. */
  background: var(--color-badge-bg);
  color: var(--color-badge-text);
}

/* These now carry a leading category icon too (chess-pawn, compass, etc.),
   so only the trailing "external" glyph gets pushed to the far edge —
   the leading icon stays grouped tightly with the label like every other
   item, instead of `justify-content: space-between` spreading all three
   children apart evenly. */
.nav-dropdown-menu a.is-external svg:last-child {
  width: 13px;
  height: 13px;
  margin-left: auto;
  color: var(--color-text-subtle);
}

.nav-dropdown-heading {
  display: block;
  font-size: 0.7em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-subtle);
  padding: var(--space-3) var(--space-4) 0;
}

.nav-dropdown-heading:first-child {
  padding-top: 0;
}

@media screen and (max-width: 480px) {
  /* The pills wrap onto two rows here, so a menu anchored to its own pill
     (left:0 / left:50%) can poke past the viewport's right edge — even
     while closed, since visibility:hidden still lays it out, which is what
     made the whole page scroll sideways into empty space on phones.
     Anchor every menu to the full-width bar instead and centre it, capped
     to the viewport. */
  .nav-dropdown {
    position: static;
  }

  .nav-dropdown-menu {
    left: 50%;
    transform: translateX(-50%);
    width: max-content;
    max-width: calc(100vw - var(--space-6) * 2);
  }

  .nav-dropdown-menu-inner {
    min-width: 0;
  }
}

/* ---------- In-page pill sub-nav (jump links within a content page) ---------- */
.pill-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  margin: 0 0 var(--space-9);
}

/* Deliberately plain/neutral — distinct from the colored .site-nav pills,
   so an in-page jump nav doesn't read as another top-level nav. */
.pill-nav a,
.pill-nav button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--color-card-bg);
  color: var(--color-text);
  border: 1px solid var(--color-border-neutral);
  padding: var(--space-3) var(--space-7);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: var(--font-size-small);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.pill-nav a:hover,
.pill-nav button:hover,
.pill-nav a[aria-current="true"],
.pill-nav button[aria-pressed="true"] {
  background: var(--gradient-brand);
  color: #ffffff;
  border-color: transparent;
}

/* Separator between two groups of pills (e.g. Work page's main employers
   vs part-time jobs) — a plain flex item, so it picks up the same `gap`
   spacing as every pair of pills either side of it (no separate margin to
   keep in sync), and stretches to the row's own height for free via the
   container's default align-items: stretch (nothing here overrides it),
   matching the capsules' height exactly rather than a hand-picked value. */
.pill-nav-divider {
  width: 0;
  border-left: 1px dashed var(--color-border-neutral);
}

/* ---------- Buttons ---------- */
.btn-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: 12px 30px;
  border-radius: var(--radius-md);
  font-weight: 600;
  text-decoration: none;
  border: none;
  cursor: pointer;
  font-size: 1em;
  font-family: inherit;
  transition: opacity var(--transition-med), transform var(--transition-fast);
}

.btn-pill--primary {
  background: var(--gradient-brand);
  color: #ffffff;
}

.btn-pill--primary:hover {
  opacity: 0.9;
}

.btn-pill--icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  background: var(--overlay-black-06);
  color: var(--color-text);
  transition: background var(--transition-fast);
}

.btn-pill--icon:hover {
  background: var(--overlay-black-12);
}

.btn-pill--icon svg {
  width: 20px;
  height: 20px;
}

/* ---------- Cards ---------- */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-9);
}

.card {
  position: relative;
  background: var(--color-card-bg);
  border-radius: var(--radius-lg);
  padding: var(--space-9);
  box-shadow: var(--shadow-card-resting);
  transition: transform var(--transition-med), box-shadow var(--transition-med);
}

.card--interactive {
  cursor: pointer;
}

.card--interactive:hover,
.card--interactive:focus-within {
  transform: translateY(-5px);
  box-shadow: var(--shadow-card-hover);
}

.card--interactive a.card-link-overlay {
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.card-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-md);
  background: var(--color-badge-bg);
  color: var(--color-badge-text);
  margin-bottom: var(--space-6);
}

.card-icon svg {
  width: 28px;
  height: 28px;
}

.card-title {
  font-size: var(--font-size-card-title);
  font-weight: 700;
  color: var(--color-text);
  margin-bottom: var(--space-4);
}

.card-description {
  color: var(--color-text-muted);
  margin-bottom: var(--space-7);
}

/* Photo-identity cards (landing page) — a photo top half with the title
   overlaid on a dark scrim, and a plain body below. Gives each destination
   visual identity (borrowed from the old site's own photos) while keeping
   the rounded/shadow card language of the rest of the new design. */
.card--photo {
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.card--photo .card-photo-media {
  position: relative;
  aspect-ratio: 16 / 9;
  /* Capped so a card doesn't blow up when it's alone (or one of only two)
     in a `minmax(260px, 1fr)` grid row and the column stretches far wider
     than the usual 3-across row — e.g. the single-card "Programming"
     sub-block. object-fit: cover on the <img> just shows a wider slice of
     the same photo instead of growing the box taller. */
  max-height: 210px;
  overflow: hidden;
  background: var(--color-badge-bg);
}

.card--photo .card-photo-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--transition-med);
}

.card--interactive.card--photo:hover .card-photo-media img {
  transform: scale(1.06);
}

.card--photo .card-photo-media::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0) 55%);
}

/* A pseudo-element paints after real children by default, so without an
   explicit z-index here the ::after scrim above was rendering ON TOP of
   this title instead of behind it. */
.card--photo .card-photo-title {
  position: absolute;
  z-index: 1;
  left: var(--space-7);
  right: var(--space-7);
  bottom: var(--space-5);
  color: #ffffff;
  font-size: var(--font-size-card-title);
  font-weight: 700;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}

.card--photo .card-photo-body {
  padding: var(--space-6) var(--space-7) var(--space-7);
  flex: 1;
}

.card--photo .card-photo-body .card-description {
  margin-bottom: 0;
}

/* ---------- Work-in-progress items ----------
   Pages that exist but aren't ready: shown in the nav menu and landing
   cards with a WIP badge, but not clickable (no href / link overlay). */
.wip-badge {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  padding: 2px var(--space-4);
  border-radius: var(--radius-pill);
  background: #fff4d6;
  color: #8a5a00;
  border: 1px dashed #e0a800;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.5;
  white-space: nowrap;
}

.card--wip {
  cursor: default;
}

.card--wip .card-photo-media img {
  filter: grayscale(0.7);
  opacity: 0.8;
}

.card--wip .card-photo-body .card-description {
  color: var(--color-text-subtle);
}

.card--photo .card-photo-media .wip-badge {
  position: absolute;
  z-index: 1;
  top: var(--space-5);
  right: var(--space-5);
}

.nav-dropdown-item.is-wip {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-small);
  font-weight: 600;
  white-space: nowrap;
  color: var(--color-text-subtle);
  cursor: not-allowed;
}

.nav-dropdown-item.is-wip svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

.nav-dropdown-item.is-wip .wip-badge {
  margin-left: auto;
}

/* ---------- Game showcase (Games page) ---------- */
/* One highlighted game (image + title + description) whose content swaps
   when a thumbnail below it is clicked — see game-showcase.js. */
.game-showcase {
  margin-top: var(--space-9);
}

.game-showcase-highlight {
  display: flex;
  gap: var(--space-8);
  align-items: center;
  margin-top: var(--space-9);
}

/* Themed per-game background: --game-main/--game-accent are set inline by
   game-showcase.js from each game's hand-picked data-main-color/
   data-accent-color (see games.html); --game-text is derived from
   --game-main's luminance for contrast. Defaults here are the plain site
   card look, so nothing changes before that JS runs or for a game with no
   colors set. A compound selector (two classes, beats plain
   .content-card's single-class rule below it) is what lets this override
   .content-card's own background/border-left regardless of source order. */
.content-card.game-showcase-highlight {
  --game-main: var(--color-card-bg);
  --game-accent: var(--color-brand-start);
  --game-text: var(--color-text);
  background: linear-gradient(135deg, var(--game-main), var(--game-accent));
  border-left-color: var(--game-accent);
  transition: background var(--transition-med), border-color var(--transition-med);
}

.game-showcase-highlight .card-title,
.game-showcase-highlight .card-description {
  color: var(--game-text);
  transition: color var(--transition-med);
}

/* The font-download callout picks up the same per-game colors instead of
   its site-wide neutral/purple styling, but deliberately toned down —
   it's supplementary reading, not the card's main content, so it shouldn't
   compete with it. The gradient runs accent->main, the reverse of the
   card's own main->accent sweep (.content-card.game-showcase-highlight
   above), and each stop is heavily blended with white so it reads as a
   pale, muted chip rather than a second block of saturated color; a thin
   all-around rim in --game-main (replacing the shared .note-callout's
   left-only accent border) gives it a defined edge without extra fill. */
.game-showcase-highlight .note-callout {
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--game-accent) 35%, white 65%),
    color-mix(in srgb, var(--game-main) 35%, white 65%));
  border: 2px solid var(--game-main);
  /* Not --game-text: that's calibrated for the much more saturated outer
     card background, but heavily whitening both stops above means this
     callout ends up pale regardless of how dark the game's own colors
     are — the shared dark body-text color is reliably readable on it. */
  color: var(--color-text);
  transition: background var(--transition-med), border-color var(--transition-med);
}

.game-showcase-highlight .note-callout-icon,
.game-showcase-highlight .note-callout a {
  color: var(--game-main);
}

/* The UA's [hidden] -> display:none rule loses to .note-callout's own
   `display: flex` above (same specificity, but author styles always beat
   the user-agent stylesheet) unless something more specific re-asserts
   it — without this, a game with no data-note-key still shows an empty
   callout box (icon, no text) instead of nothing. */
.note-callout[hidden] {
  display: none;
}

.game-showcase-media {
  /* Wider box (16:9 instead of 4:3, and wider in absolute terms too) so
     object-fit: cover has to crop less off the sides of these games'
     mostly-landscape key art — 4:3 was noticeably narrower than the
     source images' own aspect ratio, cutting off logos/characters at
     the edges (e.g. Tunic's title). */
  display: block;
  flex: 0 0 320px;
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 16 / 9;
}

.game-showcase-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--transition-med);
}

.game-showcase-media:hover img {
  transform: scale(1.05);
}

.game-showcase-info {
  flex: 1;
  min-width: 0;
}

.game-showcase-info .card-title {
  margin-bottom: var(--space-5);
}

.game-showcase-info .note-callout {
  margin-top: var(--space-6);
}

/* Flat-top regular hexagons chained edge-to-edge like a honeycomb: each
   hex after the first is pulled left by a quarter of its own width (so
   its slanted edge touches the previous hex's slanted edge) and every
   other one is dropped down by half a hex height (so the touching edges
   actually line up, instead of two flat tops just overlapping). Plain
   flat list of 9 buttons — no wrapper elements — so `+`/`:nth-child`
   below see all 9 as real DOM siblings; below 640px the same flat list
   is rearranged into 3 honeycomb columns via flex-wrap (see that media
   query) rather than by restructuring the markup, since `display:
   contents` looks like it flattens a wrapper for sibling-selector
   purposes but doesn't — `+`/`:nth-child` always match the real DOM
   tree, wrapper or not. */
.game-picker {
  /* A % width — rather than a fixed px one — so the chain's total span
     (7 hex-widths: the first hex plus 8 more at a 0.75-hex-width step)
     always covers exactly .game-picker's own width, matching the content
     cards above/below edge-to-edge instead of sitting narrower than them.
     Side padding is dropped for the same reason (kept only top/bottom,
     for hover-scale headroom). */
  --hex-w: calc(100% / 7);
  --hex-h: calc(var(--hex-w) * 0.8660254);
  display: flex;
  align-items: flex-start;
  justify-content: safe center;
  /* The chain is sized to exactly fill this container's width now, so
     there's nothing left to scroll — `overflow-x: auto` was only ever
     needed back when hex size was a fixed px value that could exceed a
     narrow container. With auto, though, the last hex's hover scale-up
     counted as horizontal overflow, which spawned a scrollbar and (since
     the view doesn't auto-scroll to reveal it) clipped that hex against
     the container's right edge on the very hover meant to highlight it.
     Visible lets the hover scale bleed slightly past the edge instead —
     no scrollbar, nothing clipped. */
  overflow: visible;
  /* Symmetric small padding, top and bottom — the earlier bottom value
     added calc(hex-h*0.5) to reserve room for the dropped-down even
     hexagons, but flex already grows this container to fit them (their
     own margin-top extends its natural auto height); the padding was
     reserving that same space a second time, which read as a much bigger
     gap below the chain than above it. */
  padding: var(--space-4) 0;
}

.game-picker-item {
  position: relative;
  z-index: 1;
  display: block;
  flex: 0 0 auto;
  width: var(--hex-w);
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

.game-picker-item + .game-picker-item {
  margin-left: calc(var(--hex-w) * -0.25);
}

.game-picker-item:nth-child(even) {
  margin-top: calc(var(--hex-h) * 0.5);
}

.game-picker-item:hover,
.game-picker-item:focus-visible {
  z-index: 5;
}

.game-picker-hex {
  --hex-clip: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
  position: relative;
  display: block;
  /* 100% of .game-picker-item (its own immediate parent), NOT var(--hex-w)
     again — --hex-w is a % of .game-picker, and since custom properties
     re-resolve wherever they're used rather than freezing to a pixel
     value, reusing it here would take 1/7 of .game-picker-item's width
     (itself already 1/7 of .game-picker), compounding to roughly 1/49
     instead of matching the item's actual size. Height comes from
     aspect-ratio instead of a var(--hex-h) computed the same way, since
     percentage heights don't resolve reliably against a shrink-to-fit
     parent like .game-picker-item's auto height. */
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 0.8660254;
  /* A regular `border` can't follow a clip-path outline cleanly — it's
     drawn on the box's rectangular edges before clipping, so it would only
     line up with the hexagon's shape at a few points and look broken
     everywhere else. This padding-reveals-background trick sidesteps that:
     the same hexagon clip-path is applied to both this element and the img
     inside it (which sits flush, at 100% width/height), so the 1px this
     background peeks out around the image forms a solid hexagonal ring.
     The site's own main brand color (used on the landing page, nav pills,
     etc.) rather than this page's one-off green/gold hero theme, so the
     picker still reads as part of the same site everywhere else does. */
  padding: 1px;
  background: var(--color-brand-start);
  clip-path: var(--hex-clip);
  /* box-shadow would get clipped away along with the rest of this box's
     rectangular corners — clip-path cuts everything outside the hexagon,
     shadow included. filter: drop-shadow() isn't affected by clip-path (it
     shadows the element's actual rendered — already-clipped — silhouette),
     so it's the one that can give the hexagon a shadow at all. It's NOT
     the site's usual soft, large card shadow (--shadow-card-resting) —
     these hexagons deliberately touch/overlap their neighbors (the whole
     honeycomb effect), and a later sibling paints over an earlier one in
     their shared overlap area, hiding most of a soft wide shadow directly
     behind the very next hex, exactly where the card token would put most
     of its spread. A tight, high-contrast shadow (small blur, small
     offset) stays close enough to each hex's own silhouette to still read
     at the edges neighbors don't cover — the outer perimeter of the whole
     chain, and the thin slivers around each touching point. */
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.55));
  transition: background var(--transition-fast), transform var(--transition-fast), filter var(--transition-fast);
}

.game-picker-hex img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  clip-path: var(--hex-clip);
}

/* A light white wash over every hex by default, so the selected game's
   full-color artwork reads as clearly picked out from the rest by
   contrast — cleared on hover/focus too, so the artwork underneath is
   still visible while actually looking at a given hex. */
.game-picker-hex::after {
  content: '';
  position: absolute;
  inset: 0;
  clip-path: var(--hex-clip);
  background: rgba(255, 255, 255, 0.45);
  pointer-events: none;
  transition: background var(--transition-fast);
}

.game-picker-label {
  position: absolute;
  inset: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-4);
  clip-path: var(--hex-clip);
  background: rgba(0, 0, 0, 0.6);
  color: #ffffff;
  font-size: var(--font-size-small);
  font-weight: 700;
  line-height: 1.2;
  opacity: 0;
  transition: opacity var(--transition-fast);
}

.game-picker-item:hover .game-picker-hex,
.game-picker-item:focus-visible .game-picker-hex {
  transform: scale(1.08);
  filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.6));
}

.game-picker-item:hover .game-picker-hex::after,
.game-picker-item:focus-visible .game-picker-hex::after {
  background: transparent;
}

.game-picker-item:hover .game-picker-label,
.game-picker-item:focus-visible .game-picker-label {
  opacity: 1;
}

/* The brand gradient's other end — reads as clearly distinct from the
   lighter brand-start border above while staying within the same site-wide
   palette rather than introducing an unrelated color. */
.game-picker-item[aria-pressed="true"] .game-picker-hex::after {
  background: transparent;
}

.game-picker-item[aria-pressed="true"] .game-picker-hex {
  background: var(--color-brand-end);
}

.game-picker-item[aria-pressed="true"] .game-picker-label {
  opacity: 1;
  background: rgba(118, 75, 162, 0.6);
}

@media (max-width: 640px) {
  .game-showcase-highlight {
    flex-direction: column;
    align-items: stretch;
  }

  .game-showcase-media {
    flex-basis: auto;
  }

  /* Below this width the single-row honeycomb chain would run off-screen,
     so the same flat list of 9 buttons is rearranged into a compact
     3-column honeycomb block instead — no markup change needed.

     This used to lean on column-direction flex-wrap breaking every 3rd
     item automatically (via a height tuned to force that), but the exact
     break point depends on how flex folds each item's own margin into
     the wrap decision — margin-top on item 4 (for the middle column's
     drop) can also inflate what flex treats as "this column's height",
     nudging the wrap point off by a hair and throwing the whole cluster
     out of alignment. Explicit coordinates sidestep that ambiguity
     entirely: every item is `position: absolute` at a `left`/`top` value
     computed directly from the hexagon geometry, so there's no flow
     algorithm left to second-guess.

     Hex width = 40% of the container (3 columns overlapping by a quarter
     of a hex-width each need 2.5 hex-widths of total space, so
     100/2.5 = 40%), hex height = 40% * 0.8660254 ≈ 34.641% (a flat-top
     regular hexagon's height:width ratio). Column x-positions are 0%,
     30%, 60% (each 10% left of the next column's un-overlapped 40%-step
     position, i.e. a quarter-hex-width overlap). Column 2 additionally
     drops every row by half a hex-height (17.3205%) so it nests into the
     notches of columns 1 and 3. `.game-picker`'s own aspect-ratio both
     matches the resulting cluster height and gives these `top: %` values
     a definite container height to resolve against. */
  .game-picker {
    /* `100vw` (an earlier version of this rule) counts the scrollbar's own
       width, which isn't part of the actual visible/container width — it
       made the cluster overshoot past the content cards' right edge
       whenever a scrollbar was present (e.g. testing via a desktop
       browser's device toolbar). A % aspect-ratio-driven size avoids
       that: it's resolved against .game-picker's own rendered width,
       which — same as .content-card — is simply .container's actual
       width, scrollbar or not. */
    position: relative;
    display: block;
    aspect-ratio: 1 / 1.2124356;
    overflow: visible;
    padding: 0;
  }

  .game-picker-item {
    position: absolute;
    width: 40%;
    margin: 0 !important;
  }

  .game-picker-hex {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 0.8660254;
  }

  /* `top` resolves against the CONTAINER's HEIGHT, not its width — but the
     hex-height fractions above (34.641% etc.) were derived relative to the
     container's WIDTH (since that's the axis aspect-ratio computes from).
     Because .game-picker's own height is 1.2124356x its width, using those
     width-relative numbers directly as `top` overshot every row downward
     by that same factor, which is what was reading as "too spaced".
     Dividing by 1.2124356 (== multiplying by 1/3.5, since the container is
     exactly 3.5 hex-heights tall) converts each into the equivalent
     fraction of the container's actual height: hex-height alone is
     1/3.5 = 2/7 ≈ 28.5714% of that height, so the rows land at 0, 2/7,
     4/7, and the half-hex offsets at 1/7, 3/7, 5/7. `left` didn't need
     this conversion — it already resolves against width, which is the
     same axis those fractions were computed in. */
  .game-picker-item:nth-child(1) { left: 0%; top: 0%; }
  .game-picker-item:nth-child(2) { left: 0%; top: 28.5714%; }
  .game-picker-item:nth-child(3) { left: 0%; top: 57.1429%; }
  .game-picker-item:nth-child(4) { left: 30%; top: 14.2857%; }
  .game-picker-item:nth-child(5) { left: 30%; top: 42.8571%; }
  .game-picker-item:nth-child(6) { left: 30%; top: 71.4286%; }
  .game-picker-item:nth-child(7) { left: 60%; top: 0%; }
  .game-picker-item:nth-child(8) { left: 60%; top: 28.5714%; }
  .game-picker-item:nth-child(9) { left: 60%; top: 57.1429%; }
}

/* Landing-page identity block: the KnotFont "B" portrait mark, full name,
   and tagline — sits right under the sticky nav, before the first block,
   so a visitor gets "who this is" at a glance from the very top. */
.intro-identity {
  text-align: center;
  margin-bottom: var(--space-10);
}

.intro-face {
  display: block;
  font-size: 120px;
  color: #555555;
  margin-bottom: var(--space-4);
}

.intro-name {
  font-size: 1.7em;
  font-weight: 600;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: #4a4a4a;
  /* optically compensate the trailing letter-spacing gap so the name
     still reads as centered rather than shifted left */
  padding-left: 0.28em;
}

.intro-tagline {
  margin-top: var(--space-4);
  font-size: 1em;
  color: var(--color-text-faint);
}

/* Block/section headings that group cards on the landing page. */
/* Each block is its own visible panel (tinted, rounded, bordered) so the
   cards inside visibly belong together, distinct from both the page
   background and the plain-white cards nested inside it. */
.card-block {
  background: var(--color-badge-bg);
  border: 1px solid var(--color-badge-border);
  border-radius: var(--radius-lg);
  padding: var(--space-9) var(--space-8);
  box-shadow: 0 4px 20px rgba(30, 20, 60, 0.06);
}

/* Now a sibling that sits BEFORE its .card-block panel (not nested inside
   it), so the section label reads as introducing the panel rather than
   being trapped in it. Top margin gives it room to breathe from whatever
   came before (another section's panel, now that the block-dividers that
   used to separate sections were removed from every page but the
   landing one); bottom margin does the same before its own panel below. */
.card-block-title {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  font-size: var(--font-size-section-title);
  font-weight: 700;
  text-align: center;
  margin: var(--space-10) 0 var(--space-8);
}

/* On a page with no pill-nav above it (nothing to "breathe" away from —
   see the comment above), the top margin is just dead space between the
   hero and the title, so drop it when the title opens the page outright. */
.container > .card-block-title:first-child {
  margin-top: 0;
}

.card-block-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--gradient-brand);
  color: #ffffff;
  flex-shrink: 0;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  appearance: none;
  -webkit-appearance: none;
}

.card-block-icon svg {
  width: 18px;
  height: 18px;
}

/* Main (full-time) employers on the Work page swap the generic briefcase
   glyph for the company's own logo — a plain white disc rather than the
   brand-gradient fill, since a logo brings its own colors and a purple
   background would fight with them; part-time jobs (several employers,
   no single badge to show) keep the generic icon.

   It's a <button>, not a <span>: hovering (or focusing, or tapping) it
   reveals a much bigger version of the same logo in a popover centered
   on top of the badge, since the small badge alone is too small to make
   out some logos clearly. Hover/focus opens it with CSS alone; main.js
   also toggles an .is-open class on click so touch devices and keyboard
   users (Enter/Space activate a button natively, no extra JS needed for
   that part) get the same result, with outside-click/Escape closing it. */
.card-block-icon--logo {
  background: #ffffff;
  border: 1px solid var(--color-badge-border);
  padding: 3px;
  position: relative;
  cursor: pointer;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.card-block-icon--logo:hover,
.card-block-icon--logo:focus-visible {
  box-shadow: 0 2px 10px rgba(30, 20, 60, 0.18);
  transform: translateY(-1px);
}

.card-block-icon--logo > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 50%;
}

.logo-popover {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 140px;
  height: 140px;
  padding: var(--space-5);
  background: #ffffff;
  border: 1px solid var(--color-badge-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 12px 32px rgba(30, 20, 60, 0.22);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0.85);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s;
}

.logo-popover img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.card-block-icon--logo:hover .logo-popover,
.card-block-icon--logo:focus-visible .logo-popover,
.card-block-icon--logo.is-open .logo-popover {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
  transition: opacity 0.15s ease, transform 0.15s ease;
}

@media (prefers-reduced-motion: reduce) {
  .card-block-icon--logo,
  .logo-popover {
    transition: none;
  }
}

/* Decorative break between landing-page blocks — a clear "new section"
   cue now that every block shares the same panel color. */
.block-divider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  margin: var(--space-10) auto;
  max-width: 20em;
}

.block-divider-line {
  flex: 1;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--color-border-neutral) 50%, transparent);
}

.block-divider-line:first-child {
  background: linear-gradient(to right, transparent, var(--color-border-neutral));
}

.block-divider-line:last-child {
  background: linear-gradient(to left, transparent, var(--color-border-neutral));
}

.block-divider svg {
  width: 16px;
  height: 16px;
  color: var(--color-badge-text);
  flex-shrink: 0;
}

.card-subblock-title {
  font-size: 1em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-subtle);
  text-align: center;
  margin: var(--space-8) 0 var(--space-6);
}

.card-subblock-title:first-of-type {
  margin-top: 0;
}

/* Read-only prose sections (replaces the old site's `.field` blocks). */
.content-card {
  background: var(--color-card-bg);
  border-radius: var(--radius-lg);
  padding: var(--space-9);
  box-shadow: var(--shadow-card-resting);
  border-left: 4px solid var(--color-brand-start);
}

.content-card + .content-card {
  margin-top: var(--space-8);
}

/* Background page's Knot Gallery — each tile renders one diagram from the
   custom KnotFont (see base.css's .knot-glyph) with its Rolfsen table
   label underneath, echoing the old site's own knot gallery. */
/* 11 equal-width columns so 5 big knots per row (even columns 2/4/6/8/10)
   sit with a full knot-sized gap of white space between them AND at both
   row edges (odd columns 1/3/5/7/9/11) — plain `gap` alone can't give
   spacing that wide without also shrinking the knots themselves. */
.knot-gallery {
  display: grid;
  grid-template-columns: repeat(11, 1fr);
  gap: var(--space-8) 0;
  text-align: center;
}

.knot-gallery-item:nth-child(5n+1) { grid-column: 2; }
.knot-gallery-item:nth-child(5n+2) { grid-column: 4; }
.knot-gallery-item:nth-child(5n+3) { grid-column: 6; }
.knot-gallery-item:nth-child(5n+4) { grid-column: 8; }
.knot-gallery-item:nth-child(5n)   { grid-column: 10; }

.knot-gallery-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  /* Grid items stretch to fill their cell by default, which would center
     the tile's own content (glyph + label) off from its actual column. */
  justify-self: center;
  align-self: center;
  /* Lets the glyph render at its natural size without inflating its 1fr
     track (the default min-width: auto would otherwise force every column
     to grow to fit the widest glyph, breaking the equal 11-column split). */
  min-width: 0;
  /* The glyph is plain text, which would otherwise show the text-selection
     (I-beam) cursor on hover instead of the normal pointer arrow. */
  cursor: default;
}

.knot-gallery-glyph {
  font-size: 160px;
}

/* Each knot's color is a flat step along the brand blue→purple scale,
   positioned by its own crossing number (0 through 7) — 0 crossings is
   pure --color-brand-start, 7 crossings is pure --color-brand-end, and
   3/4/5/6 sit at their proportional point in between (crossings/7). */
.knot-gallery-glyph--c0 { color: #667eea; }
.knot-gallery-glyph--c3 { color: #6d68cb; }
.knot-gallery-glyph--c4 { color: #6f61c1; }
.knot-gallery-glyph--c5 { color: #715ab7; }
.knot-gallery-glyph--c6 { color: #7452ac; }
.knot-gallery-glyph--c7 { color: #764ba2; }

.knot-gallery-label {
  font-size: var(--font-size-small);
  color: var(--color-text-subtle);
}

/* Quick-facts grid (About page's who/when/where/what) — one small card per
   fact instead of a single card holding a bulleted list, each led by the
   same circular icon (.card-block-icon, reused from the section headers
   above) and a plain label instead of a pill badge. Four in a row on
   desktop; the Responsive section below drops it to a 2x2 grid on narrow
   screens. */
.fact-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-6);
}

.fact-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  background: var(--color-card-bg);
  border-radius: var(--radius-lg);
  padding: var(--space-7) var(--space-6);
  box-shadow: var(--shadow-card-resting);
  text-align: center;
}

/* Recolored to the About page's own hero theme (.hero--about's red/gold
   "flame" gradient) instead of the generic brand purple, tying these
   cards to the page they're actually on rather than the site-wide default. */
.fact-card .card-block-icon {
  background: linear-gradient(135deg, #aa151b, #f1bf00);
}

.fact-label {
  font-size: var(--font-size-badge);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-subtle);
}

.fact-card p {
  margin: var(--space-2) 0 0;
  color: var(--color-text);
}

/* The Japanese katakana reading under Adrián's name (bioWho) — 4px
   smaller than the line above it, like a parenthetical gloss rather than
   equal-weight text. calc() off 1em (not a fixed size) so it stays 4px
   below whatever .fact-card p's own size is. */
.bio-reading {
  font-size: calc(1em - 4px);
}

/* Part-time jobs: each entry's logo sits outside its .content-card (like
   the main employers' logo sitting in the heading above their card)
   rather than padded inside the card's own box — this wrapper just keeps
   an entry's logo+card together and gives consecutive entries the same
   spacing .content-card + .content-card gives the main employer cards. */
.part-entry + .part-entry {
  margin-top: var(--space-8);
}

.content-card h3 {
  font-size: var(--font-size-card-title);
  margin-bottom: var(--space-5);
}

/* Centered, like fonts.html's "Possibilities"/"What to Expect" and every
   other plain subheading a content-card carries — excluded here is
   .card-title, the game-showcase-info's own left-aligned title that sits
   beside its media, a distinct component rather than this generic case. */
.content-card h3:not(.card-title) {
  text-align: center;
}

.content-card p + p,
.content-card ul + p,
.content-card p + ul {
  margin-top: var(--space-5);
}

.content-card ul {
  padding-left: 1.2em;
}

.content-card li {
  margin-bottom: var(--space-4);
}

.content-card img.inline-logo {
  vertical-align: middle;
  height: 28px;
  width: auto;
  margin-right: var(--space-3);
}

.content-card iframe {
  width: 100%;
  border: none;
  border-radius: var(--radius-md);
}

/* ---------- Note callout (an aside sentence with a link, e.g. the Fez/
   Tunic font-download notes) ---------- */
/* .tag (below) is built for a short 2-4 word label — uppercase, tiny,
   bold — and looked cramped/dull stretched over a full sentence with an
   embedded link. This is sized like body text instead, with the same
   left-accent-border language .content-card uses for its own emphasis. */
.note-callout {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  background: var(--color-badge-bg);
  border-left: 4px solid var(--color-brand-start);
  border-radius: var(--radius-md);
  padding: var(--space-5) var(--space-6);
  color: var(--color-text);
  line-height: var(--line-height-body);
}

.note-callout-icon {
  flex-shrink: 0;
  width: 1.2em;
  height: 1.2em;
  margin-top: 0.15em;
  color: var(--color-brand-start);
}

.note-callout a {
  color: var(--color-brand-start);
  font-weight: 600;
  text-decoration: underline;
}

.note-callout a:hover {
  text-decoration: none;
}

/* ---------- Category badges (icon + label pill) ----------
   Originally the My Apps tool-card category pills (still used there, plus
   the filter chips built on the same look); reused wherever a short
   labeled fact benefits from an icon + pill treatment instead of plain
   text, e.g. the About page's who/when/where/what list. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: #eef0fc;
  color: #4c51bf;
  border: 1px solid rgba(102, 126, 234, 0.25);
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: 0.72em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.badge-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

/* ---------- Badges (small external/legacy tags) ---------- */
.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--overlay-black-06);
  color: var(--color-text-subtle);
  padding: 3px var(--space-5);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-badge);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  text-decoration: none;
}

/* Work page's employment date ranges — the same light-blue badge palette
   used site-wide (nav pills, the lasso home circle, ...), rather than the
   plain gray .tag gets by default, with the em dash kept a neutral gray
   (see .tag-emdash) instead of picking up the badge's blue text color. */
.tag--dates {
  background: var(--color-badge-bg);
  border: 1px solid var(--color-badge-border);
  color: var(--color-badge-text);
}

.tag--dates .tag-emdash {
  color: var(--color-text-subtle);
}

/* Job-title badge — a different (purple) hue from .tag--dates' blue so a
   role and its period read as two distinct kinds of information even
   when they sit right next to each other in the same row. */
.tag--role {
  background: var(--color-role-bg);
  border: 1px solid var(--color-role-border);
  color: var(--color-role-text);
}

/* Trips page map legend — one pastel tint per category, echoing that
   category's actual fill color on the map itself (red/orange/blueviolet/
   light blue) so the badge and the map read as the same color coding.
   Business and transit reuse .tag--role's purple and .tag--dates' blue
   rather than inventing new hues, since the map's blueviolet/lightblue
   are already close to those. */
.tag--residence {
  background: rgba(220, 38, 38, 0.1);
  border: 1px solid rgba(220, 38, 38, 0.3);
  color: #b91c1c;
}

.tag--visit {
  background: rgba(234, 88, 12, 0.1);
  border: 1px solid rgba(234, 88, 12, 0.3);
  color: #c2410c;
}

.tag--business {
  background: var(--color-role-bg);
  border: 1px solid var(--color-role-border);
  color: var(--color-role-text);
}

.tag--transit {
  background: var(--color-badge-bg);
  border: 1px solid var(--color-badge-border);
  color: var(--color-badge-text);
}

/* About page's press mentions: a fourth hue (emerald) for the language
   badge, since neither .tag--dates' blue (used for the year) nor
   .tag--role's purple (used for the outlet) was free to reuse here. */
.tag--language {
  background: rgba(5, 150, 105, 0.1);
  border: 1px solid rgba(5, 150, 105, 0.3);
  color: #047857;
}

/* About page's video entries: a fifth, neutral slate hue for the video's
   length — deliberately muted rather than another bright color, since
   it's supplementary metadata rather than a category like the others. */
.tag--length {
  background: rgba(71, 85, 105, 0.1);
  border: 1px solid rgba(71, 85, 105, 0.3);
  color: #334155;
}

/* Clickable reference badges — Background page's arXiv links, and Work
   page's Yahoo patent link. These are the only .tag variant that's
   actually a link, so besides its own color (arXiv's own brand red) it
   gets a hover/active highlight the plain tags don't need, to read as
   clickable rather than just another metadata pill. */
.tag--arxiv {
  background: rgba(179, 27, 27, 0.1);
  border: 1px solid rgba(179, 27, 27, 0.3);
  color: #b31b1b;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}

.tag--arxiv:hover,
.tag--arxiv:focus-visible {
  background: rgba(179, 27, 27, 0.22);
  border-color: rgba(179, 27, 27, 0.45);
}

.tag--arxiv:active {
  background: rgba(179, 27, 27, 0.32);
}

/* A row of standalone .tag badges (e.g. the individual years the Todai
   part-time lecturing spans) rather than one badge inline with text —
   sits between the card's logo image and its description, same position
   as the single .tag--dates badge in the main-employer sections above. */
.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: var(--space-5) 0;
}

/* Video entries (About page) — collapsed by default so four YouTube
   embeds don't all sit expanded on the page at once. Native <details>
   rather than a JS-driven toggle: no script needed, and modern browsers
   don't fetch a closed <details>'s iframe src until it's expanded, so
   this also defers loading every video until someone actually opens it. */
.video-entry {
  padding: var(--space-5) 0;
}

/* Each entry now lives in its own <li> (for the bullet marker), so
   :first-of-type would match every entry — it's the only .video-entry
   under its own <li> parent. "a preceding <li> sibling exists" is what
   actually means "not the first entry" here. */
li + li .video-entry {
  border-top: 1px solid var(--color-border-neutral);
}

.video-entry summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  list-style: none;
}

.video-entry summary::-webkit-details-marker {
  display: none;
}

.video-entry-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  color: var(--color-brand-start);
  font-size: var(--font-size-small);
  font-weight: 700;
}

.video-entry-caret {
  width: 14px;
  height: 14px;
  transition: transform var(--transition-fast);
}

.video-entry[open] .video-entry-caret {
  transform: rotate(180deg);
}

.video-entry-body {
  margin-top: var(--space-6);
}

.video-entry-body p {
  margin-top: var(--space-5);
}

/* An entry's optional caption line (e.g. the Ubisoft one's tournament
   result) — flex-basis: 100% pushes it onto its own row within the
   summary's wrapping flex layout without disturbing the badges/title/
   toggle row above it, and the left margin indents it to sit under the
   title rather than flush with the badges. */
.video-entry-subtitle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-basis: 100%;
  margin-left: calc(var(--space-3) * 3);
  color: var(--color-text-muted);
  font-size: var(--font-size-small);
}

.video-entry-subtitle-arrow {
  color: var(--color-text-faint);
}

.video-entry-subtitle-icon {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  color: #ca8a04;
}

/* Generic arrow-prefixed caption line for supplementary info under a
   badge+title list entry (Background page's Papers/Talks) — same visual
   idiom as .video-entry-subtitle above, but usable inside a plain <li>
   rather than a <details><summary>. */
.entry-note {
  display: block;
  margin-top: var(--space-2);
  margin-left: calc(var(--space-3) * 3);
  color: var(--color-text-muted);
  font-size: var(--font-size-small);
}

.entry-note-arrow {
  color: var(--color-text-faint);
  margin-right: var(--space-2);
}

/* Work page only: a thin secondary bar (thinner than the sticky
   .site-nav-bar above it, and not sticky itself — just a one-time key,
   not something worth re-showing on every scroll) explaining what each
   badge color on this page means. Swatches use each badge type's own
   *text* color as a solid fill rather than its pastel background, since
   the background alone is too faint to read as "a color" at this size. */
.badge-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  margin: 0 0 var(--space-9);
  background: var(--color-card-bg);
  border: 1px solid var(--color-border-neutral);
  border-radius: var(--radius-pill);
}

/* ---------- Forms (Contact page) ----------
   One centered card under a section heading, like every other page. Fields
   use the same pale tint as the site's inner tiles and turn white with the
   brand focus ring; labels echo the small uppercase captions of About's
   fact cards. */
.contact-card {
  max-width: 46em;
  margin: 0 auto;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: var(--space-8);
}

.form-field {
  margin-bottom: var(--space-7);
}

.form-field label {
  display: block;
  margin-bottom: var(--space-3);
  color: var(--color-text-muted);
  font-size: var(--font-size-small);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.form-field input,
.form-field textarea {
  width: 100%;
  font: inherit;
  padding: var(--space-6) var(--space-7);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: var(--color-page-bg);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
}

.form-field textarea {
  min-height: 11em;
  resize: vertical;
}

.form-field input::placeholder,
.form-field textarea::placeholder {
  color: var(--color-text-faint);
}

.form-field input:hover,
.form-field textarea:hover {
  border-color: var(--color-border-neutral);
}

.form-field input:focus,
.form-field textarea:focus {
  outline: none;
  background: #fff;
  border-color: var(--color-brand-start);
  box-shadow: var(--shadow-focus-ring);
}

.form-field input:user-invalid,
.form-field textarea:user-invalid {
  border-color: #dc2626;
}

.form-hint {
  margin-top: var(--space-3);
  color: var(--color-text-faint);
  font-size: var(--font-size-small);
}

.form-status {
  margin-top: var(--space-6);
  color: #b91c1c;
  font-size: var(--font-size-small);
  font-weight: 600;
  text-align: center;
}

.form-status[hidden] { display: none; }

.form-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--space-9);
}

.form-actions .btn-pill {
  padding: 14px 40px;
}

.form-actions .btn-pill svg {
  width: 18px;
  height: 18px;
  transition: transform var(--transition-fast);
}

.form-actions .btn-pill:hover svg {
  transform: translate(2px, -2px);
}

@media screen and (min-width: 769px) {
  .form-row { grid-template-columns: 1fr 1fr; }
}

/* Thank-you page: the same card as the contact form, narrower and centered. */
.thanks-card {
  max-width: 34em;
  margin: 0 auto;
  text-align: center;
}

.thanks-card p {
  font-size: 1.1em;
}

.thanks-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--space-8);
}

.thanks-actions .btn-pill {
  padding: 14px 40px;
}

.thanks-actions .btn-pill svg {
  width: 18px;
  height: 18px;
}

/* Topic picker: three pill options (same look as the site's pill navigation),
   built on real radio buttons that stay keyboard-focusable. */
.topic-picker {
  margin: 0 0 var(--space-8);
  padding: 0;
  border: none;
  text-align: center;
}

.topic-picker legend {
  width: 100%;
  margin-bottom: var(--space-5);
  padding: 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-small);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.topic-options {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
}

.topic-option {
  position: relative;
  cursor: pointer;
}

.topic-option input {
  position: absolute;
  opacity: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.topic-option span {
  display: inline-block;
  padding: 9px 20px;
  border: 1px solid var(--color-border-neutral);
  border-radius: var(--radius-pill);
  background: #fff;
  color: var(--color-text);
  font-size: 0.95em;
  font-weight: 600;
  transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.topic-option:hover span {
  border-color: var(--color-brand-start);
}

.topic-option input:checked + span {
  border-color: transparent;
  background: var(--gradient-brand);
  color: #fff;
}

.topic-option input:focus-visible + span {
  box-shadow: var(--shadow-focus-ring);
}

.form-field .optional {
  color: var(--color-text-faint);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

/* Name and email share the first row; the subject gets a row of its own. */
.field-subject {
  grid-column: 1 / -1;
}

.form-row[hidden],
.form-field[hidden] {
  display: none;
}

/* ---------- Call to action ----------
   "Want more?" + button, set directly on the page background (no card). */
.cta {
  text-align: center;
  margin: var(--space-12) 0 var(--space-6);
}

.cta h2 {
  margin-bottom: var(--space-6);
  font-size: 1.5em;
  font-weight: 700;
}

/* ---------- Footer ----------
   Same look as the footers of the My Apps tools: a thin divider across the
   content width, then a small dimmed "© year name" line with the name as a
   bold accent link (underlined only on hover). */
.site-footer {
  text-align: center;
  color: var(--color-text-faint);
  padding: var(--space-10) var(--space-8) var(--space-9);
}

.site-footer .container::before {
  content: '';
  display: block;
  margin-bottom: var(--space-9);
  border-top: 1px solid var(--color-border-neutral);
}

.site-footer .copyright {
  font-size: var(--font-size-small);
}

.site-footer .copyright a {
  color: var(--color-brand-start);
  font-weight: 600;
  text-decoration: none;
}

.site-footer .copyright a:hover {
  text-decoration: underline;
}

/* ---------- Scroll reveal ----------
   Progressive enhancement: reveal.js adds `has-reveal` to <html> at load
   time (before this rule can apply to any content), then flips each target
   to `.is-visible` as it enters the viewport. If JS never runs, the
   `has-reveal` scope is simply absent and every block/card renders normally
   — content is never hidden by CSS alone. */
.has-reveal .card-block,
.has-reveal .content-card {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.7s cubic-bezier(0.16, 0.84, 0.44, 1),
              transform 0.7s cubic-bezier(0.16, 0.84, 0.44, 1);
}

.has-reveal .card-block.is-visible,
.has-reveal .content-card.is-visible {
  opacity: 1;
  transform: none;
}

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

/* ---------- Responsive ---------- */
@media screen and (max-width: 768px) {
  .hero h1 {
    font-size: var(--font-size-hero-mobile);
  }

  .lang-toggle button {
    padding: 12px 18px;
  }

  .content-card,
  .card {
    padding: var(--space-7);
  }

  .fact-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .knot-gallery {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-6);
  }

  .knot-gallery-item {
    grid-column: auto;
  }

  .knot-gallery-glyph {
    font-size: 90px;
  }
}
