/* ============================================================================
   Tejaswi — personal site.

   Design language: Apple's dark-mode system palette and material hierarchy —
   systemOrange accent, pure-white label ramp, translucent surfaces that blur
   AND saturate, specular edges, SF Pro with optical tracking, spring easing.
   ========================================================================= */

:root {
  /* Apple systemOrange (dark mode) and a tint/shade pair derived from it. */
  --amber: #ff9f0a;
  --amber-bright: #ffd08a;
  --amber-dim: #c77b06;
  /* Apple layers its dark UI over true black; surfaces lift off it. */
  --ink: #000000;
  --ink-2: #0a0a0c;

  /* Vibrancy ladder modelled on Apple's material hierarchy, built from their
     neutral dark greys (systemGray6 #1c1c1e upward) rather than a blue-tinted
     slate. Each step is more opaque than the last; all lean on heavy blur plus
     a saturation boost so colour from behind bleeds through rather than being
     flattened to grey. Lower alpha than a conventional "card" is the whole
     point — these should feel like frosted glass, not paint. */
  --material-ultrathin: rgba(44, 44, 46, 0.34);
  --material-thin: rgba(36, 36, 38, 0.44);
  --material: rgba(28, 28, 30, 0.55);
  --material-thick: rgba(24, 24, 26, 0.72);
  --blur: saturate(190%) blur(30px);
  --blur-heavy: saturate(200%) blur(44px);

  /* Apple's glass edges are not a uniform 1px box: light catches the top rim
     and falls away toward the bottom. These tokens build that gradient. */
  --hairline: rgba(255, 255, 255, 0.09);
  --hairline-warm: rgba(255, 159, 10, 0.18);
  --edge-top: rgba(255, 255, 255, 0.15);
  --edge-bottom: rgba(255, 255, 255, 0.04);
  --stroke: rgba(255, 255, 255, 0.11);

  /* Apple's dark-mode label ramp: pure white stepped down by opacity.
     Their tertiary sits near 30%, but that fails WCAG AA on the small mono
     labels this token drives, so it is held at 52% (5.4:1 over black). */
  --text: #ffffff;
  --text-2: rgba(255, 255, 255, 0.6);
  --text-3: rgba(255, 255, 255, 0.52);
  --ok: #32d74b;

  /* Apple splits its UI face by optical size: SF Pro Display above ~20px,
     SF Pro Text below, where the larger x-height and looser spacing aid
     legibility. Segoe UI Variable mirrors that split on Windows. */
  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text",
    "Segoe UI Variable Text", "Segoe UI", Inter, system-ui, sans-serif;
  --display: -apple-system, BlinkMacSystemFont, "SF Pro Display",
    "Segoe UI Variable Display", "Segoe UI", Inter, system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "SFMono-Regular", "Cascadia Mono",
    "JetBrains Mono", Menlo, monospace;

  /* Apple's tracking ramp. Type gets optically tighter as it grows: large
     display text needs negative tracking to avoid looking loose, small text
     needs positive tracking to stay legible. */
  --track-hero: -0.045em;
  --track-title: -0.028em;
  --track-heading: -0.021em;
  --track-body: -0.011em;
  --track-caption: 0.006em;
  --track-mono: 0.08em;

  /* Apple's dark-mode card radius sits around 16px at this scale, with a
     smaller step for nested elements. */
  --radius: 16px;
  --radius-sm: 10px;

  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --spring: cubic-bezier(0.34, 1.36, 0.64, 1);
  /* Decelerating curve for entrances: fast out of the gate, soft landing. */
  --out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Diffuse and layered rather than one hard drop — glass sits close to the
     surface, so the contact shadow is tight and the ambient one is wide. */
  --shadow: 0 12px 44px rgba(0, 0, 0, 0.42), 0 3px 10px rgba(0, 0, 0, 0.26);

  /* The specular rim: a bright top edge fading to almost nothing at the base,
     plus a faint inner glow just under the top edge. Applied as box-shadows so
     it composites over the material without needing an extra element. */
  --glass-edge: inset 0 1px 0 0 var(--edge-top),
    inset 0 0 0 1px var(--hairline), inset 0 -1px 0 0 var(--edge-bottom);

  /* One shared horizontal rhythm for every section. */
  --gutter: clamp(20px, 5vw, 72px);
  --measure: 1180px;

  /* Fixed chrome heights, so .app can claim exactly the space between. */
  --topbar-h: 58px;
  --rail-h: 78px;
}

* {
  box-sizing: border-box;
}

html {
  /* No page scrolling: each view owns the viewport and paginates internally. */
  height: 100%;
}

body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--ink);
  color: var(--text);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-optical-sizing: auto;
  /* Apple enables contextual alternates and kerning by default. */
  font-feature-settings: "calt" 1, "kern" 1;
}

/* Mono labels and figures use tabular, lining numerals so digits occupy equal
   width — Apple's convention wherever numbers sit in a column or update in
   place, since proportional figures make them jitter. */
.hud-meta,
.pillar-num,
.chip,
.clink-label,
.focus-label,
.hud-title {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* Display face for anything large enough to need optical tightening. */
h1,
h2,
h3,
.hero-tagline,
.pullquote p,
.project-name,
.page-title {
  font-family: var(--display);
  font-optical-sizing: auto;
}

::selection {
  background: rgba(255, 159, 10, 0.28);
  color: var(--amber-bright);
}

a {
  color: var(--amber);
  text-decoration: none;
}

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

.skip-link {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 100;
  padding: 10px 16px;
  background: var(--amber);
  color: var(--ink);
  font-size: 13px;
  font-weight: 700;
  border-radius: var(--radius-sm);
  transform: translateY(-200%);
  transition: transform 0.2s var(--ease);
}

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

/* --------------------------- ambient backdrop --------------------------- */

.backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Faint engineering graph paper. Masked so it fades out toward the edges. */
.backdrop-grid {
  position: absolute;
  inset: -10%;
  background-image: linear-gradient(rgba(255, 159, 10, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 159, 10, 0.055) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, #000 0%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, #000 0%, transparent 78%);
}

/* Warm bloom anchored behind the hero, drifting slowly. */
.backdrop-glow {
  position: absolute;
  top: -22vh;
  right: -12vw;
  width: 78vw;
  height: 78vw;
  max-width: 1100px;
  max-height: 1100px;
  background: radial-gradient(
    circle,
    rgba(255, 159, 10, 0.15) 0%,
    rgba(255, 159, 10, 0.05) 38%,
    transparent 68%
  );
  filter: blur(28px);
  animation: drift 22s var(--ease) infinite alternate;
}

@keyframes drift {
  from {
    transform: translate3d(0, 0, 0) scale(1);
  }
  to {
    transform: translate3d(-5%, 4%, 0) scale(1.1);
  }
}

/* Film grain — keeps large flat blacks from banding. */
.backdrop-noise {
  position: absolute;
  inset: 0;
  opacity: 0.035;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='180' height='180' filter='url(%23n)'/></svg>");
}

/* Amber halo trailing the cursor. Position comes from --mx/--my, written by
   app.js on an animation frame; opacity is toggled by .is-live so it fades in
   on first movement rather than flashing at 0,0 on load. */
.cursor-glow {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 0;
  width: 340px;
  height: 340px;
  margin: -170px 0 0 -170px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    circle closest-side,
    rgba(255, 159, 10, 0.13) 0%,
    rgba(255, 159, 10, 0.05) 38%,
    transparent 68%
  );
  transform: translate3d(var(--mx, 50vw), var(--my, 50vh), 0);
  transition: opacity 0.5s var(--ease);
  will-change: transform;
}

.cursor-glow.is-live {
  opacity: 1;
}

/* Per-tile spotlight. Each card gets a local highlight that tracks the cursor
   within its own bounds, which reads as the surface catching the light. */
.principle,
.project,
.offclock-item,
.clink,
.contact-form {
  --sx: 50%;
  --sy: 50%;
  --slit: 0;
}

.principle::before,
.project::before,
.offclock-item::before,
.clink::before,
.contact-form::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: var(--slit);
  background: radial-gradient(
    150px circle at var(--sx) var(--sy),
    rgba(255, 159, 10, 0.15),
    transparent 70%
  );
  transition: opacity 0.35s var(--ease);
  pointer-events: none;
  z-index: 0;
}

/* These need a positioning context and their content lifted above the wash. */
.offclock-item,
.clink {
  position: relative;
}

.offclock-item > *,
.clink > *,
.contact-form > * {
  position: relative;
  z-index: 1;
}

/* ------------------------------- topbar -------------------------------- */

.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding: 14px var(--gutter);
  background: rgba(6, 7, 10, 0.55);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}

.topbar.is-stuck {
  background: rgba(6, 7, 10, 0.78);
  border-bottom-color: var(--hairline);
}

/* Nav only - no brand mark. The name is already the largest thing on the home
   page, so repeating it in the corner just says the same thing twice. */
.topbar-inner {
  max-width: var(--measure);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.45;
    transform: scale(0.82);
  }
}
/* An iOS-style segmented control: one pill glides between items rather than
   each item painting its own background. The pill is a single element whose
   position and width are driven by CSS variables written in JS. */
.topbar-nav {
  position: relative;
  display: flex;
  gap: 4px;
}

.nav-pill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: var(--pill-w, 0);
  border-radius: 999px;
  background: rgba(255, 159, 10, 0.1);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
  transform: translateX(var(--pill-x, 0));
  opacity: 0;
  pointer-events: none;
  /* The spring curve is what makes it feel iOS rather than mechanical: the
     pill overshoots very slightly and settles. Width trails marginally behind
     position, which reads as the pill stretching into its new home. */
  transition: transform 0.46s var(--spring), width 0.42s var(--spring),
    opacity 0.25s var(--ease);
}

.topbar-nav.is-ready .nav-pill {
  opacity: 1;
}

/* Before first paint the pill has no measured position, so suppress the
   transition to stop it flying in from the left on load. */
.topbar-nav:not(.is-ready) .nav-pill {
  transition: none;
}

.topbar-nav a {
  position: relative;
  /* Above the pill so the label stays legible as it slides underneath. */
  z-index: 1;
  padding: 7px 14px;
  border-radius: 999px;
  color: var(--text-2);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
  transition: color 0.28s var(--ease);
}

.topbar-nav a:hover {
  color: var(--text);
}

.topbar-nav a.is-active {
  color: var(--amber);
}

/* ---------------------------- views & pages ---------------------------- */

/* The app fills whatever is left between the fixed topbar and the pager/footer
   rail. Views stack in the same box; only one is displayed at a time. */
.app {
  position: fixed;
  top: var(--topbar-h);
  left: 0;
  right: 0;
  bottom: var(--rail-h);
}

.view {
  position: absolute;
  inset: 0;
  display: none;
}

.view.is-active {
  display: block;
}

.page {
  position: absolute;
  inset: 0;
  display: none;
  /* If a viewport is too short for a page, that page scrolls rather than
     clipping its content. Hiding text is never the right trade. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.page.is-active {
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

/* "Safe centring": auto block margins centre the content when there is spare
   room but collapse to zero when the content is taller than the viewport, so
   an overflowing page scrolls from its true top instead of having its first
   lines cut off (which is what plain `align-items: center` does). */
/* Entering pages settle in from the direction of travel: a springy slide with
   a slight scale, so it reads as a card moving into place rather than a
   crossfade. */
.page.is-active.from-next {
  animation: pageInNext 0.52s var(--out) both;
}

.page.is-active.from-prev {
  animation: pageInPrev 0.52s var(--out) both;
}

@keyframes pageInNext {
  from {
    opacity: 0;
    transform: translate3d(44px, 0, 0) scale(0.985);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes pageInPrev {
  from {
    opacity: 0;
    transform: translate3d(-44px, 0, 0) scale(0.985);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Children cascade in behind the page itself. The delay is set per-element in
   app.js so the stagger follows document order regardless of nesting. */
/* The glyph is excluded: it runs its own fade to a very low opacity, and
   riseIn would end it at full strength. */
.page.is-active .stagger > *:not(.page-glyph) {
  animation: riseIn 0.5s var(--out) both;
  animation-delay: calc(var(--i, 0) * 55ms + 90ms);
}

@keyframes riseIn {
  from {
    opacity: 0;
    transform: translate3d(0, 14px, 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Reduced motion: no slide, no cascade — content simply appears. */
@media (prefers-reduced-motion: reduce) {
  .page.is-active.from-next,
  .page.is-active.from-prev,
  .page.is-active .stagger > * {
    animation: none !important;
  }
}

.page-inner {
  position: relative;
  width: 100%;
  max-width: var(--measure);
  /* Top-anchored, not centred. Centring made every page's heading land at a
     different height — a 127px swing between the shortest and tallest page —
     so the title visibly jumped as you moved between them. A fixed top offset
     keeps the heading on one line across the whole site and lets the content
     below simply run shorter or longer, which is how a document behaves.
     `margin-inline: auto` still centres horizontally. */
  margin-inline: auto;
  margin-block: 0;
  /* The top offset is capped rather than purely proportional: on tall tablet
     viewports a 5vh offset pushed the longest page past the fold. */
  padding: clamp(20px, 4vh, 44px) var(--gutter) clamp(14px, 2.5vh, 32px);
}

.page-head {
  display: flex;
  align-items: baseline;
  gap: 16px;
  /* Every page carries at least 11% slack, so the heading is given real
     separation from the content rather than a token gap. */
  margin-bottom: clamp(20px, 3.6vh, 40px);
}

.page-title {
  margin: 0;
  font-size: clamp(26px, 3.6vw, 40px);
  font-weight: 700;
  letter-spacing: var(--track-title);
}

.page-rule {
  flex: 1;
  height: 1px;
  background: linear-gradient(to right, var(--hairline-warm), transparent 85%);
}

.page-lede {
  margin: 0 0 clamp(18px, 3vh, 34px);
  max-width: 62ch;
  font-family: var(--display);
  font-size: clamp(16px, 1.9vw, 20px);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: var(--track-heading);
  color: var(--text);
  text-wrap: pretty;
}

/* -------------------------------- pager -------------------------------- */

/* Sits centre-bottom. On desktop the footer moves to the left corner so the
   two never collide. */
.pager {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 55;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px;
  border-radius: 999px;
  background: var(--material-thick);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  box-shadow: inset 0 0 0 1px var(--hairline), var(--shadow);
}

.pager[hidden] {
  display: none;
}

/* Form controls and buttons do not inherit font-family by default, so every
   interactive element states it explicitly. These are icon-only, but leaving
   them on the browser default would break the type stack if a label is ever
   added. */
.pager-btn,
.pager-dot {
  font-family: inherit;
}

.pager-btn {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  transition: color 0.2s var(--ease), background 0.2s var(--ease),
    box-shadow 0.2s var(--ease), opacity 0.2s var(--ease);
}

/* Glass on hover, matching the nav pill and buttons — a solid amber fill was
   the last hard-edged element left in the chrome. */
.pager-btn:hover:not(:disabled) {
  color: var(--amber-bright);
  background: rgba(255, 159, 10, 0.16);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.24),
    inset 0 0 0 1px var(--hairline-warm);
}

.pager-btn:disabled {
  opacity: 0.25;
  cursor: default;
}

.pager-btn svg {
  width: 16px;
  height: 16px;
}

.pager-dots {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 6px;
}

.pager-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  /* Inactive dots must still clear the 3:1 non-text contrast threshold. */
  background: rgba(255, 255, 255, 0.45);
  cursor: pointer;
  transition: background 0.25s var(--ease), width 0.3s var(--spring);
}

.pager-dot:hover {
  background: rgba(255, 159, 10, 0.6);
}

.pager-dot.is-current {
  width: 20px;
  border-radius: 999px;
  background: var(--amber);
}

/* ----------------------------- shared bits ----------------------------- */

/* No z-index here on purpose. A stacking context would isolate the hero
   portrait's `screen` blend from the fixed backdrop below, and it would paint
   as an opaque black rectangle over the grid. Document order alone already
   puts main above .backdrop. */
main {
  position: relative;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 22px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--amber);
}

.eyebrow-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 10px rgba(50, 215, 75, 0.8);
  animation: pulse 2.6s var(--ease) infinite;
}


.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 13px 24px;
  border: 0;
  border-radius: 999px;
  font-family: inherit;
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: transform 0.28s var(--spring), box-shadow 0.28s var(--ease),
    background 0.2s var(--ease), color 0.2s var(--ease);
}

/* Glass rather than solid fill, matching the nav pill: a translucent amber
   wash over the blurred backdrop with a warm hairline and a specular top edge.
   The label carries the colour instead of the background carrying the weight. */
.btn-primary {
  background: rgba(255, 159, 10, 0.14);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  color: var(--amber-bright);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.28),
    inset 0 0 0 1px var(--hairline-warm), 0 4px 18px rgba(255, 159, 10, 0.12);
}

.btn-primary:hover {
  background: rgba(255, 159, 10, 0.22);
  color: var(--amber-bright);
  transform: translateY(-2px);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.38),
    inset 0 0 0 1px rgba(255, 159, 10, 0.34), 0 8px 26px rgba(255, 159, 10, 0.2);
}

.btn-ghost {
  background: rgba(255, 255, 255, 0.045);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  color: var(--text);
  box-shadow: inset 0 1px 0 0 var(--edge-top), inset 0 0 0 1px var(--stroke);
}

.btn-ghost:hover {
  color: var(--amber);
  background: rgba(255, 159, 10, 0.08);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.2),
    inset 0 0 0 1px var(--hairline-warm);
  transform: translateY(-2px);
}

.btn-arrow {
  transition: transform 0.28s var(--spring);
}

.btn:hover .btn-arrow {
  transform: translateX(4px);
}

.btn:active {
  transform: translateY(0) scale(0.98);
}

/* --------------------------------- hero -------------------------------- */

/* The engraving is absolutely positioned, so the copy owns the whole width. */
/* The home page is a hero rather than a document page, so it stays optically
   centred instead of top-anchored like the rest. */
.hero-inner {
  position: relative;
  margin-block: auto;
  padding-top: clamp(14px, 2.5vh, 32px);
}

.hero-copy {
  position: relative;
  z-index: 1;
  max-width: 640px;
  /* Nudged marginally left of the content column so the copy block and the
     engraving sit as a balanced pair rather than crowding centre-right. */
  margin-left: -3%;
}

.hero-name {
  margin: 0 0 18px;
  font-weight: 700;
  line-height: 1;
}

.hero-hi {
  display: block;
  font-size: clamp(15px, 2vw, 19px);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-3);
  margin-bottom: 8px;
}

.hero-name-main {
  display: block;
  font-size: clamp(52px, 9.5vw, 108px);
  /* `background-clip: text` paints only inside the element's box, so a tight
     line box guillotined the descender of the "j" and made it read as an "i".
     The extra line-height plus a little padding give the glyph room; the
     negative bottom margin puts the layout back where it was. */
  line-height: 1.2;
  padding-bottom: 0.1em;
  margin-bottom: -0.1em;
  letter-spacing: var(--track-hero);
  background: linear-gradient(
    170deg,
    var(--text) 0%,
    var(--amber-bright) 46%,
    var(--amber-dim) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero-tagline {
  margin: 0 0 22px;
  font-size: clamp(22px, 3.1vw, 34px);
  font-weight: 600;
  line-height: 1.14;
  letter-spacing: var(--track-title);
  color: var(--text);
}

.hero-blurb {
  margin: 0 0 34px;
  max-width: 47ch;
  font-size: 16.5px;
  line-height: 1.68;
  letter-spacing: var(--track-body);
  color: var(--text-2);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* Portrait: black-on-black artwork, so the frame does the containing. */
/* The portrait is a stipple engraving, so it works far better as a texture
   than as a framed badge: screened over the ink and faded to nothing on every
   side, it reads as an impression in the page rather than a photo pasted on. */
.hero-engraving {
  position: absolute;
  /* Inset rather than top:50% + translateY(-50%): a transform doesn't affect
     layout, so the translated version still reported its untransformed bottom
     edge and inflated the page's scroll height. */
  top: 0;
  bottom: 0;
  right: 4%;
  z-index: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* Sized so `cover` downscales the 1024px source rather than magnifying it —
     upscaling a fine stipple turns crisp dots to mush. */
  width: min(43%, 600px);
  pointer-events: none;
}

/* The <picture> wrapper sits between the figure and the image, so it must
   carry the same sizing the image does — otherwise it takes its intrinsic
   height, overflows the figure and inflates the page's scroll height. */
.hero-engraving picture {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 0;
}

.hero-engraving img {
  width: 100%;
  height: 100%;
  /* `cover`, not `contain`: the figure is wider than it is tall, so `contain`
     letterboxed the portrait down to roughly half its box. Cover fills the box
     and the elliptical mask below dissolves the crop, so nothing reads as a cut. */
  object-fit: cover;
  object-position: 50% 22%;
  display: block;
  /* The asset carries a real alpha channel — its black background was baked out
     at build time, so there is no rectangle to hide and no blend mode or
     contrast-crush needed. The stipple is already amber-tinted in the pixels.
     Held at 0.5: the portrait is the largest warm mass on the page, and any
     higher it competes with the name for first read rather than supporting it. */
  opacity: 0.5;
  /* A single elliptical mask, not two composited linear ones: mask-composite
     support is inconsistent and the two-gradient version left hard vertical
     seams where the layers met. One ellipse fades on every side at once.
     The radii must keep the final transparent stop inside the box — a larger
     ellipse pushes that stop past the bottom edge, so the fade never completes
     and the image terminates in a hard horizontal cut. */
  mask-image: radial-gradient(
    ellipse 52% 50% at 50% 48%,
    #000 52%,
    rgba(0, 0, 0, 0.45) 78%,
    transparent 98%
  );
  -webkit-mask-image: radial-gradient(
    ellipse 52% 50% at 50% 48%,
    #000 52%,
    rgba(0, 0, 0, 0.45) 78%,
    transparent 98%
  );
  animation: engrave 900ms var(--out) both;
}

@keyframes engrave {
  from {
    opacity: 0;
    transform: scale(1.03);
  }
  to {
    opacity: 0.5;
    transform: none;
  }
}


/* ------------------------------ about text ----------------------------- */

.about-lede p {
  margin: 0 0 clamp(14px, 2.4vh, 26px);
  max-width: 62ch;
  font-family: var(--display);
  font-size: clamp(17px, 2vw, 21px);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--track-heading);
  color: var(--text);
}

/* Job title sits here as a header rather than in the hero - it's context for
   the lede that follows, not the headline a visitor reads first. */
.role-line {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 clamp(14px, 2.2vh, 22px);
  padding: 8px 15px 8px 13px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
  background: rgba(255, 255, 255, 0.03);
  box-shadow: var(--glass-edge);
}

.about-body p {
  margin: 0 0 clamp(18px, 3vh, 34px);
  max-width: 68ch;
  font-size: 16px;
  line-height: 1.68;
  color: var(--text-2);
}

.about-lede em,
.pillar em,
.principle em {
  color: var(--amber-bright);
  font-style: italic;
}
/* -------------------------------- pillars ------------------------------ */

.pillars {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--hairline);
}

/* Editorial three-column row: counter, title, description. Titles form one
   aligned column and descriptions another, so the row fills the full content
   width instead of stacking into the left half. */
.pillar {
  position: relative;
  display: grid;
  grid-template-columns: 44px minmax(0, 0.85fr) minmax(0, 1.15fr);
  align-items: start;
  gap: 5px 28px;
  padding: clamp(11px, 1.9vh, 22px) 0;
  border-bottom: 1px solid var(--hairline);
  transition: background 0.35s var(--ease);
}

.pillar::before {
  /* Amber wash bleeding in from the left on hover. */
  content: "";
  position: absolute;
  inset: 0 -20px;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    to right,
    rgba(255, 159, 10, 0.05),
    transparent 55%
  );
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  pointer-events: none;
}

.pillar:hover::before {
  opacity: 1;
}

.pillar-num {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--amber);
  opacity: 0.55;
  padding-top: 6px;
  transition: opacity 0.3s var(--ease);
}

.pillar:hover .pillar-num {
  opacity: 1;
}

.pillar h3 {
  margin: 0;
  font-size: clamp(17px, 1.6vw, 20px);
  font-weight: 600;
  letter-spacing: var(--track-heading);
  line-height: 1.3;
  text-wrap: balance;
}

.pillar p {
  margin: 0;
  padding-top: 2px;
  color: var(--text-2);
  font-size: 15px;
  line-height: 1.58;
  letter-spacing: var(--track-body);
  text-wrap: pretty;
}

/* Below the three-column threshold, fall back to counter + stacked text. */
@media (max-width: 900px) {
  .pillar {
    grid-template-columns: 44px minmax(0, 1fr);
    gap: 5px 16px;
  }

  .pillar-num {
    grid-row: span 2;
  }

  .pillar p {
    padding-top: 0;
    max-width: 66ch;
  }
}

/* ------------------------------ page glyph ----------------------------- */

/* A large, very faint mark behind each page's heading, echoing the engraved
   portrait on home. The top-left corner is the one region that is open on every
   page — the title sits there and nothing else does — so the mark never
   underlays a tile. Pointer-inert and outside the flow, so it can affect
   neither layout nor scroll height; masked so it dissolves rather than ending
   on an edge. */
.page-glyph {
  position: absolute;
  /* One token drives both the size and the horizontal offset, so the overlap
     stays proportional at every viewport instead of drifting. */
  --glyph-size: clamp(200px, min(30vh, 26vw), 340px);
  /* Anchored to the content column's left edge, offset by a fixed fraction of
     its own size. That keeps the overlap proportional — only the mark's right
     edge tucked behind the heading, the bulk of it out in the margin — at every
     width, rather than the offset drifting as the window grows. Some overlap
     reads as layered depth; too much is just noise behind the text. */
  left: max(
    calc(50% - var(--measure) / 2 - var(--glyph-size) * 0.86),
    calc(var(--glyph-size) * -0.58)
  );
  top: 0;
  z-index: 0;
  width: var(--glyph-size);
  height: var(--glyph-size);
  color: var(--amber);
  opacity: 0.045;
  pointer-events: none;
  user-select: none;
  mask-image: radial-gradient(
    ellipse 62% 62% at 50% 50%,
    #000 40%,
    rgba(0, 0, 0, 0.5) 72%,
    transparent 96%
  );
  -webkit-mask-image: radial-gradient(
    ellipse 62% 62% at 50% 50%,
    #000 40%,
    rgba(0, 0, 0, 0.5) 72%,
    transparent 96%
  );
  animation: glyphIn 900ms var(--out) both;
}

.page-glyph svg {
  width: 100%;
  height: 100%;
}

@keyframes glyphIn {
  from {
    opacity: 0;
    transform: scale(1.04);
  }
  to {
    opacity: 0.045;
    transform: none;
  }
}

/* Content sits above the glyph. Scoped to the glyph's own siblings by z-index
   alone — an earlier version set `position: relative` on every child of
   .page-inner, which outranked `.hero-engraving { position: absolute }` and
   dragged the hero portrait back into the flow, inflating the home page by
   400px. Raising the stacking order is all that is actually needed here. */
.page-inner {
  z-index: 1;
}

@media (max-width: 900px) {
  /* Smaller on a phone, and marginally stronger: at this size the mark covers
     far less area, so the desktop opacity disappears altogether. */
  .page-glyph {
    --glyph-size: clamp(150px, 38vw, 230px);
    /* Less of the mark is pushed off-canvas on a phone: the desktop offset is
       scaled to a wide margin that simply does not exist here, and it left only
       a sliver showing. */
    left: calc(var(--glyph-size) * -0.34);
    opacity: 0.075;
  }

  @keyframes glyphIn {
    from {
      opacity: 0;
      transform: scale(1.04);
    }
    to {
      opacity: 0.075;
      transform: none;
    }
  }
}

/* ------------------------------ editorial ------------------------------ */

/* A magazine spread rather than a single narrow column. The lede runs full
   width as a standfirst, then the body and the focus panel sit side by side —
   which uses the page's width instead of stranding the copy in its left half.
   Collapses to one column below the two-column threshold. */
.editorial {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  column-gap: clamp(32px, 5vw, 68px);
  row-gap: clamp(18px, 3vh, 30px);
  align-items: start;
}

.editorial .about-lede {
  grid-column: 1 / -1;
}

/* The standfirst: set wider and given its own rule beneath, exactly as a
   magazine sets an introductory paragraph. */
.editorial .about-lede p {
  max-width: 42ch;
  font-size: clamp(19px, 1.9vw, 25px);
  line-height: 1.44;
  padding-bottom: clamp(16px, 2.6vh, 26px);
  border-bottom: 1px solid var(--hairline);
  margin-bottom: 0;
}

.editorial .about-body {
  grid-column: 1;
}

.editorial .about-body p {
  /* The measure is set by the column now, not an arbitrary cap. */
  max-width: none;
  font-size: clamp(16px, 1.25vw, 18px);
  line-height: 1.74;
  margin-bottom: 0;
}

/* A drop cap anchors the column and signals "this is the article". */
.editorial .about-body p::first-letter {
  float: left;
  margin: 0.02em 0.09em 0 0;
  font-family: var(--display);
  font-size: 3.1em;
  font-weight: 700;
  line-height: 0.82;
  letter-spacing: var(--track-hero);
  color: var(--amber);
}

/* The focus panel becomes a sidebar: set apart by a rule rather than a box,
   which is how a magazine handles a standout column. The rule runs the full
   column height rather than stopping at the last item, so it reads as a
   structural gutter instead of a stray line. */
.editorial .focus {
  grid-column: 2;
  grid-row: 2 / span 2;
  align-self: stretch;
  padding-top: 0;
  border-top: 0;
  padding-left: clamp(20px, 2.4vw, 32px);
  border-left: 1px solid var(--hairline);
}

.editorial .focus-list {
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}

@media (max-width: 900px) {
  /* One column: the sidebar returns to a full-width block beneath the body,
     with its rule back on top where it reads as a divider, not a gutter. */
  .editorial {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(14px, 2.2vh, 22px);
  }

  .editorial .about-body,
  .editorial .focus {
    grid-column: 1;
  }

  .editorial .focus {
    /* Reset the desktop row span, or the sidebar keeps its two-row placement
       and sits alongside the body instead of stacking beneath it. */
    grid-row: auto;
    align-self: auto;
    padding-left: 0;
    border-left: 0;
    padding-top: clamp(14px, 2.2vh, 22px);
    border-top: 1px solid var(--hairline);
  }

  .editorial .focus-list {
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  }

  /* The desktop editorial type scale is set for a wide two-column spread; on
     phones it must return to the compact body sizes or the page overflows. */
  .editorial .about-lede p {
    font-size: 17px;
    line-height: 1.45;
  }

  .editorial .about-body p {
    font-size: 15px;
    line-height: 1.62;
  }

  /* A drop cap needs a tall column to sit in; at phone widths it crowds the
     first two lines rather than anchoring them. */
  .editorial .about-body p::first-letter {
    float: none;
    margin: 0;
    font-size: inherit;
    font-weight: inherit;
    line-height: inherit;
    letter-spacing: inherit;
    color: inherit;
  }
}

/* -------------------------------- focus -------------------------------- */

/* Replaces the old stat readout: rather than manufacture numbers, this simply
   states what the work is about right now. A live amber pulse marks it as a
   current signal rather than a static credential. */
.focus {
  padding-top: clamp(14px, 2.2vh, 22px);
  border-top: 1px solid var(--hairline);
}

.focus-label {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 clamp(12px, 1.8vh, 18px);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
}

.focus-pulse {
  position: relative;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--amber);
  flex: none;
}

/* A slow halo breathing out of the dot — the only continuously animated
   element on the site, so it reads as "live" without demanding attention. */
.focus-pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--amber);
  animation: focusPulse 2.4s var(--ease) infinite;
}

@keyframes focusPulse {
  0% {
    transform: scale(1);
    opacity: 0.5;
  }
  70%,
  100% {
    transform: scale(3.2);
    opacity: 0;
  }
}

.focus-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: 10px 26px;
}

.focus-list li {
  position: relative;
  padding-left: 18px;
  font-family: var(--display);
  font-size: clamp(15px, 1.7vw, 18px);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: var(--track-heading);
  color: var(--text);
  text-wrap: pretty;
}

/* A thin amber rule instead of a bullet, echoing the page-head hairline. */
.focus-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 9px;
  height: 1px;
  background: var(--amber);
  opacity: 0.8;
}

/* -------------------------------- skills ------------------------------- */

/* Two-by-two rather than four-across: a 2x2 block gives each card roughly
   double the width, so the lists sit comfortably instead of running as four
   narrow strips, and it collapses to a single column on phones. */
.skills {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

.skillset {
  position: relative;
  padding: 28px 28px 30px;
  border-radius: var(--radius);
  background: var(--material);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  box-shadow: var(--glass-edge);
  transition: background 0.3s var(--ease), box-shadow 0.3s var(--ease);
}

.skillset:hover {
  background: var(--material-thick);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
}

.skillset-title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 18px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--hairline);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: var(--track-heading);
  color: var(--text);
}

.skillset-icon {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  color: var(--amber);
  background: rgba(255, 159, 10, 0.1);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
}

.skillset-icon svg {
  width: 15px;
  height: 15px;
}

.skillset-list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* Two columns inside the now-wider card: keeps each card compact vertically
     and uses the horizontal room the 2x2 layout freed up. */
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 20px;
}

.skillset-list li {
  position: relative;
  padding-left: 16px;
  font-size: 14.5px;
  line-height: 1.5;
  letter-spacing: var(--track-caption);
  color: var(--text-2);
}

/* Small amber tick, drawn rather than bulleted. */
.skillset-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  width: 5px;
  height: 5px;
  border-radius: 1px;
  background: var(--amber);
  opacity: 0.65;
}

/* ------------------------------ principles ----------------------------- */

/* 230px keeps all four cards on one row inside the 1036px content column
   (4 x 230 + 3 x 16 = 968) before collapsing to two-up. */
/* Two rows of subgrid so every title block occupies the same height and every
   description therefore starts on one shared line, no matter how many lines a
   title wraps to. */
.principles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
  grid-template-rows: auto auto;
  gap: 16px;
}

.principle {
  position: relative;
  grid-row: span 2;
  display: grid;
  grid-template-rows: subgrid;
  align-content: start;
  gap: 9px;
  padding: 26px 24px;
  border-radius: var(--radius);
  background: var(--material);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  box-shadow: var(--glass-edge);
  transition: transform 0.35s var(--spring), box-shadow 0.35s var(--ease),
    background 0.35s var(--ease);
}

.principle:hover {
  transform: translateY(-4px);
  background: var(--material-thick);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.22),
    inset 0 0 0 1px var(--hairline-warm), var(--shadow);
}

.principle h3 {
  margin: 0;
  align-self: start;
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.32;
  letter-spacing: var(--track-heading);
  text-wrap: balance;
  color: var(--amber-bright);
}

.principle p {
  margin: 0;
  align-self: start;
  font-size: 15px;
  line-height: 1.62;
  letter-spacing: var(--track-body);
  text-wrap: pretty;
  color: var(--text-2);
}

/* Browsers without subgrid: pin the title to two lines so the descriptions
   still share a starting line. */
@supports not (grid-template-rows: subgrid) {
  .principle {
    display: flex;
    flex-direction: column;
  }

  .principle h3 {
    min-height: calc(2 * 1.32em);
  }
}

/* ------------------------------ HUD panel ------------------------------ */

/* Modelled on the Vector telemetry panel: a single glass slab divided by
   hairlines into icon-led rows, with uppercase mono labels on the left and
   values ranged right. Denser and more legible than four separate cards. */
.hud {
  max-width: 600px;
  border-radius: var(--radius);
  background: var(--material);
  backdrop-filter: var(--blur-heavy);
  -webkit-backdrop-filter: var(--blur-heavy);
  box-shadow: var(--glass-edge), var(--shadow);
  overflow: hidden;
}

.hud-head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 16px 24px;
  border-bottom: 1px solid var(--hairline);
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.035),
    transparent
  );
}

.hud-icon {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  color: var(--amber);
  flex: none;
}

.hud-icon svg {
  width: 100%;
  height: 100%;
}

.hud-title {
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--amber);
}

.hud-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Rows are links to the issuer's public verification page. Three columns: the
   mark, the credential, and an arrow that appears on hover.
   The columns are fixed rather than fluid and the whole track group is centred:
   a `1fr` value column stretched the rows across the full panel and left the
   text stranded against a wide empty margin. Fixed tracks keep every logo on
   one rule while the group sits centred in the panel. */
.hud-row {
  position: relative;
  display: grid;
  grid-template-columns: 78px minmax(0, 300px) 18px;
  justify-content: center;
  align-items: center;
  gap: 28px;
  padding: 15px 24px;
  color: inherit;
  text-decoration: none;
  transition: background 0.3s var(--ease);
}

.hud-rows li + li .hud-row {
  border-top: 1px solid var(--hairline);
}

.hud-row:hover {
  background: rgba(255, 255, 255, 0.035);
}

.hud-row:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: -2px;
}

.hud-row:hover .hud-value {
  color: var(--amber-bright);
}

/* The arrow is the only affordance that the row is clickable, so it fades in
   on hover and keyboard focus rather than sitting there permanently. */
.hud-go {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  color: var(--amber);
  opacity: 0;
  transform: translate(-3px, 3px);
  transition: opacity 0.28s var(--ease), transform 0.28s var(--out);
}

.hud-go svg {
  width: 100%;
  height: 100%;
}

.hud-row:hover .hud-go,
.hud-row:focus-visible .hud-go {
  opacity: 0.9;
  transform: none;
}

/* Available to screen readers, removed from the visual layout. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* No plate: every mark is transparent — the two universities as white
   wordmarks, Microsoft and Google as their full-colour glyphs — so they sit
   directly on the glass instead of on white cards that fight the dark surface. */
.hud-mark {
  display: grid;
  /* Centred in the well rather than ranged left: the marks range from a 1:1
     glyph to a 5:1 wordmark, so left-aligning them left the small ones marooned
     far from the credential text. Centring evens the optical gap on every row. */
  place-items: center;
  height: 34px;
  opacity: 0.92;
  transition: opacity 0.3s var(--ease);
}

.hud-row:hover .hud-mark {
  opacity: 1;
}

/* `min-height: 0` is required: grid items default to `min-height: auto`, which
   refuses to shrink below the image's intrinsic size, so max-height alone let
   the tall Penn lockup overflow. */
.hud-mark svg,
.hud-mark img {
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}

/* The square glyphs are scaled up so they carry roughly the same optical
   weight as the wide university wordmarks. */
.hud-mark svg {
  width: 27px;
  height: 27px;
}

.hud-value {
  font-size: 16px;
  font-weight: 600;
  letter-spacing: var(--track-heading);
  color: var(--text);
  text-wrap: pretty;
}

/* ------------------------------- projects ------------------------------ */

/* Three across on desktop: each page holds exactly three cards, so a single
   row is both shorter and more legible than a 2+1 wrap. Collapses to two, then
   one, as width allows. */
.projects {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr));
  gap: 16px;
}

.project {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 22px 22px 24px;
  border-radius: var(--radius);
  background: var(--material);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  box-shadow: var(--glass-edge);
  overflow: hidden;
  transition: transform 0.4s var(--spring), box-shadow 0.4s var(--ease),
    background 0.4s var(--ease);
}

.project::after {
  /* Amber pool rising from the bottom edge on hover. */
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 60%;
  background: linear-gradient(to top, rgba(255, 159, 10, 0.07), transparent);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}

.project:hover {
  transform: translateY(-5px);
  background: var(--material-thick);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.22),
    inset 0 0 0 1px var(--hairline-warm), var(--shadow);
}

.project:hover::after {
  opacity: 1;
}

.project > * {
  position: relative;
  z-index: 1;
}

.project-head {
  margin-bottom: 0;
}

/* A small amber tile above the project name, matching the icon language used
   for skillsets and off-the-clock cards. Drawn inline rather than shipped as
   files: at this size a stroked glyph is sharper than a scaled-down logo, and
   it keeps every project on one visual footing. */
.project-icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin-bottom: 10px;
  border-radius: 9px;
  color: var(--amber);
  background: rgba(255, 159, 10, 0.1);
  box-shadow: inset 0 1px 0 0 rgba(255, 208, 138, 0.2),
    inset 0 0 0 1px var(--hairline-warm);
  transition: color 0.3s var(--ease), background 0.3s var(--ease),
    transform 0.35s var(--spring);
}

.project-icon svg {
  width: 17px;
  height: 17px;
}

.project:hover .project-icon {
  background: rgba(255, 159, 10, 0.18);
  transform: translateY(-1px);
}

/* Below the three-column threshold the project grid wraps to two columns and
   the cards grow taller, so the icon sits inline with the name to stop the
   page running past the fold. A float cannot do this because `.project` is a
   flex container and flex items ignore float, so the card becomes a two-column
   grid with everything below the title spanning the full width. */
@media (max-width: 1000px) {
  .project {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-content: start;
    column-gap: 11px;
  }

  .project-icon {
    grid-column: 1;
    grid-row: 1;
    width: 30px;
    height: 30px;
    margin: 0;
    border-radius: 8px;
    align-self: center;
  }

  .project-icon svg {
    width: 16px;
    height: 16px;
  }

  .project-name {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    margin-bottom: 0;
  }

  .project-meta,
  .project-desc,
  .tags,
  .project-links {
    grid-column: 1 / -1;
  }

  .project-meta {
    margin-top: 10px;
  }
}

.project-name {
  margin: 0 0 8px;
  font-size: 23px;
  font-weight: 700;
  letter-spacing: var(--track-title);
}

/* Role and status share a line so a long project name always gets the full
   card width and never collides with its chip. */
.project-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
}

.project-role {
  margin: 0;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--amber-dim);
}

.chip {
  flex-shrink: 0;
  padding: 4px 10px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
  background: rgba(255, 255, 255, 0.055);
  box-shadow: var(--glass-edge);
}

.chip-live {
  color: var(--ok);
  background: rgba(50, 215, 75, 0.1);
  box-shadow: inset 0 0 0 1px rgba(50, 215, 75, 0.24);
}

.chip-internal {
  color: var(--amber);
  background: rgba(255, 159, 10, 0.1);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
}

.chip-green {
  color: #7ee08f;
  background: rgba(126, 224, 143, 0.09);
  box-shadow: inset 0 0 0 1px rgba(126, 224, 143, 0.2);
}

.project-role {
  margin: 0 0 14px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--amber-dim);
}

.project-desc {
  margin: 0 0 14px;
  font-size: 15px;
  line-height: 1.62;
  letter-spacing: var(--track-body);
  color: var(--text-2);
  flex: 1;
  text-wrap: pretty;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
}

.tags li {
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 11.5px;
  letter-spacing: 0.03em;
  color: var(--text-3);
  background: rgba(255, 255, 255, 0.04);
  box-shadow: var(--glass-edge);
}

.project-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 18px;
  padding-top: 13px;
  border-top: 1px solid var(--hairline);
}

.link-out {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 14px;
  font-weight: 600;
  color: var(--amber);
  transition: color 0.2s var(--ease), gap 0.25s var(--spring);
}

.link-out:hover {
  color: var(--amber-bright);
  gap: 9px;
}

.link-muted {
  color: var(--text-2);
}

.link-muted:hover {
  color: var(--text);
}

.link-static {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ------------------------------ off the clock -------------------------- */

/* Two-by-two, matching the skills and credentials grids: four across squeezed
   each description into a three-line rag in a narrow column. */
.offclock {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

/* Icon beside the text: the 2x2 grid gives each card enough width that the
   side-by-side arrangement no longer squeezes the description, and it saves a
   row of vertical space per card. */
.offclock-item {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 20px 20px 22px;
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.022);
  box-shadow: var(--glass-edge);
  transition: background 0.3s var(--ease), box-shadow 0.3s var(--ease),
    transform 0.35s var(--spring);
}

.offclock-item:hover {
  transform: translateY(-3px);
  background: rgba(255, 159, 10, 0.05);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
}

.offclock-icon {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: var(--amber);
  background: rgba(255, 159, 10, 0.1);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
  transition: color 0.25s var(--ease), background 0.25s var(--ease);
}

.offclock-item:hover .offclock-icon {
  color: var(--ink);
  background: var(--amber);
}

.offclock-icon svg {
  width: 19px;
  height: 19px;
}

.offclock-item h3 {
  margin: 0 0 5px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: var(--track-heading);
}

.offclock-item p {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  letter-spacing: var(--track-caption);
  color: var(--text-2);
  text-wrap: pretty;
}

/* ------------------------------- pullquote ----------------------------- */

.pullquote {
  margin: 84px 0 0;
  padding: 0 0 0 clamp(20px, 4vw, 38px);
  border-left: 2px solid var(--amber);
}

.pullquote p {
  margin: 0;
  max-width: 46ch;
  font-size: clamp(20px, 2.9vw, 30px);
  font-weight: 500;
  line-height: 1.32;
  letter-spacing: var(--track-title);
  color: var(--amber-bright);
}

/* -------------------------------- origin map --------------------------- */

.origin {
  margin: 0;
}

.origin-map {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: #0b0d12;
  box-shadow: var(--glass-edge);
  transition: box-shadow 0.35s var(--ease);
  /* The tiles carry a CSS filter, which promotes them to their own layer and
     lets WebKit paint them past the rounded overflow clip — square corners
     bleed out over the border on iOS. Masking the container forces it to
     render into its own buffer, where the radius is honoured. */
  isolation: isolate;
  -webkit-mask-image: -webkit-radial-gradient(#fff, #000);
  mask-image: radial-gradient(#fff, #000);
}

.origin-map:hover {
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
}

/* Tiles are appended by JS so the section still renders without network. */
.origin-map canvas,
.origin-map img.origin-tile {
  position: absolute;
  display: block;
  /* CartoDB dark-matter is blue-grey; warm it into the amber palette. The
     contrast lift keeps the coastline crisp so the sea reads as water rather
     than a washed-out panel, while brightness keeps place labels readable. */
  filter: grayscale(1) sepia(0.5) saturate(1.5) hue-rotate(-12deg)
    brightness(1.08) contrast(1.18);
}

/* Light vignette only. The scrim used to darken the lower half for a caption
   that no longer exists, which buried the coastline and the place label. */
.origin-scrim {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle at 50% 42%,
    transparent 42%,
    rgba(6, 7, 10, 0.42) 96%
  );
  pointer-events: none;
}

.origin-pin {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  z-index: 2;
}

.origin-pin-dot {
  position: absolute;
  top: -4px;
  left: -4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--amber);
  box-shadow: 0 0 12px 2px rgba(255, 159, 10, 0.9);
}

.origin-pin-ring {
  position: absolute;
  top: -5px;
  left: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1.5px solid var(--amber);
  animation: ping 2.8s var(--ease) infinite;
}

@keyframes ping {
  0% {
    transform: scale(1);
    opacity: 0.9;
  }
  70%,
  100% {
    transform: scale(4.6);
    opacity: 0;
  }
}

/* Required by the CARTO and OpenStreetMap terms. */
.origin-attrib {
  position: absolute;
  right: 8px;
  bottom: 6px;
  z-index: 2;
  font-size: 9.5px;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.52);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

.origin-attrib a {
  color: inherit;
}

.origin-attrib a:hover {
  color: var(--text-2);
}

/* -------------------------------- contact ------------------------------ */

.section-contact {
  padding-bottom: clamp(84px, 12vh, 140px);
}

.contact-grid {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  /* A generous gutter: the aside and the form are two distinct things, and at
     the previous spacing they read as one crowded block. */
  gap: clamp(36px, 6.5vw, 88px);
  align-items: start;
}

.contact-lede {
  margin: 0 0 20px;
  font-size: 16.5px;
  line-height: 1.58;
  letter-spacing: var(--track-body);
  color: var(--text-2);
  text-wrap: pretty;
}

/* Compact icon tiles instead of exposed URLs — nothing for a scraper to read,
   and visually calmer than a wall of links. */
.contact-links {
  list-style: none;
  margin: 0 0 26px;
  padding: 0;
  display: grid;
  gap: 10px;
}

.clink {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  border: 0;
  border-radius: var(--radius-sm);
  text-align: left;
  font-family: inherit;
  color: var(--text);
  background: rgba(255, 255, 255, 0.026);
  box-shadow: var(--glass-edge);
  cursor: pointer;
  transition: background 0.28s var(--ease), box-shadow 0.28s var(--ease),
    transform 0.28s var(--spring);
}

.clink:hover {
  background: rgba(255, 159, 10, 0.06);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
  transform: translateX(3px);
}

.clink-static {
  cursor: default;
}

.clink-static:hover {
  background: rgba(255, 255, 255, 0.026);
  box-shadow: var(--glass-edge);
  transform: none;
}

.clink-icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  color: var(--amber);
  background: rgba(255, 159, 10, 0.1);
  box-shadow: inset 0 0 0 1px var(--hairline-warm);
  transition: color 0.25s var(--ease), background 0.25s var(--ease);
}

.clink:hover .clink-icon {
  color: var(--ink);
  background: var(--amber);
}

.clink-static:hover .clink-icon {
  color: var(--amber);
  background: rgba(255, 159, 10, 0.1);
}

.clink-icon svg {
  width: 17px;
  height: 17px;
}

.clink-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1;
}

.clink-label {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
}

.clink-value {
  font-size: 14px;
  letter-spacing: var(--track-body);
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.clink-arrow {
  flex-shrink: 0;
  font-size: 13px;
  color: var(--text-3);
  transition: color 0.25s var(--ease), transform 0.28s var(--spring);
}

.clink:hover .clink-arrow {
  color: var(--amber);
  transform: translate(2px, -2px);
}

.contact-note {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-3);
}

.contact-form {
  position: relative;
  padding: clamp(24px, 3vw, 34px);
  border-radius: var(--radius);
  background: var(--material);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  box-shadow: inset 0 0 0 1px var(--hairline), var(--shadow);
}

.field {
  margin-bottom: 18px;
}

.field label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-3);
}

.field-count {
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--text-3);
  opacity: 0.7;
  text-transform: none;
}

.field input,
.field textarea {
  width: 100%;
  padding: 11px 14px;
  border: 0;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.32);
  box-shadow: inset 0 0 0 1px var(--stroke);
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
  line-height: 1.5;
  transition: box-shadow 0.22s var(--ease), background 0.22s var(--ease);
}

.field textarea {
  resize: vertical;
  min-height: 84px;
}

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

.field input:focus,
.field textarea:focus {
  outline: none;
  background: rgba(0, 0, 0, 0.45);
  box-shadow: inset 0 0 0 1px var(--amber), 0 0 0 4px rgba(255, 159, 10, 0.14);
}

.field.has-error input,
.field.has-error textarea {
  box-shadow: inset 0 0 0 1px rgba(255, 69, 58, 0.65);
}

.field.has-error input:focus,
.field.has-error textarea:focus {
  box-shadow: inset 0 0 0 1px #ff453a, 0 0 0 4px rgba(255, 69, 58, 0.14);
}

.field-error {
  margin: 7px 0 0;
  font-size: 12.5px;
  color: #ff8078;
}

.btn-submit {
  width: 100%;
  justify-content: center;
  margin-top: 4px;
}

.form-status {
  margin: 14px 0 0;
  min-height: 18px;
  font-size: 13px;
  text-align: center;
  color: var(--text-3);
  transition: color 0.2s var(--ease);
}

.form-status.is-ok {
  color: var(--ok);
}

.form-status.is-bad {
  color: #ff8078;
}

/* -------------------------------- footer ------------------------------- */

/* Bottom-left on desktop so it clears the centred pager; centred on narrow
   screens where the pager sits above it instead of beside it. */
.footer {
  position: fixed;
  left: 0;
  bottom: 0;
  z-index: 40;
  padding: 12px var(--gutter);
  pointer-events: none;
}

.footer-note {
  margin: 0;
  font-size: 11.5px;
  letter-spacing: var(--track-caption);
  color: var(--text-3);
}

.footer-sep {
  padding: 0 6px;
  opacity: 0.4;
}

.footer-heart {
  /* Keep the emoji on the text baseline at this small size. */
  font-size: 10.5px;
  vertical-align: -0.5px;
  display: inline-block;
  animation: heartbeat 2.4s var(--ease) infinite;
}

@keyframes heartbeat {
  0%,
  38%,
  100% {
    transform: scale(1);
  }
  18% {
    transform: scale(1.22);
  }
  28% {
    transform: scale(1.08);
  }
}

.footer-year {
  font-family: var(--mono);
  letter-spacing: 0.1em;
}

/* --------------------------------- responsive -------------------------- */

@media (max-width: 900px) {
  :root {
    --rail-h: 96px;
  }

  /* Pager sits above the centred footer rather than beside it. */
  .pager {
    bottom: 40px;
  }

  .footer {
    left: 0;
    right: 0;
    padding: 8px var(--gutter);
    text-align: center;
  }

  /* Smaller on a phone: the footer is a signature, not content, and at the
     desktop size it competes with the calls to action directly above it. */
  .footer-note {
    font-size: 10px;
  }

  .footer-sep {
    padding: 0 4px;
  }

  .hero-inner {
    grid-template-columns: 1fr;
    gap: clamp(18px, 3vh, 32px);
  }

  /* On phones the copy fills the middle of the screen, so a centred engraving
     sits directly behind the paragraph text and hurts legibility. Anchor it to
     the empty upper band instead and fade its lower edge out before the copy
     begins. */
  /* On phones the engraving becomes a real flow item above the copy rather
     than an absolutely-positioned backdrop. Overlapping body text is then
     structurally impossible instead of a matter of tuning offsets. */
  .hero-engraving {
    position: static;
    order: -1;
    /* Full-bleed: cancel the page gutter so the portrait runs edge to edge.
       100% of the page, not 100vw — the viewport unit includes the scrollbar
       gutter, which pushed the figure 7px past the right edge.

       The height scales on both axes rather than snapping between fixed steps:
       `vh` lets it grow on a tall window, `vw` lets it grow on a wide one, and
       `min()` takes whichever is the real constraint — so a short, wide window
       is governed by its height rather than being handed a band it cannot fit.
       The 26vh term is what keeps the calls to action on screen: the copy below
       needs roughly two thirds of the page, whatever the width. */
    height: clamp(180px, min(26vh, 38vw), 400px);
    width: auto;
    margin-left: calc(var(--gutter) * -1);
    margin-right: calc(var(--gutter) * -1);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 6px;
  }

  .hero-inner {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* The <picture> element is the flex child, so it must carry the sizing —
     otherwise it shrink-wraps and the image inside overflows the band. */
  .hero-engraving picture {
    display: block;
    width: 100%;
    height: 100%;
    min-height: 0;
  }

  .hero-engraving img {
    /* The crop is supplied by the <picture> source above; `contain` then
       guarantees the subject is never cut, whatever the viewport.
       `min-height: 0` and an explicit max are both required: as a flex child
       the image was stretching past the figure's height and being guillotined
       by its edge, which showed up as a hard horizontal line under the chin. */
    width: 100%;
    height: 100%;
    min-height: 0;
    max-height: 100%;
    object-fit: contain;
    object-position: 50% 50%;
    opacity: 0.58;
    /* No CSS mask on phones. The fade is baked into the asset's alpha channel,
       which is exact: a mask sized in percentages of the box does not know
       where `contain` actually painted the image, so its falloff landed short
       of the edge and left the shoulders ending on a hard line. */
    mask-image: none;
    -webkit-mask-image: none;
  }

  @keyframes engrave {
    from {
      opacity: 0;
      transform: scale(1.03);
    }
    to {
      opacity: 0.58;
      transform: none;
    }
  }

  /* The copy block never sits over the engraving on phones. */
  .hero-copy {
    margin-left: 0;
  }

  /* Shorter phones cannot carry both a generous portrait and the full hero
     copy. Rather than shrink the portrait everywhere, only short screens trade
     it down and tighten the copy's rhythm — tall phones keep the larger crop. */
  /* Real phones lose 130-160px to browser chrome, so an iPhone 16 Pro reports
     roughly 716-745px rather than its nominal 874. At those heights the hero
     could not fit and the calls to action fell off the bottom. */
  @media (max-height: 860px) {
    .hero-engraving {
      height: clamp(150px, 21vh, 220px);
    }

    .hero-name {
      margin-bottom: 10px;
    }

    .hero-tagline {
      margin-bottom: 10px;
    }

    .hero-blurb {
      margin-bottom: 14px;
    }
  }

  /* Shorter still — Firefox and Chrome on iOS keep more chrome on screen than
     Safari does. The blurb is the first thing to go rather than the portrait:
     shrinking the engraving to fit left it a 130px strip against 53% copy,
     which reads as a header image rather than a presence. The tagline already
     says what the blurb elaborates. */
  @media (max-height: 760px) {
    .hero-engraving {
      height: clamp(200px, 27vh, 260px);
    }

    .hero-blurb {
      display: none;
    }

    .hero-name-main {
      font-size: clamp(42px, 12.5vw, 56px);
    }

    .hero-tagline {
      font-size: 17.5px;
      margin-bottom: 18px;
    }

    .hero-hi {
      font-size: 14px;
      margin-bottom: 4px;
    }

    .hero-name {
      margin-bottom: 10px;
    }
  }

  /* Contact keeps its two columns down to the phone breakpoint: stacking it at
     900px made the page far taller than a 900px-tall window, and there is
     ample width for both columns at this size. */
  .contact-grid {
    gap: 24px;
  }

  /* The map stays. It was hidden here to save vertical space before the page
     scrolled internally; now that it does, hiding it only removed the one
     visual element on an otherwise plain column. */
  .origin-map {
    aspect-ratio: 5 / 2;
  }
}

@media (max-width: 700px) {
  body {
    font-size: 15.5px;
  }

  /* Contact stacks here rather than at 900px — see the note in that block. */
  .contact-grid {
    grid-template-columns: 1fr;
    gap: 20px;
  }

  /* Phones get generous spacing rather than a forced single-screen fit. The
     few genuinely dense pages scroll internally; the pager guarantees nothing
     is unreachable, so cramming the type was the wrong trade. */
  .page-inner {
    /* `.app` already stops above the rail, so no extra rail clearance is
       needed here — adding it double-counted the pager and left a dead band
       under every short page. */
    padding-top: 20px;
    padding-bottom: 20px;
  }

  /* Hide the scrollbar track on phones — the pager already signals position,
     and a visible bar cuts into the content column. */
  .page {
    scrollbar-width: none;
  }

  .page::-webkit-scrollbar {
    width: 0;
    height: 0;
  }

  .page-title {
    font-size: clamp(24px, 6.5vw, 30px);
  }

  .page-lede {
    font-size: 16px;
    margin-bottom: 20px;
  }

  .page-head {
    margin-bottom: 20px;
  }

  .about-lede p {
    font-size: 17px;
    margin-bottom: 16px;
  }

  .about-body p {
    font-size: 15.5px;
    line-height: 1.65;
    margin-bottom: 22px;
  }

  .role-line {
    font-size: 10px;
    padding: 6px 12px 6px 10px;
    margin-bottom: 16px;
  }





  .pillar {
    padding: 16px 0;
  }

  .pillar h3 {
    font-size: 16.5px;
  }

  .pillar p {
    font-size: 14.5px;
    line-height: 1.6;
  }

  /* One card per row on phones: two columns forced each list into a narrow
     column that wrapped awkwardly, while leaving vertical space unused. The
     page scrolls, so a single readable column is the better trade. */
  .skills {
    grid-template-columns: 1fr;
    gap: 14px;
  }

  /* The HUD panel folds: mark on the first line, credential name on the
     second, so nothing is crushed at phone width. */
  .hud-row {
    grid-template-columns: minmax(0, 1fr) 16px;
    gap: 10px;
    padding: 14px 16px;
  }

  /* Stacked on phones. An explicit width is required, not `width: auto`: the
     Johns Hopkins SVG declares only a viewBox, so it has no intrinsic size and
     an auto-width track collapses it to zero. */
  .hud-mark {
    width: 78px;
    height: 28px;
    justify-self: start;
    place-items: center start;
  }

  /* Touch has no hover, so the arrow is always visible on phones. */
  .hud-go {
    align-self: end;
    opacity: 0.75;
    transform: none;
  }

  .hud-value {
    grid-column: 1;
    font-size: 15.5px;
  }

  .hud-head {
    padding: 14px 16px;
  }

  .skillset {
    padding: 18px 18px 20px;
  }

  .skillset-list {
    gap: 9px 16px;
  }

  .skillset-title {
    font-size: 13px;
    margin-bottom: 13px;
    padding-bottom: 11px;
  }

  .skillset-list li {
    font-size: 13.5px;
  }

  .principle {
    padding: 20px 18px;
  }

  .principle h3 {
    font-size: 16px;
  }

  .principle p {
    font-size: 14.5px;
  }

  .pullquote p {
    font-size: 18px;
  }

  .project {
    padding: 20px 18px 22px;
  }

  /* The icon sits inline with the name on phones (see the max-width: 1000px
     block above), so only the sizing needs adjusting here. */
  .project-icon {
    width: 28px;
    height: 28px;
  }

  .project-icon svg {
    width: 15px;
    height: 15px;
  }

  .project-name {
    font-size: 20px;
  }

  .project-desc {
    font-size: 14.5px;
    line-height: 1.6;
    margin-bottom: 14px;
  }

  .tags {
    margin-bottom: 14px;
  }

  /* Single column on phones: at 2x2 the cards were narrow enough to rag each
     description over five lines. Stacked, the icon still sits beside the text
     so each card stays compact. */
  .offclock {
    grid-template-columns: 1fr;
    gap: 12px;
  }

  .offclock-item {
    padding: 18px 18px 20px;
    gap: 12px;
  }

  .offclock-icon {
    width: 32px;
    height: 32px;
  }

  .offclock-item p {
    font-size: 14px;
  }

  .contact-lede {
    font-size: 15.5px;
    margin-bottom: 18px;
  }

  .clink {
    padding: 12px 14px;
  }

  .clink-icon {
    width: 30px;
    height: 30px;
  }

  .field {
    margin-bottom: 11px;
  }

  .field textarea {
    min-height: 68px;
  }
}

@media (max-width: 560px) {
  .topbar-nav a {
    padding: 6px 9px;
    font-size: 12.5px;
  }

  .pillar {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .pillar-num {
    grid-row: auto;
    padding-top: 0;
  }
}

/* Short windows: scale the vertical rhythm down before falling back to
   internal scrolling, so most laptops still get a page that fits. */
@media (max-height: 880px) {
  .page-inner {
    padding-top: clamp(8px, 1.4vh, 18px);
    padding-bottom: clamp(8px, 1.4vh, 18px);
  }

  .page-head {
    margin-bottom: 12px;
  }

  .page-title {
    font-size: clamp(24px, 3vw, 32px);
  }

  .page-lede {
    margin-bottom: 12px;
    font-size: clamp(15px, 1.6vw, 17px);
  }

  .about-lede p {
    font-size: clamp(16px, 1.8vw, 19px);
    margin-bottom: 12px;
  }

  .about-body p {
    font-size: 15px;
    line-height: 1.6;
    margin-bottom: 16px;
  }

  .hero-blurb {
    margin-bottom: 22px;
  }

  .hero-tagline {
    margin-bottom: 16px;
  }


  .project {
    padding: 16px 17px 18px;
  }

  .project-desc {
    font-size: 14px;
    line-height: 1.5;
    margin-bottom: 10px;
  }

  .project-name {
    font-size: 20px;
  }

  .pillar {
    padding: 9px 0;
  }

  .pillar p {
    font-size: 14.5px;
    line-height: 1.5;
  }

  .skillset {
    padding: 16px 17px 18px;
  }

  .principle {
    padding: 18px 18px;
  }

  .offclock-item {
    padding: 16px 17px 18px;
  }

  .contact-lede {
    margin-bottom: 14px;
  }

  .field {
    margin-bottom: 11px;
  }

  .field textarea {
    min-height: 68px;
  }

  .origin-map {
    aspect-ratio: 2 / 1;
  }
}

/* Laptop-class heights: the contact page carries a form, a tile grid and a map,
   so it needs a second, tighter pass to stay inside the viewport. */
@media (max-height: 800px) {
  .contact-lede {
    margin-bottom: 10px;
  }

  .field {
    margin-bottom: 7px;
  }

  .field textarea {
    min-height: 0;
    height: 78px;
  }

  .origin-map {
    aspect-ratio: 3 / 1;
  }

  .clink {
    padding: 10px 13px;
  }
}

/* 720p-class laptops: the contact form is the tallest element on the site, so
   it gets one final trim to stay inside the viewport. */
@media (max-height: 760px) {
  .field {
    margin-bottom: 6px;
  }

  /* `rows` on the textarea sets an intrinsic height that min-height can't pull
     down — an explicit height is required to actually shrink it. */
  .field textarea {
    min-height: 0;
    height: 62px;
  }

  .origin-map {
    aspect-ratio: 4 / 1;
  }
}

/* 820px-tall tablets clear everything except the contact page by a hair. */
@media (min-height: 801px) and (max-height: 1200px) {
  .field textarea {
    min-height: 0;
    height: 92px;
  }

  .origin-map {
    aspect-ratio: 5 / 2;
  }
}

/* Respect the system setting. Content still arrives; it just doesn't move. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}
