/* Inter, self-hosted (no third-party request, no FOUT from a CDN round-trip).
   Variable weight 100-900, latin + latin-ext subsets only. */
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url("assets/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url("assets/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- theme tokens ----
   Dark is the default. The background is az-dev's own rgb(2,8,23) (Tailwind slate-950),
   with the rest of the neutrals moved onto the matching slate ramp so the whole surface
   reads navy rather than grey-black. The light-blue accent and white text are unchanged. */
:root, :root[data-theme="dark"] {
  --bg: #020817;
  --bg-soft: #0b1120;
  --card: #0f172a;
  --stroke: #1e293b;
  --text: #f8fafc;
  --dim: #94a3b8;
  --faint: #64748b;
  --accent: #4cc2ff;
  --accent-ink: #04121f;         /* text drawn on top of --accent */
  --accent-soft: rgba(76, 194, 255, 0.12);
  --accent-ring: rgba(76, 194, 255, 0.2);   /* the portrait's 4px ring — az-dev's primary/20 */
  --glow-btn: rgba(76, 194, 255, 0.3);      /* his resume button's 0 0 15px glow */
  --glow-btn-strong: rgba(76, 194, 255, 0.4);
  --glow: rgba(76, 194, 255, 0.5);
  --glow-mid: rgba(76, 194, 255, 0.2);
  --header-bg: rgba(2, 8, 23, 0.85);
  --code-bg: #0d1424;
  --code-text: #e6edf7;
  --code-border: rgba(255, 255, 255, 0.08);
  --gold: #e3b341;
  /* code-card syntax tokens */
  --c-kw: #c792ea;
  --c-id: #4cc2ff;
  --c-key: #a5b4fc;
  --c-str: #7ee787;
  --c-punct: #f0a26b;
  --c-bool: #ff9d7a;
  color-scheme: dark;
}

:root[data-theme="light"] {
  --bg: #f8fafc;
  --bg-soft: #f1f5f9;
  --card: #ffffff;
  --stroke: #dbe3ec;
  --text: #0f172a;
  --dim: #47576b;
  --faint: #64748b;
  /* a deeper blue on light backgrounds — #4cc2ff on white fails contrast badly */
  --accent: #0284c7;
  --accent-ink: #ffffff;
  --accent-soft: rgba(2, 132, 199, 0.1);
  --accent-ring: rgba(2, 132, 199, 0.22);
  --glow-btn: rgba(2, 132, 199, 0.28);
  --glow-btn-strong: rgba(2, 132, 199, 0.38);
  --glow: rgba(2, 132, 199, 0.28);
  --glow-mid: rgba(2, 132, 199, 0.12);
  --header-bg: rgba(248, 250, 252, 0.85);
  /* The code card INVERTS with the theme — az-dev's card is #ffffff in light mode
     (measured on his live DOM), and this card matches. Syntax colours here are
     az-dev's own light-mode set (Tailwind 600-level), which his site uses unchanged
     in BOTH themes; ours stay brighter in dark because a 600-level ramp goes muddy
     on navy. */
  --code-bg: #ffffff;
  --code-text: #1e293b;
  --code-border: #e2e8f0;
  --c-kw: #db2777;
  --c-id: #2563eb;
  --c-key: #9333ea;
  --c-str: #16a34a;
  --c-punct: #6b7280;
  --c-bool: #f97316;
  --gold: #a16207;
  color-scheme: light;
}
/* Before the toggle has run, follow the OS preference rather than flashing dark.
   ⚠️ This list MUST mirror :root[data-theme="light"] above, token for token. It has
   already drifted once: it carried --code-text but never --code-bg, so a first-visit
   light-mode user got a half-themed card. Add a light token in BOTH places or neither. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --bg: #f8fafc; --bg-soft: #f1f5f9; --card: #ffffff; --stroke: #dbe3ec;
    --text: #0f172a; --dim: #47576b; --faint: #64748b;
    --accent: #0284c7; --accent-ink: #ffffff; --accent-soft: rgba(2, 132, 199, 0.1);
    --accent-ring: rgba(2, 132, 199, 0.22);
    --glow-btn: rgba(2, 132, 199, 0.28); --glow-btn-strong: rgba(2, 132, 199, 0.38);
    --glow: rgba(2, 132, 199, 0.28); --glow-mid: rgba(2, 132, 199, 0.12);
    --header-bg: rgba(248, 250, 252, 0.85); --gold: #a16207;
    --code-bg: #ffffff; --code-text: #1e293b; --code-border: #e2e8f0;
    --c-kw: #db2777; --c-id: #2563eb; --c-key: #9333ea;
    --c-str: #16a34a; --c-punct: #6b7280; --c-bool: #f97316;
    color-scheme: light;
  }
}

* { box-sizing: border-box; margin: 0; }
html { scroll-behavior: smooth; }
body {
  background: var(--bg); color: var(--text);
  font: 16px/1.65 "Inter", -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
/* az-dev's container is `max-w-7xl` + `px-8` — 1280 wide on 32px gutters, so 1216px of
   content at 1280. Ours was 1140 on 22px gutters = 1096, about 11% narrower, which is
   most of what read as "his is bigger than mine": the type scale already matches him,
   the column didn't. This now also lines the content up with the nav, which is 1280 on
   the same 32px gutters — the alignment question left open on 7/30.
   The homepage hero is NOT affected: .hero.full carries its own max-width: 1030px. */
.wrap { max-width: 1280px; margin: 0 auto; padding: 0 32px; }
@media (max-width: 640px) { .wrap { padding: 0 22px; } }

/* Rendered height of the sticky header. .hero.full subtracts this to occupy exactly
   one screen, so it MUST match what the header actually measures at each width —
   the mobile bar is shorter. Verify with headerHeight + heroMinHeight === innerHeight
   rather than assuming. */
:root { --nav-h: 88px; }
@media (max-width: 767px) { :root { --nav-h: 80px; } }

/* Hero entrance timing, in one place so it can be tuned without hunting through
   five rules. --hero-in-step is the stagger between successive lines; the delays
   below are multiples of it. These govern the ON-LOAD entrance ONLY — the
   scroll-triggered .reveal transition is separate and deliberately quicker. */
:root {
  --hero-in-dur: 1.5s;
  --hero-in-step: 0.24s;
}

/* No bottom rule — az-dev's nav has border-bottom-width: 0 and a fully transparent
   background. The blur stays so text stays legible as content scrolls under it. */
/* Skip link — invisible until focused, then it drops into view. Without it a
   keyboard user tabs through nine header controls on every page before reaching
   content. WCAG 2.1 SC 2.4.1 (Bypass Blocks). */
.skipLink {
  position: absolute; left: 16px; top: -60px; z-index: 200;
  background: var(--accent); color: var(--accent-ink);
  padding: 10px 16px; border-radius: 0 0 10px 10px;
  font-size: 14px; font-weight: 600; text-decoration: none;
  transition: top 0.15s ease;
}
.skipLink:focus { top: 0; outline: 2px solid var(--accent-ring); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .skipLink { transition: none; } }
/* the skip target must not draw a focus ring when jumped to */
#main:focus { outline: none; }

header {
  position: sticky; top: 0; z-index: 20;
  background: var(--header-bg); backdrop-filter: blur(10px);
}
/* 88px is az-dev's measured nav height — it was 58px, which is the "his drops down
   farther than mine" difference. Declared as min-height rather than padding on
   purpose: padding is measured from the tallest child (the Resume pill), so a
   padding figure silently drifts whenever that child changes size — 29px padding
   produced a 95px bar, not 88. A fixed min-height stays true, and it has to stay in
   sync with .hero.full's calc(100svh - 88px). */
/* The nav and .wrap now share 1280px on 32px gutters, so the wordmark and the Resume
   button line up with the content below. The hero is still inset from both because
   .hero.full caps itself at 1030 — that inset is the hero moving, not the nav. */
.nav { display: flex; align-items: center; gap: 22px; min-height: 88px;
  max-width: 1280px; padding-inline: 32px; }
@media (max-width: 1320px) { .nav { max-width: 100%; padding-inline: 22px; } }
/* the whole wordmark is accent, not just the period (his "AZ." is entirely accent) */
.brand { font-size: 21px; font-weight: 800; letter-spacing: -0.5px; color: var(--accent); text-decoration: none; }
/* Right-aligned, az-dev's layout: the mark sits alone on the left and the links,
   theme toggle and Resume travel together as one group on the right. `margin-left:auto`
   on the first of them pushes the whole tail over (it was `margin: 0 auto`, which
   centred the links and stranded the toggle and pill against the edge). */
.navlinks { display: flex; align-items: center; gap: 26px; margin-left: auto; }
/* Scoped to .navlinks, NOT `.nav a` — `.nav a` (0,1,1) outranks both `.brand` (0,1,0)
   and `.nav .cta` (0,2,0 for colour, but 0,2,1 once :hover is added), so a blanket
   nav-link rule silently repaints the wordmark and turns the Resume label
   accent-on-accent the moment you hover it. Target the link group directly. */
.navlinks a { color: var(--text); text-decoration: none; font-size: 15px; font-weight: 500;
  transition: color 0.15s cubic-bezier(0.4, 0, 0.2, 1); }
.navlinks a:hover { color: var(--accent); }
.navlinks a.active { color: var(--accent); }
.nav a { text-decoration: none; }
/* 10px radius and weight 500 — az-dev's measured values. Was a 999px pill, which read
   as too round next to his. */
.nav .cta { color: var(--accent-ink); background: var(--accent); padding: 7px 15px; border-radius: 10px; font-weight: 500; font-size: 14px; }
.nav .cta:hover { filter: brightness(1.08); }

/* theme toggle, sitting between the links and the Resume button as his does */
.themeToggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0; border-radius: 10px;
  background: none; border: 0; color: var(--dim); cursor: pointer;
  transition: color 0.18s ease, background-color 0.18s ease;
}
.themeToggle:hover { color: var(--accent); background: var(--bg-soft); }
.themeToggle svg { width: 18px; height: 18px; display: block;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* show the icon for the theme you'd switch TO */
.themeToggle .moon { display: none; }
:root[data-theme="light"] .themeToggle .moon { display: block; }
:root[data-theme="light"] .themeToggle .sun { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .themeToggle .moon { display: block; }
  :root:not([data-theme]) .themeToggle .sun { display: none; }
}

/* The homepage hero owns the first screen on its own (az-dev's min-h-screen).
   88px is the sticky header (see .nav's padding — change one and you must change the
   other); svh keeps mobile browser chrome from causing a jump. */
/* Plain block flow. The two-column grid below belongs to the HOMEPAGE only — it used
   to live here, on the base selector, and the five inner pages (plain `.hero`, with a
   loose eyebrow / h1 / sub / buttons rather than one .heroText wrapper) auto-placed
   their children into its two tracks: the eyebrow took column 1 with the headline
   beside it in column 2, and `align-items: center` opened a column's worth of dead
   space between them. Anything added here applies to all six heroes — put homepage
   layout on .hero.full. */
.hero {
  padding: 84px 0 48px;
}
/* Genuinely centred: no top-padding bias. An earlier pass biased the content downward
   by 80px to shrink the gap to the next section, which is exactly why the hero sat ~60%
   down the screen instead of at the halfway mark. Centring wins; the residual gap is
   handled by trimming the next section's padding instead. */
/* Inset from the nav. On az-dev the hero container is shrink-to-fit rather than
   full-width, so its content lands ~115px right of the wordmark and ~103px left of
   the Resume button — you can drop a plumb line from either and miss the hero. Ours
   shared one .wrap with the nav, so both edges lined up exactly. This narrows the
   hero alone; the nav still spans the full wrap. */
.hero.full { min-height: calc(100svh - var(--nav-h)); padding-top: 0; padding-bottom: 0;
  max-width: 1030px; margin-inline: auto;
  /* the card column is a fixed width, not a fraction: it has one job, holding the
     447px `focuses` line without scrolling, and everything left over goes to the text
     column so the button + social row can stay on ONE line as it does on his */
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 505px);
  gap: 44px; align-items: center; }
.hero.full + section { padding-top: 8px; }
/* ---- hero responsive, on az-dev's breakpoints ----
   His hero is `flex-col lg:flex-row` with `text-center lg:text-left`, so it stacks
   AND centres below 1024 — not at 900, and not left-aligned. His H1 steps
   60 -> 48 -> 36 across lg/md/base. Ours had a single 900px stack that kept
   everything left-aligned, which is why resizing barely changed anything. */
@media (max-width: 1023px) {
  /* minmax(0, 1fr), NOT 1fr: a bare `1fr` track has an automatic minimum of
     min-content, so the code card's 450px-wide longest line propagates outward and
     forces the whole PAGE wider than the phone's window — 390px of window laying out
     488px of page. The card scrolls internally instead. */
  .hero { padding-top: 56px; }
  .hero.full { grid-template-columns: minmax(0, 1fr); gap: 34px; justify-items: center; }
  /* The hero KEEPS the full screen when stacked — az-dev's is min-h-screen at every
     width, and releasing it here is what let "About Me" show on first paint on a
     tablet. min-height (not height) can't crop: if the stacked content is taller than
     a screen the section simply grows past it. */
  .hero.full { padding: 24px 0 40px; max-width: 640px; }
  .hero .heroText { text-align: center; }
  /* .hero.full, not .hero — the base `.hero h1` clamp is (0,1,1) and would otherwise
     outrank a plain `.hero h1` here, leaving these rules inert. */
  .hero.full h1 { font-size: 48px; }
  /* .full only — the inner pages stay left-aligned when they stack; only the homepage
     hero centres, which is az-dev's `text-center lg:text-left`. */
  .hero.full p.sub { margin-inline: auto; }
  .heroActions { justify-content: center; }
  .hero .codeWrap { width: 100%; }
}
@media (max-width: 767px) {
  .hero.full h1 { font-size: 36px; }
}
/* Every value below is az-dev's, read off his live DOM at 1440x900 — the hero was
   uniformly larger and louder than his. His rhythm is one 24px gap between every
   block (Tailwind space-y-6), not the 18/16/22/38 ladder this had. */
/* The default .kicker-line is an EYEBROW — it labels the five inner pages ("About",
   "Software", "The Build"), and uppercase mono tracking is the correct treatment for
   that job. Left alone deliberately. */
.hero .kicker-line {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--accent); font-size: 14px; letter-spacing: 2.5px; text-transform: uppercase;
  margin-bottom: 18px; font-weight: 600;
}
/* The HOMEPAGE kicker is not an eyebrow — it's the opening words of a sentence that
   runs into the H1 ("Hello! I'm" / "Samuel Settie"), so it reads as prose and is set
   as prose. Sentence case in Inter, 16px/500, az-dev's `text-base font-medium
   text-primary`. The mono caps were the hard-to-read part Samuel flagged: 14px +
   2.5px tracking + uppercase stacks three legibility penalties on the smallest text
   in the hero. Scoped to .hero.full so the inner pages keep their eyebrow. */
.hero.full .kicker-line {
  font-family: inherit;
  font-size: 16px; font-weight: 500; letter-spacing: normal; text-transform: none;
  margin-bottom: 24px;
}
.hero h1 { font-size: clamp(36px, 6vw, 60px); line-height: 1; letter-spacing: -1px; }
.hero h1 span { color: var(--accent); }
.hero p.role { color: var(--dim); font-size: clamp(18px, 2.2vw, 24px); font-weight: 300; line-height: 1.33; margin-top: 24px; }
.hero p.sub { color: var(--dim); font-size: 16px; line-height: 1.5; margin-top: 24px; max-width: 46ch; }
/* buttons + socials share one row, wrapping together (az-dev pattern) */
.heroActions { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 24px; }
.heroActions .buttons { margin: 0; }
/* Bare icons — no box, no border, no background (az-dev has none either).
   His gap is 16px on 8px padding, but he has THREE icons in this row and we have
   four; at his spacing the row no longer fits beside the buttons and wraps to a
   second line, which it doesn't do on his. 10px/6px buys back the 4th icon's width. */
.hero .socials { display: flex; gap: 10px; align-items: center; }
.hero .socials a {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 6px; color: var(--dim); background: none; border: 0;
  transition: color 0.18s ease, transform 0.18s ease;
}
/* az-dev's exact hover value, read off his inline style: scale(1.2) rotate(5deg) */
.hero .socials a:hover { color: var(--accent); transform: scale(1.2) rotate(5deg); }
.hero .socials a:active { transform: scale(1.05) rotate(5deg); }
/* Lucide-style OUTLINE icons, 24px on a 2px round-joined stroke, drawn rather than
   filled — his are the Lucide set and the solid glyphs were the visible difference.
   fill:none is required: these paths are outlines, and filling them makes blobs. */
.hero .socials svg {
  width: 24px; height: 24px; display: block;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
@media (prefers-reduced-motion: reduce) {
  .hero .socials a { transition: color 0.18s ease; }
  .hero .socials a:hover { transform: none; }
}
.buttons { margin-top: 28px; display: flex; gap: 12px; flex-wrap: wrap; }
/* weight 500, not 600 — his buttons are the same size as ours but read lighter, and
   the extra stroke weight was most of why the row felt heavy */
.btn { display: inline-block; padding: 12px 20px; border-radius: 10px; text-decoration: none; font-weight: 500; font-size: 15px;
  transition: transform 0.18s ease, filter 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; }
.btn.primary { background: var(--accent); color: var(--accent-ink); }
.btn.ghost { border: 1px solid var(--stroke); color: var(--text); }
/* buttons lift slightly on hover and press back in — the tactile cue on az-dev */
.btn:hover { filter: brightness(1.1); transform: translateY(-2px) scale(1.03); }
.btn.primary:hover { box-shadow: 0 6px 20px var(--glow-mid); }
.btn.ghost:hover { border-color: var(--dim); }
.btn:active { transform: translateY(0) scale(0.97); }
.nav .cta { transition: transform 0.18s ease, filter 0.18s ease; }
.nav .cta:hover { transform: translateY(-1px) scale(1.04); }
.nav .cta:active { transform: scale(0.97); }

.proof { display: flex; gap: 12px; flex-wrap: wrap; padding: 6px 0 20px; }
.proof div {
  background: var(--bg-soft); border: 1px solid var(--stroke); border-radius: 10px;
  padding: 10px 16px; font-size: 13.5px; color: var(--dim);
}
.proof b { color: var(--text); display: block; font-size: 16px; }
.proof a { color: var(--accent); text-decoration: none; }

section { padding: 52px 0; border-top: 1px solid var(--stroke); }
section.first { border-top: none; }
h2 { font-size: 26px; letter-spacing: -0.3px; margin-bottom: 6px; }
.kicker { color: var(--accent); text-transform: uppercase; font-size: 12px; letter-spacing: 2px; font-weight: 700; }
/* 792px, matching the homepage About column — one prose measure site-wide. The old 660
   dated from when .wrap was 980 and left prose using barely half of the 1216 container.
   NOT full width: 1216px runs ~140 characters per line, well past where the eye can
   find the next line reliably. 792 is ~90. */
.lede { color: var(--dim); max-width: 792px; margin-bottom: 28px; }

.grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; }
.card { background: var(--card); border: 1px solid var(--stroke); border-radius: 14px; padding: 24px; }
.card h3 { font-size: 18px; margin-bottom: 8px; }
.card p { color: var(--dim); font-size: 14.5px; }
.card .tag { display: inline-block; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--accent); background: var(--accent-soft); border-radius: 6px; padding: 3px 8px; margin-bottom: 12px; font-weight: 700; }
.card a { color: var(--accent); text-decoration: none; font-size: 14px; }

/* ---- shared browser-frame cards (homepage hero, websites.html, software.html) ---- */
/* minmax raised to 420px so the wider .wrap keeps these at two columns — the video
   demos lose too much detail at three across. */
.folio { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 22px; margin-top: 22px; }
@media (max-width: 520px) { .folio { grid-template-columns: 1fr; } }
.site-card { background: var(--card); border: 1px solid var(--stroke); border-radius: 14px; overflow: hidden; display: flex; flex-direction: column; }
.site-card .chrome { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--bg-soft); border-bottom: 1px solid var(--stroke); }
.site-card .dots { display: flex; gap: 5px; }
.site-card .dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--stroke); display: block; }
.site-card .urlbar { flex: 1; font-size: 12px; color: var(--dim); background: var(--bg); border: 1px solid var(--stroke); border-radius: 6px; padding: 4px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.site-card .meta { padding: 16px 18px 18px; }
.site-card .meta h3 { margin: 0 0 6px; font-size: 17px; }
.site-card .meta p { margin: 0; color: var(--dim); font-size: 14px; line-height: 1.55; }
.site-card .meta .tag { margin-bottom: 10px; }

.plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; align-items: stretch; }
.plan { background: var(--card); border: 1px solid var(--stroke); border-radius: 14px; padding: 26px; display: flex; flex-direction: column; }
.plan.popular { border-color: var(--accent); position: relative; }
.plan.popular::before {
  content: "MOST POPULAR"; position: absolute; top: -10px; left: 22px;
  background: var(--accent); color: var(--accent-ink); font-size: 10px; font-weight: 800;
  letter-spacing: 1.5px; padding: 3px 10px; border-radius: 6px;
}
.plan h3 { font-size: 17px; }
.price { font-size: 34px; font-weight: 800; margin: 10px 0 2px; }
.price small { font-size: 14px; color: var(--dim); font-weight: 500; }
.plan ul { list-style: none; padding: 0; margin: 18px 0; flex: 1; }
.plan li { padding: 7px 0; color: var(--dim); font-size: 14.5px; border-bottom: 1px dashed var(--stroke); }
.plan li::before { content: "✓  "; color: var(--accent); font-weight: 700; }
.plan .btn { text-align: center; }
.finePrint { color: var(--faint); font-size: 12.5px; margin-top: 16px; }

.ghchart { width: 100%; margin-top: 18px; background: var(--bg-soft); border: 1px solid var(--stroke); border-radius: 12px; padding: 18px; }
.ghchart img { width: 100%; height: auto; display: block; }
.ghchart .cap { color: var(--faint); font-size: 12.5px; margin-top: 10px; }

.nowbox { background: var(--bg-soft); border: 1px solid var(--stroke); border-left: 3px solid var(--gold); border-radius: 10px; padding: 18px 20px; }
.nowbox .kicker { color: var(--gold); }
ul.plain { list-style: none; padding: 0; }
ul.plain li { padding: 8px 0; border-bottom: 1px dashed var(--stroke); color: var(--dim); font-size: 15px; }
ul.plain b { color: var(--text); }

/* ---- hero code card ---- */
/* A tilted gradient rectangle sits behind the card and sways continuously. The
   wrapper exists because .codecard needs overflow:hidden for its own corners, which
   would clip a pseudo-element drawn on it. */
.codeWrap { position: relative; }
/* inset: 0 — the rectangle is the SAME SIZE as the card, so it only peeks out at the
   corners as it rotates, rather than framing the card permanently. Alphas (0.20 -> 0.05)
   and blur(1px) are az-dev's own values: bg-gradient-to-tr from-primary/20 to-primary/5. */
.codeWrap::before {
  content: ""; position: absolute; inset: 0; z-index: 0; border-radius: 16px;
  background: linear-gradient(to top right, var(--glow-mid), var(--accent-soft));
  filter: blur(1px);
  animation: sway 11s ease-in-out infinite;
}
.codeWrap .codecard { position: relative; z-index: 1; }
@keyframes sway {
  0%, 100% { transform: rotate(5deg); }
  50%      { transform: rotate(-5deg); }
}
/* The card inverts with the theme (see --code-bg). The border has to be a token too:
   a white/8 border is invisible on a white card. */
.codecard { background: var(--code-bg); border: 3px solid var(--code-border); border-radius: 16px; overflow: hidden;
  box-shadow: 0 0 30px var(--accent-soft);
  transition: transform 0.24s ease; }
.codeWrap:hover .codecard { transform: translateY(-6px); }
.codecard .bar { display: flex; align-items: center; padding: 13px 16px; border-bottom: 1px solid var(--stroke); }
.codecard .lights { display: flex; gap: 7px; }
.codecard .lights i { width: 11px; height: 11px; border-radius: 50%; display: block; }
.codecard .lights i:nth-child(1) { background: #ff5f57; }
.codecard .lights i:nth-child(2) { background: #febc2e; }
.codecard .lights i:nth-child(3) { background: #28c840; }
.codecard .file { margin-left: auto; color: var(--faint); font-size: 12.5px; font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.codecard pre {
  /* az-dev's own metrics: p-6 (24px) and font-mono text-sm, i.e. 14px on a 20px line
     box (1.43). The 2.1 line-height this replaced was not a design choice — it was
     added to inflate the card's height and close the gap under the hero, on a
     comparison that turned out to be wrong. It was also the single largest source of
     the hero feeling oversized: nine lines at 2.1 is ~75px of pure padding. */
  /* 20px horizontal rather than a flat 24: our object carries three more keys than
     his AND longer values, so the `focuses` line needs every pixel. It is 447px wide
     at this font size — the binding constraint on how far the hero can be inset. */
  margin: 0; padding: 24px 20px; overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 13.5px; line-height: 1.55; color: var(--code-text); tab-size: 2;
}
.codecard .cm { color: var(--faint); }
.codecard .kw { color: var(--c-kw); }
.codecard .id { color: var(--c-id); }
.codecard .k  { color: var(--c-key); }
.codecard .s  { color: var(--c-str); }
.codecard .p  { color: var(--c-punct); }
.codecard .b  { color: var(--c-bool); }
@media (max-width: 900px) { .codecard pre { font-size: 12.5px; padding: 18px; } }

/* ---- homepage short-about block ---- */
/* az-dev's split is `lg:col-span-8` + `lg:col-span-4` of a 12-col grid with `gap-14`,
   i.e. 2:1 at 56px. Ours was 1.35:1 at 34px, which made the portrait column 43% of the
   width instead of 33% — the portrait floated in an empty field and the two fact cards
   were squeezed narrow enough that Experience wrapped much taller than Education. */
/* margin-top 0, not 22px: .sectionHead already carries his 48px margin-bottom, and the
   extra 22 was stacking on top of it — measured 70px from heading to "Bio" against his
   48px. */
/* A real 12-column grid, because 8/4 of one is NOT 2fr/1fr: the eleven 56px gaps sit
   inside the tracks, so his text column is 8*50+7*56 = 792 and the portrait column
   4*50+3*56 = 368. A 2:1 split gives 773/387 — close, but off by ~19px each way. */
.aboutGrid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 56px; align-items: start; margin-top: 0; }
/* span 12 while the portrait card is out — the text column would otherwise stop at 8
   and leave the right third empty. Change back to `span 8` when .meCard returns. */
.aboutGrid > *:first-child { grid-column: 1 / -1; }
.aboutGrid > .meCard { grid-column: span 4; }
@media (max-width: 860px) { .aboutGrid > *:first-child, .aboutGrid > .meCard { grid-column: 1 / -1; } }
/* Capped at his measured 792px rather than uncapped. His paragraph fills an 8-col
   column and happens to be 792 wide; now that ours spans all 12 the same rule would
   run the line to 1216px, which is well past a readable measure. The fact cards below
   still use the full width — only the prose is held back. */
.aboutGrid .lede { max-width: 792px; line-height: 1.5; }
@media (max-width: 860px) { .aboutGrid { grid-template-columns: 1fr; } }
/* ---- stacked layouts centre their content (<=860px) ----
   Everything in the page body reads centred once the columns collapse, matching what
   .meCard already did on the homepage. .lede carries its own max-width, so it needs
   margin-inline:auto to actually sit in the middle rather than hug the left edge. */
@media (max-width: 860px) {
  main.wrap > section, main.wrap > .hero { text-align: center; }
  main.wrap .lede, main.wrap .sub { margin-inline: auto; }
  main.wrap .buttons, main.wrap .socialRow { justify-content: center; }
  main.wrap .plain li { text-align: left; }   /* list items stay readable left-aligned */
  /* Code is never centred. The card clips with overflow-x:hidden, so centring its <pre>
     makes long lines overhang BOTH edges instead of just the clipped right one — which
     pushed the homepage from 2 overflowing elements to 6 at 375px. */
  .hero .codeWrap, .hero .codecard, .hero .codecard pre { text-align: left; }
}

/* ---- about.html "The path": narrative left, timeline right ----
   Same 12-column 8/4 split as .aboutGrid, so the prose column lands at exactly 792px —
   the .lede measure — and the timeline takes the 368px that was empty. */
.pathGrid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 56px;
  align-items: start; }
/* min-width:0 on the ITEMS, not just minmax(0,…) on the tracks. A grid item's automatic
   minimum is its min-content width, so the prose column refused to shrink below its
   longest unbreakable run and pushed the page 21px wider than the viewport on a 375px
   screen. Sizing the tracks does not fix the items — both are needed. Same family as
   the hero's minmax(0,1fr) note above. */
.pathGrid > * { min-width: 0; }
.pathGrid > *:first-child { grid-column: span 8; }
.pathGrid > .timeline { grid-column: span 4; }
@media (max-width: 860px) {
  /* Collapse to ONE column, not just span 1/-1. Keeping twelve tracks here overflows
     narrow screens: eleven 34px gaps come to 374px, which is wider than the 331px
     container at 375px, so the tracks collapse to 0 and an item spanning 1/-1 ends up
     374px wide — the gap total. .aboutGrid already does this; .pathGrid did not. */
  /* minmax(0, 1fr), not 1fr: the timeline below is width:max-content, and a bare 1fr
     track takes its automatic minimum from the item's content — so max-content would
     drag the single track out to 442px and overflow again. */
  .pathGrid { grid-template-columns: minmax(0, 1fr); gap: 34px; justify-items: center; }
  .pathGrid > *:first-child, .pathGrid > .timeline { grid-column: 1 / -1; }
  /* The timeline centres as a BLOCK but keeps its own text left-aligned — the rail and
     dots run down its left edge, so centring the text inside it would leave them
     orphaned against ragged lines. */
  .timeline { text-align: left; width: max-content; max-width: 100%; }
}
/* The rail and the dots have to share a centre line, which is fiddly enough to be worth
   stating: the <ol> has 22px of left padding, so a dot at left:-22px starts at the ol's
   border-box left edge. Dot is 10px wide on a 2px border = 14px outer, centre 7px. The
   rail therefore sits at left:6px with width:2px — centre 7px. Change one, change both. */
.timeline { list-style: none; margin: 0; padding: 0 0 0 22px; position: relative; }
.timeline::before { content: ""; position: absolute; left: 6px; top: 7px; bottom: 7px;
  width: 2px; background: var(--accent-ring); }
.timeline li { position: relative; }
.timeline li + li { margin-top: 18px; }
.timeline li::before { content: ""; position: absolute; left: -22px; top: 5px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--bg); border: 2px solid var(--accent); }
.timeline .when { display: block; font-size: 12px; letter-spacing: 1.5px;
  text-transform: uppercase; color: var(--accent); font-weight: 700; }
.timeline .what { display: block; font-size: 14px; color: var(--dim); margin-top: 3px;
  line-height: 1.45; }

/* Section heading with the accent rule beneath it, az-dev's "About Me" treatment.
   display:inline-block is what makes it work — the box shrinks to the text, so the
   ::after bar can be a fraction of the WORDS rather than a fraction of the column.
   (His carries a text-center class that does nothing for exactly this reason.)
   Bar measured at 4px tall, 8px below the text, one third of the heading width. */
.sectionHead {
  display: inline-block; position: relative;
  font-size: 36px; font-weight: 700; letter-spacing: -0.5px;
  margin-bottom: 48px;
}
.sectionHead::after {
  content: ""; position: absolute; left: 0; bottom: -8px;
  width: 33%; height: 4px; background: var(--accent); border-radius: 2px;
}
@media (max-width: 640px) { .sectionHead { font-size: 28px; margin-bottom: 34px; } }

/* Heading-with-icon rows (Bio, Education, Experience). The icon is a 20px accent
   Lucide outline, so it needs the same fill:none/stroke treatment as the socials. */
.blockHead, .fact h4 { display: flex; align-items: center; gap: 8px; }
.blockHead svg, .fact h4 svg {
  width: 20px; height: 20px; flex: none; color: var(--accent);
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.blockHead { font-size: 20px; font-weight: 600; margin-bottom: 8px; }

/* Cards size to their own content — az-dev's do too. His `h-full` is on a card nested
   inside a wrapper that is NOT full-height, so it never fills: measured on his live
   page his two cards are 198px and 234px, not equal. Stretching ours put ~158px of
   dead space under Education.
   What makes his read tidy is that his step is only 36px. Ours is large because the
   content is lopsided (3 lines against 9), which is a content problem, not a layout
   one — see the Education card's line count. */
.factGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 22px;
  align-items: start; }
@media (max-width: 560px) { .factGrid { grid-template-columns: 1fr; } }
.fact { background: var(--card); border: 1px solid var(--stroke); border-radius: 12px; padding: 24px; }
/* sizes lifted a step across the board — his card type is 18/16/14 against our
   15/14.5/13.5, which is the "mine is very small" difference */
.fact h4 { margin: 0 0 12px; font-size: 18px; font-weight: 600; color: var(--text); letter-spacing: 0; }
/* 500, not 700 — his entry titles are medium weight. Bold made every row shout and was
   a large part of why this card read heavier than his. */
.fact b { display: block; font-size: 16px; font-weight: 500; }
.fact span { display: block; color: var(--dim); font-size: 14px; }
/* his rows breathe: 8px between every line inside an entry, 16px between entries */
.fact .row > * + * { margin-top: 8px; }
.fact .row + .row { margin-top: 16px; }
/* Name and qualifier share ONE line — "City of Pittsburgh  Innovation & Performance",
   "B.S. Computer Science  University of Pittsburgh" — instead of stacking. That is two
   lines saved per entry, which is most of why this card ran so tall.
   The b/span here must be forced back to inline: the base .fact rules set both to
   display:block, which would defeat the flex row's baseline alignment. */
.fact .orgLine { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; }
.fact .orgLine b, .fact .orgLine span { display: inline; }
/* An em dash separates the name from its qualifier. On ::before rather than in the
   markup so the two stay one text node per element, and so a wrap puts the dash at the
   head of the second line instead of orphaning it at the end of the first. */
.fact .orgLine span::before { content: "—"; margin-right: 8px; color: var(--faint); }
/* Dropped once the qualifier wraps onto its own line — a leading dash there reads as a
   typo rather than a join. site.js adds .wrapped; see the note there on why it measures
   with the dash present. */
.fact .orgLine.wrapped span::before { content: none; }
/* Italic on the qualifier, matching his resume, where the sub-line under each role
   ("Sole Proprietorship", the project tech lists) is the italic one. */
.fact .orgLine span { font-style: italic; }
/* The vertical rule is what ties the three stints to the employer above them — a
   bracket down the side of the group, not a divider between it and the heading.
   accent-ring rather than --stroke so it reads as deliberate grouping and not as
   another card edge; it is a token, so it holds in both themes. */
.fact .subItems { padding-left: 14px; border-left: 2px solid var(--accent-ring); }
.fact .subItems > span + span { margin-top: 8px; }
/* .stints is the dated variant: same bracket, plus a two-column grid for the dates */
.fact .stints { display: grid; grid-template-columns: max-content 1fr; gap: 8px 16px; }
.fact .stints > span + span { margin-top: 0; }
/* right-aligned so the dates end on a common edge; the team column is max-content so
   the names still start flush left */
.fact .stints .when { text-align: right; }
/* No stacking fallback. A narrow-card breakpoint was tried and measured WORSE at every
   width — 410px against 358px for the Experience card at both 768 and 375, because
   stacking spends a whole line on each team name while the two-column form just wraps
   the one long date range. `1fr` lets the date column shrink and wrap rather than
   overflow, so the grid holds down to a 331px card with no horizontal scroll. */
/* portrait card */
/* az-dev's about column is NOT a card. Read off his live DOM: a bare flex column,
   transparent background, 0 border, 0 padding, 24px gap, align-items:flex-start —
   it sits directly on the page. Ours was a bordered, centred box, which is the
   difference Samuel was pointing at. Every value below is his, measured, not guessed.
   His column is `lg:col-span-4` of a 12-col grid beside `lg:col-span-8` of text. */
.meCard { background: none; border: 0; border-radius: 0; padding: 0; text-align: left;
  display: flex; flex-direction: column; align-items: flex-start; gap: 24px; }
/* 160px circle on a 4px ring at accent/20 (his border-4 border-primary/20). The ring
   must be a token or it breaks one of the two themes. */
.portrait {
  width: 160px; height: 160px; margin: 0; border-radius: 50%;
  border: 4px solid var(--accent-ring); overflow: hidden; background: var(--bg-soft);
  display: flex; align-items: center; justify-content: center; flex: none;
}
.portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }
.portrait .initials { font-size: 46px; font-weight: 800; color: var(--faint); letter-spacing: -1px; }
/* name + role are ONE flex child on his (a single div), so they sit tight together
   instead of taking the column's 24px gap between them */
.meCard .whoBlock { display: flex; flex-direction: column; gap: 0; }
.meCard h3 { font-size: 24px; font-weight: 700; line-height: 1.33; margin-bottom: 0; }
.meCard .who { color: var(--dim); font-size: 16px; }
/* his chips: 12px/600, 2px 10px, full radius, 1px stroke, accent at 10% */
.chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-start; margin: 0; }
.chips span { background: var(--accent-soft); border: 1px solid var(--stroke); border-radius: 999px; padding: 2px 10px; font-size: 12px; color: var(--text); font-weight: 600; }
/* shrink-to-fit, not full width: his is an inline-flex 40px pill, 8px/16px padding.
   The glow is his too, measured: 0 0 15px rgba(primary, 0.3). */
.meCard .btn { width: auto; align-self: flex-start; height: 40px; padding: 8px 16px;
  display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500;
  box-shadow: 0 0 15px var(--glow-btn); }
.meCard .btn:hover { box-shadow: 0 0 25px var(--glow-btn-strong); }
@media (max-width: 860px) {
  /* his column is `items-center lg:items-start` — centred only once it stacks */
  .meCard { align-items: center; text-align: center; }
  .meCard .whoBlock { align-items: center; }
  .chips { justify-content: center; }
}

/* ---- contact: info column + form ---- */
.contactGrid { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: 30px; align-items: start; margin-top: 26px; }
@media (max-width: 860px) { .contactGrid { grid-template-columns: 1fr; } }
.infoList { margin-top: 20px; }
.infoRow { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 20px; }
.infoRow .ico { flex: none; width: 38px; height: 38px; border-radius: 10px; background: var(--accent-soft); color: var(--accent); display: flex; align-items: center; justify-content: center; }
.infoRow .ico svg { width: 18px; height: 18px; fill: currentColor; }
.infoRow b { display: block; font-size: 14.5px; }
.infoRow span, .infoRow a { color: var(--dim); font-size: 14px; text-decoration: none; word-break: break-word; }
.infoRow a:hover { color: var(--accent); }
.socialRow { display: flex; gap: 12px; margin-top: 16px; }
/* az-dev's contact icons: 44px filled circle, NO border, and on hover the whole circle
   fills with the accent while the glyph goes dark. */
.socialRow a {
  width: 44px; height: 44px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-soft); border: 0; color: var(--dim);
  transition: color 0.18s ease, background-color 0.18s ease;
}
.socialRow a:hover { background: var(--accent); color: var(--accent-ink); }
.socialRow svg { width: 20px; height: 20px; fill: currentColor; }

.formCard { background: var(--card); border: 1px solid var(--stroke); border-radius: 16px; padding: 26px; }
.formRow { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 560px) { .formRow { grid-template-columns: 1fr; } }
.field { margin-bottom: 18px; }
.field label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 8px; }
.field input, .field textarea, .field select {
  width: 100%; background: var(--bg); border: 1px solid var(--stroke); border-radius: 10px;
  padding: 12px 14px; color: var(--text); font: inherit; font-size: 15px;
}
.field textarea { min-height: 132px; resize: vertical; }
.field input::placeholder, .field textarea::placeholder { color: var(--faint); }
.field select { appearance: none; cursor: pointer; background-image: linear-gradient(45deg, transparent 50%, var(--dim) 50%), linear-gradient(135deg, var(--dim) 50%, transparent 50%); background-position: calc(100% - 19px) center, calc(100% - 14px) center; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 38px; }
.field input:focus, .field textarea:focus, .field select:focus { outline: none; border-color: var(--accent); }
.formCard button {
  width: 100%; background: var(--accent); color: var(--accent-ink); border: 0; border-radius: 10px;
  padding: 14px; font: inherit; font-weight: 700; font-size: 15px; cursor: pointer;
}
.formCard button:hover { filter: brightness(1.08); }
.formCard button:disabled { opacity: 0.55; cursor: not-allowed; filter: none; }
.formNote { color: var(--faint); font-size: 13px; margin-top: 14px; text-align: center; }
.formNote a { color: var(--accent); text-decoration: none; }
.formStatus { font-size: 14px; margin-top: 14px; text-align: center; display: none; }
.formStatus.show { display: block; }
.formStatus.ok { color: var(--c-str); }
.formStatus.err { color: #ff9d7a; }

/* ---- footer ---- */
footer { border-top: 1px solid var(--stroke); padding: 40px 0 60px; color: var(--faint); font-size: 14px; }
footer a { color: var(--dim); }
.footGrid { display: flex; gap: 24px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
.footGrid .tagline { color: var(--dim); font-size: 14px; margin-top: 10px; max-width: 42ch; }
.footGrid .right { text-align: right; }
.footGrid .right .socialRow { justify-content: flex-end; margin-top: 0; }
.footGrid .right .socialRow a { width: 36px; height: 36px; }
.footGrid .copy { margin-top: 14px; color: var(--faint); font-size: 13.5px; }
@media (max-width: 560px) { .footGrid .right, .footGrid .right .socialRow { text-align: left; justify-content: flex-start; } }

/* back-to-top */
#toTop {
  position: fixed; right: 26px; bottom: 26px; z-index: 30;
  width: 48px; height: 48px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--accent); color: var(--accent-ink); display: none;
  align-items: center; justify-content: center;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
}
#toTop.show { display: flex; }
#toTop svg { width: 22px; height: 22px; fill: currentColor; }
#toTop:hover { filter: brightness(1.08); }

/* ---- shared card hover: lift ----
   NOTE: the lift uses `translate`, not `transform`, on purpose. Most of these cards
   are also .reveal elements, whose entrance animates `transform` — sharing one
   property would make the hover fight the entrance (and .js .reveal's transition,
   being more specific, would drop the other properties entirely). `translate` is a
   separate animatable property, so the two never collide. */
.card, .fact, .site-card, .plan, .formCard { transition: translate 0.24s ease; }
.card:hover, .fact:hover, .site-card:hover, .plan:hover, .formCard:hover,
.card:focus-within, .fact:focus-within, .site-card:focus-within,
.plan:focus-within, .formCard:focus-within { translate: 0 -6px; }

/* ---- cursor-tracking glow ----
   Reproduces az-dev's two hover layers. Their values were read from his live DOM *while
   hovering* — they don't exist at rest, which is why inspecting a resting card missed
   them entirely the first time:
     ::before  a blurred radial gradient whose centre follows the pointer
     ::after   a 1px accent border revealed only near the pointer, via a radial mask
   His card's box-shadow never changes on hover, so there is no whole-card glow.
   --mx/--my are written by site.js on mousemove.
   .plan is deliberately excluded: it needs overflow visible for the MOST POPULAR badge
   at top:-10px, which overflow:hidden would clip. Plans keep the lift only. */
.card, .fact, .site-card { position: relative; overflow: hidden; }
.card::before, .fact::before, .site-card::before,
.card::after,  .fact::after,  .site-card::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; opacity: 0; transition: opacity 0.3s ease;
}
/* blur is 42px, not his 60px: his cards are ~448px wide and mine are as narrow as 258px,
   where a 60px blur smears the 150px gradient across the whole card and reads as a flat
   wash instead of something tracking the pointer. This is the one knob to turn if the
   glow is too subtle (lower) or too obvious (higher). */
.card::before, .fact::before, .site-card::before {
  z-index: 0; filter: blur(42px);
  background: radial-gradient(150px 150px at var(--mx, 50%) var(--my, 50%),
              var(--glow), var(--glow-mid) 40%, transparent 70%);
}
.card::after, .fact::after, .site-card::after {
  z-index: 1; border: 1px solid var(--accent); filter: blur(1px);
  -webkit-mask-image: radial-gradient(120px 120px at var(--mx, 50%) var(--my, 50%), #fff 30%, transparent 65%);
          mask-image: radial-gradient(120px 120px at var(--mx, 50%) var(--my, 50%), #fff 30%, transparent 65%);
}
.card:hover::before, .fact:hover::before, .site-card:hover::before,
.card:hover::after,  .fact:hover::after,  .site-card:hover::after { opacity: 1; }
/* keep real content above the glow layer so text is never washed out */
.card > *, .fact > *, .site-card > * { position: relative; z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  .codeWrap::before { animation: none; }
  .hero .codeWrap { animation: none; }
  .card, .fact, .site-card, .plan, .formCard, .codecard, .btn, .nav .cta { transition: none; }
  .card:hover, .fact:hover, .site-card:hover, .plan:hover, .formCard:hover { translate: none; }
  .codeWrap:hover .codecard, .btn:hover, .btn:active, .nav .cta:hover { transform: none; }
  .card::before, .fact::before, .site-card::before,
  .card::after, .fact::after, .site-card::after { transition: none; }
}

/* ---- mobile navigation ----
   Previously the links were simply `display: none` below 640px with nothing to
   replace them, so on a phone the site had NO navigation at all beyond the
   wordmark. az-dev hides his links below 768 too, but swaps in a menu button —
   this does the same. The button is hidden above the breakpoint and the links
   come back inline, so desktop is untouched. */
.navToggle {
  display: none;
  align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0; border-radius: 10px;
  background: none; border: 0; color: var(--text); cursor: pointer;
  transition: color 0.18s ease, background-color 0.18s ease;
}
.navToggle:hover { color: var(--accent); background: var(--bg-soft); }
.navToggle svg {
  width: 22px; height: 22px; display: block;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.navToggle .iconClose { display: none; }
header.menuOpen .navToggle .iconClose { display: block; }
header.menuOpen .navToggle .iconOpen { display: none; }

@media (max-width: 767px) {
  .navToggle { display: inline-flex; order: 2; }
  .nav { flex-wrap: wrap; min-height: 64px; }
  /* Zero-height full-width pseudo-element = a flex line break. Everything ordered
     after it starts on a new row, which is what lets the Resume button leave the top
     bar without moving it in the HTML on six pages. */
  .nav::after { content: ""; order: 2; flex-basis: 100%; height: 0; }
  /* push the theme + menu buttons to the right of the wordmark on the first row */
  .themeToggle { margin-left: auto; }

  /* Resume drops out of the top bar into the panel, right-aligned (az-dev puts his
     bottom-left under Contact instead). It does NOT get a row of its own — it shares
     the second flex line WITH the link stack, so it sits across from "Home" and
     nothing below it is pushed down. `align-self: flex-start` pins it to the top of
     that line and the 8px nudge centres its 37px height against the 52px link. */
  .nav .cta { order: 5; margin-left: auto; align-self: flex-start; margin-top: 8px; display: none; }
  header.menuOpen .nav .cta { display: inline-flex; align-items: center; }

  /* The link stack takes the rest of line two. `flex: 1 1 0` rather than
     flex-basis:100% — a 100% basis would force it onto a line of its own and put
     Resume back on a separate row. */
  .navlinks {
    order: 4; flex: 1 1 0; min-width: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    margin-left: 0;
    max-height: 0; overflow: hidden;
    transition: max-height 0.28s ease;
  }
  header.menuOpen .navlinks { max-height: 320px; }
  header.menuOpen .nav { padding-bottom: 14px; }
  /* no rules between items — Samuel didn't want the divider lines */
  .navlinks a { padding: 13px 2px; font-size: 16px; }

  /* Long lines WRAP instead of scrolling sideways, which is what his card does at
     this width. pre-wrap breaks at the spaces after commas, so an array splits
     between values rather than mid-token. */
  .codecard pre { white-space: pre-wrap; }
}
@media (prefers-reduced-motion: reduce) {
  .navlinks { transition: none; }
}

/* ---- motion: hero entrance + scroll reveal ---- */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: none; }
}
/* Slower and more spaced out than the first pass — the original ~0.3s of total stagger
   read as everything arriving at once. Left column top-to-bottom, then the card. */
.hero .kicker-line, .hero h1, .hero p.role, .hero p.sub, .hero .heroActions {
  animation: rise-in var(--hero-in-dur) cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
.hero h1 { animation-delay: calc(var(--hero-in-step) * 1); }
.hero p.role { animation-delay: calc(var(--hero-in-step) * 2); }
.hero p.sub { animation-delay: calc(var(--hero-in-step) * 3); }
.hero .heroActions { animation-delay: calc(var(--hero-in-step) * 4); }
/* the card slides in from the right rather than just appearing */
@keyframes slide-in-right {
  from { opacity: 0; transform: translateX(56px); }
  to   { opacity: 1; transform: none; }
}
.hero .codeWrap { animation: slide-in-right calc(var(--hero-in-dur) * 1.15)
  cubic-bezier(0.2, 0.7, 0.3, 1) calc(var(--hero-in-step) * 2.5) both; }

/* .reveal elements start hidden only when JS is running (html.js) */
.js .reveal {
  opacity: 0; transform: translateY(18px);
  /* the last three keep card hover alive on .reveal elements — this rule is more
     specific than the .card/.fact/... transition and would otherwise replace it */
  transition: opacity 0.65s cubic-bezier(0.2, 0.7, 0.3, 1) var(--d, 0s),
              transform 0.65s cubic-bezier(0.2, 0.7, 0.3, 1) var(--d, 0s),
              translate 0.24s ease, border-color 0.24s ease, box-shadow 0.24s ease;
}
.js .reveal.in { opacity: 1; transform: none; }
.grid3 .reveal:nth-child(2) { --d: 0.07s; }
.grid3 .reveal:nth-child(3) { --d: 0.14s; }
.grid3 .reveal:nth-child(4) { --d: 0.07s; }
.grid3 .reveal:nth-child(5) { --d: 0.14s; }
.grid3 .reveal:nth-child(6) { --d: 0.21s; }

@media (prefers-reduced-motion: reduce) {
  .hero .kicker-line, .hero h1, .hero p.role, .hero p.sub,
  .hero .heroActions, .hero .codeWrap { animation: none; }
  .js .reveal { opacity: 1; transform: none; transition: none; }
}
