/* ==========================================================================
   Pol Balatbat — portfolio stylesheet

   One stylesheet, loaded once, content-hashed by the build so it can be
   cached immutably. It replaces the previous Tailwind Play CDN script, which
   compiled utility classes in the browser on every page load and required
   'unsafe-inline' and 'unsafe-eval' in any CSP that allowed it.

   Conventions
   -----------
   * Design tokens only on :root. One accent colour, used everywhere.
   * Fluid type and spacing via clamp(); explicit breakpoints only where a
     layout genuinely has to change.
   * Nothing is positioned absolutely unless it is decorative and cannot
     collide with text (the timeline rail, the hero glow, the caret).
   * Every interactive target is at least 44px on its short axis.
   * Motion is opt-out at the top of the file, once, for the whole site.

   Sections
   --------
   01 Tokens
   02 Reset and base
   03 Utilities
   04 Buttons, tags, panels
   05 Header and navigation
   06 Hero
   07 Sections and shared section furniture
   08 Background
   09 Competencies
   10 Projects
   11 Timeline
   12 Philosophy
   13 Contact
   14 Footer and update toast
   15 Document pages (privacy, 404, offline)
   16 Reveal animation and reduced motion
   ========================================================================== */

/* ==========================================================================
   01 Tokens
   ========================================================================== */

:root {
  color-scheme: dark;

  /* Surfaces — strict dark, solid black page with near-black panels. */
  --bg: #000000;
  --surface: #0a0a0b;
  --surface-raised: #101013;
  --line: rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.18);

  /* Text — white for primary, two greys for support.
     Contrast on #000: #ffffff 21:1, #b5b8be 9.4:1, #8c8f96 6.1:1. */
  --text: #ffffff;
  --text-secondary: #b5b8be;
  --text-muted: #8c8f96;

  /* The single accent. #8ab4f8 on black is 9.6:1, so it is safe for body
     text, small text and focus rings without a second "accessible" variant. */
  --accent: #8ab4f8;
  --accent-ink: #04101f;
  --accent-soft: rgba(138, 180, 248, 0.1);
  --accent-line: rgba(138, 180, 248, 0.32);

  --danger: #f08a7c;

  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "SF Mono", Menlo,
    Consolas, monospace;

  /* Type scale. Each step is clamped so it stops growing on ultra-wide
     displays instead of turning into a billboard. */
  --fs-xs: clamp(0.75rem, 0.735rem + 0.08vw, 0.8125rem);
  --fs-sm: clamp(0.875rem, 0.855rem + 0.1vw, 0.9375rem);
  --fs-base: clamp(1rem, 0.975rem + 0.12vw, 1.0625rem);
  --fs-md: clamp(1.0625rem, 1.01rem + 0.25vw, 1.1875rem);
  --fs-lg: clamp(1.1875rem, 1.11rem + 0.38vw, 1.375rem);
  --fs-xl: clamp(1.5rem, 1.33rem + 0.85vw, 2.125rem);
  --fs-2xl: clamp(1.875rem, 1.55rem + 1.6vw, 2.75rem);
  --fs-hero: clamp(2.5rem, 1.45rem + 5.2vw, 5.5rem);
  --fs-role: clamp(1.375rem, 0.95rem + 2.1vw, 2.5rem);

  /* Spacing scale. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: clamp(3rem, 7vw, 5.5rem);
  --space-9: clamp(4rem, 10vw, 8rem);

  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 20px;
  --radius-pill: 999px;

  --shell-max: 1240px;
  --shell-narrow: 860px;
  --shell-pad: clamp(1.25rem, 4vw, 3rem);

  /* Header height is a token because scroll-padding, the hero height and the
     mobile drawer offset all have to agree with it. */
  --header-h: 60px;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --t-fast: 150ms;
  --t: 260ms;
  --t-slow: 620ms;
}

@media (min-width: 60em) {
  :root {
    --header-h: 72px;
  }
}

/* ==========================================================================
   02 Reset and base
   ========================================================================== */

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

/* Grid and flex children default to min-width:auto, which is the single most
   common cause of horizontal overflow from long unbroken strings. */
* {
  min-width: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor targets clear the fixed header instead of hiding behind it. */
  scroll-padding-top: calc(var(--header-h) + var(--space-5));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: 1.65;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* clip rather than hidden: it contains stray decorative overflow without
     breaking position: sticky on descendants. */
  overflow-x: clip;
}

h1,
h2,
h3,
h4 {
  margin: 0;
  color: var(--text);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

ul,
ol,
dl,
dd,
dt {
  margin: 0;
  padding: 0;
}

ul,
ol {
  list-style: none;
}

a {
  color: var(--accent);
  text-decoration: none;
  text-underline-offset: 0.2em;
}

a:hover {
  text-decoration: underline;
}

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

button,
input,
textarea {
  font: inherit;
  color: inherit;
}

[hidden] {
  display: none !important;
}

::selection {
  background: var(--accent);
  color: var(--accent-ink);
}

/* One focus treatment for the whole site. :focus-visible keeps it off mouse
   clicks while guaranteeing it for keyboard users. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}

/* Firefox still needs the scrollbar styled separately from WebKit. */
html {
  scrollbar-color: #2a2a2e var(--bg);
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: var(--bg);
}

::-webkit-scrollbar-thumb {
  background: #2a2a2e;
  border-radius: var(--radius-pill);
  border: 2px solid var(--bg);
}

::-webkit-scrollbar-thumb:hover {
  background: #3c3c42;
}

/* ==========================================================================
   03 Utilities
   ========================================================================== */

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

.shell--narrow {
  max-width: var(--shell-narrow);
}

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

.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  border-radius: var(--radius-sm);
  transform: translateY(-200%);
  transition: transform var(--t-fast) var(--ease);
}

/* :focus, not :focus-visible. The skip link exists for anyone who reaches it
   by any means, including programmatic focus and assistive technology that
   does not register as a keyboard interaction. */
.skip-link:focus {
  transform: translateY(0);
  outline-offset: 2px;
}

.icon {
  width: 1.375rem;
  height: 1.375rem;
  flex: none;
}

.icon--sm {
  width: 1.0625rem;
  height: 1.0625rem;
}

.icon--xs {
  width: 0.875rem;
  height: 0.875rem;
}

.icon--xl {
  width: 3rem;
  height: 3rem;
  margin-inline: auto;
  color: var(--accent);
}

.icon--flip {
  transform: rotate(180deg);
}

.link {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  color: var(--accent);
  font-weight: 500;
}

/* ==========================================================================
   04 Buttons, tags, panels
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  min-height: 44px;
  padding: 0.6875rem 1.25rem;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-align: center;
  cursor: pointer;
  transition: background-color var(--t) var(--ease), border-color var(--t) var(--ease),
    color var(--t) var(--ease), transform var(--t) var(--ease);
}

.btn:hover {
  text-decoration: none;
}

.btn--primary {
  background: var(--text);
  border-color: var(--text);
  color: #000;
}

.btn--primary:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.btn--ghost {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--text);
}

.btn--ghost:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* Project CTAs. Visually lighter than the hero buttons so a row of four of
   them does not read as four primary actions on the page. */
.btn--project {
  min-height: 42px;
  padding: 0.5625rem 1rem;
}

.btn--secondary {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--text-secondary);
}

.btn--secondary:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* Request-access CTAs are deliberately a different shape from "open this
   thing" buttons: dashed border, accent text, no fill. The visitor should
   be able to tell at a glance that one opens an application and the other
   starts a conversation. */
.btn--access {
  background: var(--accent-soft);
  border-style: dashed;
  border-color: var(--accent-line);
  color: var(--accent);
}

.btn--access:hover {
  background: var(--accent);
  border-style: solid;
  border-color: var(--accent);
  color: var(--accent-ink);
}

.btn--small {
  min-height: 38px;
  padding: 0.4375rem 0.875rem;
  font-size: var(--fs-xs);
}

.btn--block {
  width: 100%;
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.tag {
  padding: 0.3125rem 0.6875rem;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  line-height: 1.4;
}

.tags--mono .tag {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: -0.01em;
  color: var(--text-muted);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.375em;
  align-self: flex-start;
  padding: 0.3125rem 0.6875rem;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-pill);
  color: var(--accent);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.3;
}

/* Three deployment states, three readings. Colour is never the only signal:
   each badge also carries its own icon and its own words. */
.badge--live {
  background: rgba(126, 212, 160, 0.1);
  border-color: rgba(126, 212, 160, 0.34);
  color: #7ed4a0;
}

.badge--staging {
  background: rgba(230, 193, 120, 0.1);
  border-color: rgba(230, 193, 120, 0.34);
  color: #e6c178;
}

.badge--private {
  background: rgba(255, 255, 255, 0.05);
  border-color: var(--line-strong);
  color: var(--text-secondary);
}

/* ---------------------------------------------------------------------- *
 * Social / profile links
 *
 * Icon plus label. The label is clipped rather than removed below the
 * narrow breakpoint, so the accessible name is always present even when
 * only the icon is drawn.
 * ---------------------------------------------------------------------- */

.social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  min-width: 44px;
  min-height: 44px;
  padding: 0.5rem 0.9375rem;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
  transition: border-color var(--t) var(--ease), color var(--t) var(--ease),
    background-color var(--t) var(--ease);
}

.social:hover,
.social:focus-visible {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
  text-decoration: none;
}

@media (max-width: 29.999em) {
  .social {
    padding: 0.5rem;
  }

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

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.25rem, 3vw, 2rem);
}

.bullets {
  display: grid;
  gap: 0.625rem;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.bullets > li {
  position: relative;
  padding-left: 1.125rem;
}

/* A 3px square rather than a bullet glyph: it aligns with the first text line
   at every font size instead of drifting. */
.bullets > li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 4px;
  height: 4px;
  background: var(--accent);
  border-radius: 1px;
}

/* ==========================================================================
   05 Header and navigation
   ========================================================================== */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  background: rgba(0, 0, 0, 0.78);
  border-bottom: 1px solid var(--line);
  /* A small blur only — enough to keep text legible over content scrolling
     beneath, without the frosted-glass look. */
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--header-h);
}

/* min-width as well as min-height: below 24em the name is hidden and only
   the 32px mark is drawn, which would leave a 32px-wide tap target. */
.brand {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.625rem;
  min-width: 44px;
  min-height: 44px;
  color: var(--text);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.brand:hover {
  text-decoration: none;
  color: var(--accent);
}

.brand__mark {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0;
}

.brand__name {
  font-size: var(--fs-sm);
}

/* The name is redundant next to the mark on the narrowest phones. */
@media (max-width: 24em) {
  .brand__name {
    display: none;
  }
}

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(0.25rem, 1.2vw, 1rem);
}

.nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.5rem;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}

.nav__link:hover,
.nav__link[aria-current="true"] {
  color: var(--text);
  text-decoration: none;
}

.nav__link[aria-current="true"] {
  color: var(--accent);
}

.nav--desktop {
  display: none;
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0 0.75rem;
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  color: var(--text);
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
}

.nav-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.nav-toggle__icon {
  display: grid;
}

/* Both glyphs occupy the same cell, so swapping them cannot resize the
   button or shift the header. */
.nav-toggle__icon > .icon {
  grid-area: 1 / 1;
}

.nav-toggle__icon > .icon--close,
.nav-toggle[aria-expanded="true"] .nav-toggle__icon > .icon:not(.icon--close) {
  visibility: hidden;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__icon > .icon--close {
  visibility: visible;
}

.nav--mobile {
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

.nav--mobile .nav__list {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  padding: var(--space-2) var(--shell-pad) var(--space-4);
}

.nav--mobile .nav__link {
  justify-content: space-between;
  padding: 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
  min-height: 52px;
}

.nav--mobile .nav__list > li:last-child .nav__link {
  border-bottom: 0;
}

@media (min-width: 56em) {
  .nav--desktop {
    display: block;
  }

  .nav-toggle,
  .nav--mobile {
    display: none !important;
  }
}

/* ==========================================================================
   06 Hero

   The typed line is the one piece of this page that could cause layout
   shift, so the roles box is a single-cell grid containing an invisible
   sizer set to the longest role. The animated line and the static list are
   stacked in the same cell; neither can grow the box, and the box exists at
   full size from the first paint.
   ========================================================================== */

.hero {
  position: relative;
  display: flex;
  align-items: center;
  min-height: calc(100svh - var(--header-h));
  padding-top: calc(var(--header-h) + clamp(2.5rem, 8vh, 5rem));
  padding-bottom: clamp(3.5rem, 12vh, 7rem);
  overflow: clip;
}

/* One restrained radial wash. Low enough opacity that it reads as depth
   rather than as a glow effect, and it sits behind everything. */
.hero::before {
  content: "";
  position: absolute;
  top: -28%;
  left: 50%;
  z-index: 0;
  width: min(140vw, 1100px);
  aspect-ratio: 1;
  transform: translateX(-50%);
  background: radial-gradient(circle, rgba(138, 180, 248, 0.1), transparent 62%);
  pointer-events: none;
}

.hero__inner {
  position: relative;
  z-index: 1;
}

.hero__eyebrow {
  margin-bottom: var(--space-5);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
  line-height: 1.7;
}

.hero__name {
  font-size: var(--fs-hero);
  font-weight: 650;
  letter-spacing: -0.035em;
  line-height: 0.98;
}

.hero__roles {
  display: grid;
  margin-top: clamp(0.75rem, 2vw, 1.25rem);
}

.hero__roles > * {
  grid-area: 1 / 1;
  font-size: var(--fs-role);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.02em;
}

/* Reserves the box. Not display:none — it has to be laid out to have size. */
.hero__roles-sizer {
  visibility: hidden;
  pointer-events: none;
  user-select: none;
}

.hero__roles-list {
  color: var(--text-secondary);
}

/* While the animation runs, the static list stays in the accessibility tree
   (it is the accessible version of the typed text) but stops contributing to
   the grid's size, leaving the sizer in charge. */
.hero__roles.is-animating .hero__roles-list {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.hero__roles-typed {
  display: flex;
  align-items: baseline;
  gap: 0.08em;
  color: var(--text);
  font-weight: 500;
}

.hero__roles-typed .typed {
  color: var(--accent);
}

.caret {
  display: inline-block;
  width: 2px;
  height: 1.05em;
  flex: none;
  background: var(--accent);
  transform: translateY(0.12em);
  animation: caret-blink 1.05s steps(1, end) infinite;
}

@keyframes caret-blink {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

.hero__positioning {
  max-width: 58ch;
  margin-top: clamp(1.25rem, 3vw, 2rem);
  color: var(--text-secondary);
  font-size: var(--fs-md);
  line-height: 1.7;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: clamp(1.75rem, 4vw, 2.75rem);
}

.hero__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* ------------------------------------------------------------------------
   Signature visual: the topology strip.

   One visual system rather than several unrelated effects. It states the
   shape of the work — Leadership through to AI / Agentic — and a signal
   travels along the connectors between the stages.

   It is built from real elements, not a scaled SVG, so it reflows: a
   vertical chain on a phone, a horizontal one from 48em up, with the labels
   at a readable size at every width. The animation is decorative only: with
   it stopped the chain still reads correctly, which is what makes it safe
   to disable under prefers-reduced-motion, off-screen and on a hidden tab.
   ------------------------------------------------------------------------ */

.topology {
  margin-top: clamp(2.25rem, 6vw, 3.5rem);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}

.topology__caption {
  margin-bottom: var(--space-4);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.topology__chain {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
}

.topology__node {
  display: flex;
  align-items: center;
  gap: 0.625rem;
}

.topology__dot {
  width: 7px;
  height: 7px;
  flex: none;
  background: var(--accent);
  border-radius: 50%;
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.topology__label {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* The connector between two stages, and the travelling signal on it. The
   line is the background colour; the signal is a gradient moved across it,
   so nothing is added to the DOM and nothing is animated that would cost a
   layout or a paint of the text. */
.topology__node + .topology__node::before {
  content: "";
  display: block;
  flex: none;
  width: 1px;
  height: 1.375rem;
  margin: 0.25rem 0 0.25rem 3px;
  background-color: var(--line-strong);
  background-image: linear-gradient(180deg, transparent, var(--accent), transparent);
  background-repeat: no-repeat;
  background-size: 100% 55%;
  background-position: 0 -60%;
  animation: topo-pulse-y 3.2s var(--ease) infinite;
}

@media (min-width: 48em) {
  .topology__chain {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    row-gap: var(--space-3);
  }

  .topology__node + .topology__node::before {
    width: clamp(1.5rem, 3.4vw, 3.25rem);
    height: 1px;
    margin: 0 var(--space-4);
    background-image: linear-gradient(90deg, transparent, var(--accent), transparent);
    background-size: 55% 100%;
    background-position: -60% 0;
    animation-name: topo-pulse-x;
  }
}

@keyframes topo-pulse-y {
  0% {
    background-position: 0 -60%;
  }
  55%,
  100% {
    background-position: 0 160%;
  }
}

@keyframes topo-pulse-x {
  0% {
    background-position: -60% 0;
  }
  55%,
  100% {
    background-position: 160% 0;
  }
}

/* Staggered, so the signal reads as one pulse travelling down the chain
   rather than four connectors blinking together. */
.topology__node--2::before {
  animation-delay: 0s;
}
.topology__node--3::before {
  animation-delay: 0.28s;
}
.topology__node--4::before {
  animation-delay: 0.56s;
}
.topology__node--5::before {
  animation-delay: 0.84s;
}

/* Set by main.js when the hero scrolls out of view or the tab is hidden.
   Decorative motion should not run where nobody is looking at it. */
.topology.is-paused .topology__node::before {
  animation-play-state: paused;
}

.hero__scroll {
  position: absolute;
  bottom: clamp(1rem, 3vh, 2rem);
  left: 50%;
  display: none;
  place-items: center;
  width: 44px;
  height: 44px;
  transform: translateX(-50%);
  color: var(--text-muted);
}

.hero__scroll-icon {
  transform: rotate(90deg);
}

.hero__scroll:hover {
  color: var(--accent);
}

@media (min-height: 44em) and (min-width: 48em) {
  .hero__scroll {
    display: grid;
  }
}

/* ==========================================================================
   07 Sections and shared furniture
   ========================================================================== */

.section {
  padding-block: var(--space-9);
  /* Belt-and-braces against a fixed header overlapping a deep link. */
  scroll-margin-top: var(--header-h);
}

.section--alt {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.016), rgba(255, 255, 255, 0.016));
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.section__header {
  max-width: 62ch;
  margin-bottom: var(--space-8);
}

.section__eyebrow {
  margin-bottom: var(--space-3);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.section__title {
  font-size: var(--fs-2xl);
  letter-spacing: -0.03em;
}

.section__lead {
  margin-top: var(--space-4);
  color: var(--text-muted);
  font-size: var(--fs-md);
}

/* ==========================================================================
   08 Background
   ========================================================================== */

.background__grid {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 64em) {
  .background__grid {
    grid-template-columns: minmax(0, 1.65fr) minmax(16rem, 1fr);
    gap: var(--space-7);
    align-items: start;
  }
}

.background__prose {
  display: grid;
  gap: var(--space-5);
  max-width: 68ch;
  font-size: var(--fs-md);
  line-height: 1.75;
}

.background__closing {
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
  color: var(--text);
}

.background__facts {
  position: sticky;
  top: calc(var(--header-h) + var(--space-5));
}

.facts {
  display: grid;
  gap: var(--space-5);
  padding: clamp(1.25rem, 3vw, 1.75rem);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.facts__item + .facts__item {
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}

.facts__value {
  color: var(--text);
  font-size: var(--fs-lg);
  font-weight: 600;
  letter-spacing: -0.02em;
}

.facts__label {
  margin-top: 0.125rem;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* Sticky only helps when the column is actually shorter than the prose. */
@media (max-width: 63.999em) {
  .background__facts {
    position: static;
  }

  .facts {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
    gap: var(--space-4) var(--space-6);
  }

  .facts__item + .facts__item {
    padding-top: 0;
    border-top: 0;
  }
}

/* ==========================================================================
   09 Competencies
   ========================================================================== */

.competencies {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
}

.competency {
  display: grid;
  gap: var(--space-4);
  align-content: start;
  transition: border-color var(--t) var(--ease);
}

.competency:hover {
  border-color: var(--line-strong);
}

.competency__title {
  display: flex;
  align-items: center;
  gap: 0.6875rem;
  font-size: var(--fs-lg);
  letter-spacing: -0.015em;
}

.competency__icon {
  display: grid;
  place-items: center;
  width: 2.375rem;
  height: 2.375rem;
  flex: none;
  background: var(--accent-soft);
  border-radius: var(--radius-sm);
  color: var(--accent);
}

/* ==========================================================================
   10 Projects

   Each project is one panel carrying a thumbnail, the technical summary and
   its CTA row at the top, then the long-form detail below. Nothing is hidden
   behind hover: hover and focus only change the border and lift the image
   slightly, so everything a reader needs is present on a touch device and in
   a screenshot.
   ========================================================================== */

.projects {
  display: grid;
  gap: var(--space-6);
}

.project {
  display: grid;
  gap: var(--space-5);
  position: relative;
  transition: border-color var(--t) var(--ease);
}

/* An accent line that grows along the top edge on hover or keyboard focus.
   Decorative: it duplicates the border-colour change rather than carrying
   information of its own. */
.project::after {
  content: "";
  position: absolute;
  inset: -1px auto auto -1px;
  width: 0;
  height: 2px;
  max-width: calc(100% + 2px);
  background: linear-gradient(90deg, var(--accent), transparent);
  border-radius: var(--radius) 0 0 0;
  transition: width var(--t-slow) var(--ease);
  pointer-events: none;
}

.project:hover,
.project:focus-within {
  border-color: var(--line-strong);
}

.project:hover::after,
.project:focus-within::after {
  width: 42%;
}

/* --- top block: thumbnail beside the technical summary ------------------ */

.project__layout {
  display: grid;
  gap: var(--space-5);
}

@media (min-width: 64em) {
  .project__layout {
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    gap: var(--space-6);
    align-items: start;
  }
}

.project__media {
  position: relative;
  margin: 0;
  display: grid;
  gap: var(--space-2);
}

.project__image {
  display: block;
  width: 100%;
  height: auto;
  /* Matches the intrinsic 1200x750, so the box exists before the bytes
     arrive and the image cannot shift the page as it decodes. */
  aspect-ratio: 16 / 10;
  object-fit: cover;
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  transition: transform var(--t-slow) var(--ease), border-color var(--t) var(--ease);
}

.project:hover .project__image,
.project:focus-within .project__image {
  border-color: var(--accent-line);
  transform: scale(1.012);
}

.project__caption {
  color: var(--text-muted);
  font-size: var(--fs-xs);
  line-height: 1.5;
}

.project__body {
  display: grid;
  gap: var(--space-4);
  align-content: start;
}

.project__head {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.project__icon {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  color: var(--accent);
}

.project__headings {
  flex: 1 1 12rem;
}

.project__number {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.project__title {
  margin-top: 0.1875rem;
  font-size: var(--fs-xl);
  letter-spacing: -0.03em;
}

.project__type {
  margin-top: 0.25rem;
  color: var(--accent);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

.project__summary {
  max-width: 76ch;
  color: var(--text-secondary);
  font-size: var(--fs-md);
  line-height: 1.7;
}

/* --- languages and stack ------------------------------------------------ */

.project__tech {
  display: grid;
  gap: var(--space-4);
}

.techrow {
  display: grid;
  gap: var(--space-2);
}

.techrow__label {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Languages are the headline fact, so they are the accent row; the stack is
   longer and stays quiet underneath it. */
.techrow--languages .tag {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
  font-weight: 600;
}

/* --- CTA row ------------------------------------------------------------ */

.project__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  padding-top: var(--space-1);
}

.project__action {
  display: grid;
  gap: 0.3125rem;
  justify-items: start;
}

.project__action-note {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
  padding-inline: 0.25rem;
}

/* --- long-form detail --------------------------------------------------- */

.project__meta {
  display: grid;
  gap: var(--space-4);
  padding: var(--space-5) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

@media (min-width: 48em) {
  .project__meta {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-5) var(--space-6);
  }

  .project__meta-item--wide {
    grid-column: 1 / -1;
  }
}

.project__meta dt {
  margin-bottom: 0.25rem;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.project__meta dd {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  line-height: 1.7;
}

.project__columns {
  display: grid;
  gap: var(--space-5);
}

@media (min-width: 48em) {
  .project__columns {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
    gap: var(--space-6);
  }
}

.project__subtitle {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  margin-bottom: var(--space-3);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0;
}

.project__subtitle-icon {
  color: var(--accent);
}

/* ==========================================================================
   11 Timeline
   ========================================================================== */

.timeline {
  position: relative;
  display: grid;
  gap: var(--space-7);
}

/* The rail is inset from the dots' centre line and stops short at both ends
   so it never appears to run past the first and last entries. */
.timeline::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 0.75rem;
  bottom: 0.75rem;
  width: 1px;
  background: var(--line);
}

.timeline__item {
  position: relative;
  padding-left: clamp(1.75rem, 4vw, 2.5rem);
}

/* Progress rail. The grey rail above is the full track; this one is the
   accent overlay, scaled from 0 to 1 as the timeline is revealed, so the
   track appears to fill as the reader moves down it. transform-only, so it
   costs a composite and never a layout. */
.timeline::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 0.75rem;
  bottom: 0.75rem;
  width: 1px;
  background: linear-gradient(180deg, var(--accent), var(--accent-line));
  transform-origin: top;
  transform: scaleY(0);
  transition: transform 1.4s var(--ease);
}

.timeline.is-visible::after {
  transform: scaleY(1);
}

.timeline__dot {
  position: absolute;
  left: 0;
  top: 0.5rem;
  width: 11px;
  height: 11px;
  border: 2px solid var(--bg);
  border-radius: 50%;
  background: #3a3a40;
  transition: background-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}

/* Each entry's dot lights as that entry reveals, a beat behind the rail. */
.timeline__item.is-visible .timeline__dot {
  background: var(--accent-line);
}

.timeline__item--current .timeline__dot,
.timeline__item--current.is-visible .timeline__dot {
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.timeline__period {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.timeline__item--current .timeline__period {
  color: var(--accent);
}

.timeline__title {
  margin-top: var(--space-2);
  font-size: var(--fs-lg);
}

.timeline__place {
  margin-top: 0.125rem;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.timeline__description {
  margin-top: var(--space-3);
  max-width: 68ch;
  font-size: var(--fs-sm);
  line-height: 1.75;
}

/* ==========================================================================
   12 Philosophy
   ========================================================================== */

.philosophy {
  display: grid;
  gap: var(--space-6) var(--space-7);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

.philosophy__item {
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-strong);
}

.philosophy__title {
  font-size: var(--fs-md);
  letter-spacing: -0.01em;
}

.philosophy__body {
  margin-top: var(--space-3);
  color: var(--text-muted);
  font-size: var(--fs-sm);
  line-height: 1.75;
}

/* ==========================================================================
   13 Contact

   Direct channels only. The contact form, its endpoint and its D1 table were
   removed in STEP 0002: it stored messages and notified nobody, so a visitor
   could send one and wait indefinitely. Every item below is a plain anchor
   that works with JavaScript off, and the phone number and address are
   rendered as selectable text rather than hidden behind link text.
   ========================================================================== */

.contact__list {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.contact__card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  height: 100%;
  min-height: 72px;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text);
  transition: border-color var(--t) var(--ease), background-color var(--t) var(--ease),
    transform var(--t) var(--ease);
}

.contact__card:hover,
.contact__card:focus-visible {
  background: var(--surface-raised);
  border-color: var(--accent-line);
  text-decoration: none;
}

.contact__icon {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  color: var(--accent);
}

.contact__text {
  display: grid;
  gap: 0.125rem;
  min-width: 0;
}

.contact__label {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.contact__value {
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 500;
  overflow-wrap: anywhere;
}

/* Points right; slides a little on hover to suggest the handoff. */
.contact__chevron {
  margin-left: auto;
  flex: none;
  color: var(--text-muted);
  transition: transform var(--t) var(--ease), color var(--t) var(--ease);
}

.contact__card:hover .contact__chevron,
.contact__card:focus-visible .contact__chevron {
  color: var(--accent);
  transform: translateX(3px);
}

.contact__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-5);
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}

.contact__note {
  flex: 1 1 18rem;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  line-height: 1.6;
}

/* ==========================================================================
   14 Footer and update toast
   ========================================================================== */

.site-footer {
  padding-block: var(--space-6);
  border-top: 1px solid var(--line);
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
}

.site-footer__name {
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.site-footer__meta {
  color: var(--text-muted);
  font-size: var(--fs-xs);
}

.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
}

.footer__link {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 44px;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.footer__link:hover {
  color: var(--accent);
  text-decoration: none;
}

.update-toast {
  position: fixed;
  left: 50%;
  bottom: max(1rem, env(safe-area-inset-bottom));
  z-index: 60;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  width: calc(100% - 2rem);
  max-width: 27rem;
  padding: var(--space-3) var(--space-4);
  transform: translateX(-50%);
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: 0 18px 40px -20px rgba(0, 0, 0, 0.9);
}

.update-toast__text {
  flex: 1 1 12rem;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

/* The reload button inherited the body colour against the toast's own
   surface, which measured 2.68:1 — below AA for its 13px text. It is the
   only control in the toast, so it gets the primary treatment. */
.update-toast .btn {
  background: var(--text);
  border-color: var(--text);
  color: #000;
}

.update-toast .btn:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* ==========================================================================
   15 Document pages
   ========================================================================== */

.page--document {
  display: flex;
  flex-direction: column;
  min-height: 100svh;
}

.site-header--simple {
  position: static;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.document {
  flex: 1;
  padding-block: var(--space-8);
}

.document--center {
  display: grid;
  place-items: center;
  text-align: center;
}

.document__title {
  font-size: var(--fs-2xl);
}

.document__meta {
  margin-top: var(--space-3);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.document__lead {
  margin-top: var(--space-5);
  max-width: 68ch;
  color: var(--text-secondary);
  font-size: var(--fs-md);
  line-height: 1.75;
}

.document--center .document__lead {
  margin-inline: auto;
  margin-bottom: var(--space-6);
}

.document__section {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
  max-width: 72ch;
}

.document__section h2 {
  font-size: var(--fs-lg);
}

.document__section p {
  margin-top: var(--space-3);
  font-size: var(--fs-sm);
  line-height: 1.8;
}

.offline__icon {
  margin-bottom: var(--space-5);
}

/* ==========================================================================
   16 Reveal animation and reduced motion

   .reveal elements are visible by default so the page is fully readable with
   JavaScript disabled or still loading. main.js adds .js to <html> only after
   marking everything already in the viewport as visible, so nothing that has
   been painted is ever hidden again.
   ========================================================================== */

.js .reveal:not(.is-visible) {
  opacity: 0;
  transform: translateY(14px);
}

.js .reveal {
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}

/* Stagger inside a grid, done with nth-child rather than per-item classes
   or inline styles: the cards in one row would otherwise all arrive on the
   same frame, which reads as a flash rather than as a sequence. Capped at
   four steps so a long list never ends up waiting half a second. */
.js .competencies > .reveal:nth-child(2),
.js .philosophy > .reveal:nth-child(2),
.js .projects > .reveal:nth-child(2) {
  transition-delay: 70ms;
}

.js .competencies > .reveal:nth-child(3),
.js .philosophy > .reveal:nth-child(3),
.js .projects > .reveal:nth-child(3) {
  transition-delay: 140ms;
}

.js .competencies > .reveal:nth-child(4),
.js .philosophy > .reveal:nth-child(4),
.js .projects > .reveal:nth-child(4) {
  transition-delay: 210ms;
}

.js .competencies > .reveal:nth-child(n + 5),
.js .philosophy > .reveal:nth-child(n + 5) {
  transition-delay: 280ms;
}

.js .timeline > .reveal:nth-child(2) {
  transition-delay: 90ms;
}

.js .timeline > .reveal:nth-child(3) {
  transition-delay: 180ms;
}

/* ------------------------------------------------------------------------
   Pointer spotlight

   Desktop only, and only where the pointer is a real one. main.js writes
   --spot-x / --spot-y on the hovered panel through the CSSOM; the element
   is painted whether or not those land, because the gradient's opacity is
   what is animated and it is 0 until the panel is hovered.
   ------------------------------------------------------------------------ */

@media (hover: hover) and (pointer: fine) and (min-width: 64em) {
  .project,
  .competency {
    --spot-x: 50%;
    --spot-y: 0%;
    isolation: isolate;
    overflow: clip;
  }

  .project::before,
  .competency::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: radial-gradient(
      26rem circle at var(--spot-x) var(--spot-y),
      rgba(138, 180, 248, 0.07),
      transparent 68%
    );
    opacity: 0;
    transition: opacity var(--t) var(--ease);
    pointer-events: none;
  }

  .competency {
    position: relative;
  }

  .project:hover::before,
  .competency:hover::before {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Motion off means off: no smooth scroll, no reveal translation, no caret
     blink, no hover lift. The site has to be fully usable here. */
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .js .reveal:not(.is-visible) {
    opacity: 1;
    transform: none;
  }

  /* A delay on a transition that no longer runs would still hold the
     element back by that many milliseconds. */
  .js .reveal {
    transition-delay: 0ms !important;
  }

  .caret {
    animation: none;
    opacity: 1;
  }

  /* The topology keeps its line and loses its signal: the chain still reads
     as a chain, which is the only thing it has to do.
     The selector matches the one in the 48em block exactly — a single-class
     selector here would lose to it on specificity and leave the gradient in
     place. */
  .topology__node + .topology__node::before {
    animation: none !important;
    background-image: none;
  }

  /* The progress rail is drawn in full rather than scaled in, so the
     timeline is not left with an empty track. */
  .timeline::after {
    transform: scaleY(1);
  }

  /* No hover lift, no spotlight fade, no image scale. */
  .project:hover .project__image,
  .project:focus-within .project__image {
    transform: none;
  }

  .project::after,
  .project:hover::after,
  .project:focus-within::after {
    width: 0;
  }

  .project::before,
  .competency::before {
    display: none;
  }

  .contact__card:hover .contact__chevron,
  .contact__card:focus-visible .contact__chevron {
    transform: none;
  }
}

/* Ultra-wide: keep the measure readable instead of letting lines stretch. */
@media (min-width: 120em) {
  :root {
    --shell-max: 1320px;
  }
}

@media print {
  .site-header,
  .nav,
  .nav-toggle,
  .hero__scroll,
  .update-toast {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
  }

  .panel,
  .facts {
    border-color: #ccc;
    background: none;
  }

  h1,
  h2,
  h3,
  h4,
  .hero__name,
  .project__title {
    color: #000;
  }
}
