/* NAS Digital — lunar rebuild, 2026-09-07.
 *
 * Nathan didn't like the previous design and said the feeling predated that
 * day's changes, so this is a real redesign rather than a retouch. The
 * direction is his: space and lunar, dark, glassy, an interactive starfield,
 * and explicitly no cyan - he would rather redraw the logo to match the site
 * than hold the site hostage to a mark he made the day before.
 *
 * PALETTE. Deep space ground, bone-white type, one warm accent. The accent is
 * not a new invention: #e0915a was already in this file, sampled from the
 * burnt amber in Nathan's own portrait. Promoting it and retiring the cyan
 * does three things at once - it satisfies "no cyan", it suits earthrise over
 * a grey moon far better than a neon blue did, and it lands a neighbour of
 * With Nate's ember #e8612a, so his two brands finally rhyme instead of
 * clashing. Calibrated against Varda's site, which pairs a deep indigo ground
 * with warm amber and looks genuinely expensive. Colour only - Nathan looked
 * at their structure and did not want it.
 *
 * TYPE. Self-hosted, and that is a correctness requirement rather than a
 * preference: /privacy/ states this site loads no third-party resources, so a
 * fonts.googleapis.com link would make that sentence false. Latin subsets,
 * 130KB for the whole family set, no request leaves the origin.
 *
 * The previous note here argued for no webfont at all on the grounds that the
 * site sells speed. That argument was right about the cost and wrong about
 * where to spend it: 130KB of self-hosted, preloaded, swap-displayed woff2 is
 * a smaller tax than the hero video that used to sit on this page, and type is
 * doing much more of the work than that video ever did.
 */

:root {
  --ink: #08090d;          /* deep space - the ground everything sits on */
  --surface: #101218;      /* glass panel base */
  --surface-2: #161922;
  --line: #232733;
  --line-soft: #1a1e26;

  --text: #eef1f6;         /* bone white */
  --text-2: #a7b0bf;
  --text-3: #6f7889;

  --accent: #e0915a;       /* earthrise amber, carried over from the portrait */
  --accent-2: #f0b183;     /* lift, for hover */
  --accent-deep: #2a1a12;  /* the amber's own shadow, for tag/pill grounds */
  --moon: #f4f6fa;         /* specular edges and the mark itself */
  /* The logo's own blue. Deliberately NOT the interface accent: amber owns
     links, buttons and focus, this owns the brand object. Two colours only
     fight when they are competing for the same job. */
  --mark: #2a7fff;

  /* Retired 2026-09-07. --cyan is aliased rather than deleted so that any rule
     this rebuild has not reached yet degrades to the new accent instead of
     silently losing its colour. Nothing new should reference it; when the
     grep comes back empty, delete these two lines. */
  --cyan: var(--accent);
  --cyan-deep: var(--accent-deep);
  --amber: var(--accent);

  --wrap: 1120px;
  --narrow: 68ch;
  --pad: clamp(20px, 5vw, 48px);

  --f-display: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --f-body: "Satoshi", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --f-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* Glass. One set of tokens so every panel on the site refracts identically -
     a second, slightly-different glass is the fastest way to make the whole
     thing look accidental. */
  --glass-bg: rgba(255, 255, 255, 0.04);
  --glass-blur: blur(16px) saturate(1.4);
  --glass-edge: rgba(255, 255, 255, 0.10);
  --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
}

/* ── self-hosted faces ──────────────────────────────────────────────────
   Space Grotesk and JetBrains Mono ship as variable fonts, so one file
   covers the whole weight axis - confirmed by md5, since Google returns a
   byte-identical file for every weight you ask for. Satoshi is static and
   genuinely needs its three. */
@font-face {
  font-family: "Space Grotesk";
  src: url("/assets/fonts/space-grotesk-var.woff2") format("woff2");
  font-weight: 300 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/assets/fonts/jetbrains-mono-var.woff2") format("woff2");
  font-weight: 100 800; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("/assets/fonts/satoshi-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("/assets/fonts/satoshi-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("/assets/fonts/satoshi-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: var(--f-body);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--cyan); text-decoration-color: color-mix(in srgb, var(--cyan) 40%, transparent); }
a:hover { text-decoration-color: var(--cyan); }
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 3px; }

.skip {
  position: absolute; left: -9999px; top: 0; background: var(--cyan); color: var(--ink);
  padding: 10px 16px; z-index: 100; font-weight: 600;
}
.skip:focus { left: 0; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 var(--pad); }
.wrap-narrow { max-width: var(--narrow); margin: 0 auto; padding: 0 var(--pad); }

/* ── header ─────────────────────────────────────────────────────────────
   Thin, glassy, and it carries nothing but identity: mark, wordmark, and the
   phone number. Everything else moved out into the floating nav below, which
   is what Nathan asked for after seeing it done well elsewhere.

   Before this it was a 205px-tall solid black bar holding a 168px logo. That
   was a faithful reading of "make the logo 3x bigger" and it ate a fifth of a
   laptop screen on every scroll. The logo gets its size back in the hero
   instead, where it costs nothing. */
/* The sticky wrapper itself is invisible. The glass, the tint and the rule all
   belong to .head-row, so the header BAR ends under the logo and phone number
   - the nav row below it rides on the page rather than inside the bar. */
.site-head {
  position: sticky; top: 0; z-index: 60;
  display: block; padding: 0;
  background: none; border: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.head-row {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  /* Above the nav dock, so the dock slides UNDER this bar when it tucks away
     rather than across it. */
  position: relative; z-index: 2;
  padding: 11px var(--pad) 12px;
  background: color-mix(in srgb, var(--ink) 72%, transparent);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--line-soft);
}
.wordmark {
  display: flex; align-items: center; gap: 10px;
  color: var(--text); text-decoration: none;
  font-family: var(--f-display); font-weight: 600; letter-spacing: -.01em;
}
.wordmark img { width: 30px; height: 30px; border-radius: 7px; }
.wordmark span { font-size: 1rem; }
.wordmark em { font-style: normal; color: var(--text-3); font-weight: 500; }

/* The one action that survives at every width. Local trade buyers ring. */
.head-call {
  font-family: var(--f-mono); font-size: 13.5px; letter-spacing: .01em;
  color: var(--text); text-decoration: none;
  padding: 8px 14px; border-radius: 100px;
  border: 1px solid var(--glass-edge);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  white-space: nowrap;
}
.head-call:hover { border-color: var(--accent); color: var(--accent); }

/* ── nav row ────────────────────────────────────────────────────────────
   Second row of the sticky header: status left (under the logo), nav centre,
   email right (under the phone number). Nothing here hides or floats, so
   there is no focus-trapping edge case to guard against - which is the whole
   reason the previous auto-hiding dock needed :focus-within and a scroll
   listener, both now deleted rather than left to rot. */
/* Sits below the bar, not in it. Each chip carries its own glass, so the page
   scrolling underneath reads as depth rather than as a broken background. */
.nav-dock {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px var(--pad) 0;
  /* Behind the bar, so when it tucks away it slides UNDER the logo row rather
     than across it. That is what makes it read as receding rather than as a
     row of pills wandering off the top of the screen. */
  position: relative; z-index: 1;
  transition: transform .34s cubic-bezier(.4,0,.2,1), opacity .26s ease;
}
/* Out of the way while you are heading down the page, back the moment you
   stop. Never while scrolling UP - that is when somebody is looking for the
   menu - and never while focus is inside it, or a keyboard user would be
   tabbing through something they cannot see. */
.nav-dock.is-tucked {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .nav-dock { transition: none; }
  .nav-dock.is-tucked { transform: none; opacity: 1; pointer-events: auto; }
}

.nav-pill {
  display: flex; align-items: center; gap: 2px;
  padding: 6px;
  border-radius: 100px;
  border: 1px solid var(--glass-edge);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
}
.nav-pill a {
  color: var(--text-2); text-decoration: none; font-size: 14.5px;
  padding: 9px 15px; border-radius: 100px; white-space: nowrap;
  transition: color .18s ease, background .18s ease;
}
.nav-pill a:hover { color: var(--text); background: rgba(255,255,255,.06); }
.nav-pill a[aria-current] { color: var(--accent); }
.nav-pill a.cta {
  background: var(--accent); color: #1a0f07; font-weight: 700;
}
.nav-pill a.cta:hover { background: var(--accent-2); }

/* Status and email chips, as separate pills beside the nav - Nathan's ask.
   The dot is a real state, not decoration: fx.js sets .is-busy from the
   working-hours check so it cannot claim "available now" at 3am. */
.nav-chip {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 15px; border-radius: 100px;
  border: 1px solid var(--glass-edge);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
  font-family: var(--f-mono); font-size: 12.5px; color: var(--text-2);
  text-decoration: none; border-width: 1px; cursor: pointer;
  /* Two words that must stay on one line: letting "Available now" wrap pushed
     the sticky header from ~118px to 138px on a phone, for nothing. */
  white-space: nowrap;
}
.nav-chip:hover { color: var(--text); border-color: var(--accent); }
.nav-chip .dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: #57d977; box-shadow: 0 0 0 3px rgba(87,217,119,.18);
}
.nav-chip.is-busy .dot { background: var(--accent); box-shadow: 0 0 0 3px rgba(224,145,90,.18); }

/* Below ~860px the three pills cannot share a row. The nav itself is the
   thing worth keeping - the phone number is already pinned in the header,
   and the email chip is repeated in the footer. */
@media (max-width: 860px) {
  /* The email chip is the one that goes, rather than letting the sticky header
     wrap onto a third row: the phone number sits directly above it, and the
     address still appears in the contact section and twice in the footer.
     "Available now" stays - it is a real, live state and it is two words. */
  .nav-chip.is-email { display: none; }
  .nav-dock { gap: 8px; }
  .nav-pill a { padding: 9px 12px; font-size: 14px; }
  /* Let the pill scroll rather than shrink the tap targets any further. */
  .nav-pill { overflow-x: auto; scrollbar-width: none; }
  .nav-pill::-webkit-scrollbar { display: none; }
}

/* On a phone the status chip was taking 160 of 375 pixels and squeezing the
   menu into 168 for 286 pixels of links - two of them collapsed to nothing and
   "Get in touch", the one thing the page most wants you to press, sat off the
   right-hand edge behind a horizontal scroll nobody would guess was there. The
   chip is the thing that goes: it is a nicety, the menu is the point, and the
   header loses a chunk of height on a small screen into the bargain. */
@media (max-width: 560px) {
  .nav-chip { display: none; }
  .nav-pill { width: 100%; justify-content: space-between; }
  .nav-pill a { padding: 9px 10px; }
}
@media (max-width: 480px) {
  .nav-pill a[href="/blog"], .nav-pill a[href="#areas"], .nav-pill a[href="/#areas"] { display: none; }
}

/* ── hero ───────────────────────────────────────────────────────────── */
.hero { padding: clamp(48px, 9vw, 104px) 0 clamp(40px, 7vw, 88px); }
.hero-grid {
  display: grid; gap: clamp(32px, 6vw, 64px); align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) { .hero-grid { grid-template-columns: 1.25fr .75fr; } }

.hero h1 {
  font-family: var(--f-display);
  font-size: clamp(2.6rem, 6vw, 4.4rem);
  line-height: 1.02;
  letter-spacing: -.02em;
  margin: 0 0 22px;
  text-wrap: balance;
  font-weight: 400;
}
.hero h1 span { color: var(--cyan); }
.hero .lede { font-size: clamp(1.05rem, 2vw, 1.22rem); color: var(--text-2); max-width: 52ch; margin: 0 0 30px; }
.eyebrow {
  font-family: var(--f-mono); font-size: 12px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-3); margin: 0 0 20px; display: flex; align-items: center; gap: 10px;
}
.eyebrow::before { content: ""; width: 26px; height: 1px; background: var(--cyan); }

/* The portrait keeps its own cyan ground - it is already his identity
   elsewhere, and a flat colour block against the near-black is a stronger
   graphic move than cutting him out and floating him. */
/* width:100% matters as much as the max-width. Without it the 380px floor
   exceeded the 351px of content space at a 390px viewport and widened the
   whole page, so every section overflowed horizontally on a phone. */
.portrait {
  border-radius: 18px; overflow: hidden; border: 1px solid var(--line);
  width: 100%; max-width: 380px;
}
.portrait img { width: 100%; }

.btn {
  display: inline-block; background: var(--cyan); color: var(--ink);
  padding: 13px 24px; border-radius: 10px; font-weight: 650; text-decoration: none;
  transition: transform .15s ease, background .15s ease;
}
.btn:hover { background: #a5f1ff; transform: translateY(-1px); }
.btn.ghost { background: none; color: var(--text); border: 1px solid var(--line); }
.btn.ghost:hover { border-color: var(--cyan); color: var(--cyan); background: none; }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; }

/* =====================================================================
   ANALYTICS CONSENT BANNER

   Built by assets/consent.js, which appends it to the end of <body>. It is
   deliberately NOT a modal: it does not trap focus, does not dim the page and
   does not block reading. The site is fully usable while it sits there, which
   is what stops the answer being coerced.
   ===================================================================== */
.consent {
  position: fixed;
  z-index: 60;
  left: 16px;
  right: 16px;
  bottom: 16px;
  margin-inline: auto;
  max-width: 620px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface-2);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
}

.consent-text {
  margin: 0;
  font-family: var(--f-body);
  font-size: .95rem;
  line-height: 1.5;
  color: var(--text);
}

.consent-text a { color: var(--accent); }

.consent-row { display: flex; flex-wrap: wrap; gap: 10px; }

/* Both answers are the same visual weight class - one filled, one ghost -
   rather than the decline being a grey text link. A decline styled as an
   afterthought is a dark pattern, and it is the thing regulators actually
   name when they look at a banner. */
.consent-row .btn { padding: 11px 18px; font-size: .92rem; }

@media (prefers-reduced-motion: reduce) {
  .consent-row .btn { transition: none; }
}

/* The on/off control on the privacy page. */
.consent-switch {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin: 14px 0 0;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface-2);
}

.consent-status { margin: 0; font-weight: 600; color: var(--text); }
.consent-note { margin: 0; font-size: .9rem; color: var(--text-3); }

/* ── sections ───────────────────────────────────────────────────────── */
section { padding: clamp(48px, 8vw, 92px) 0; }
section + section { border-top: 1px solid var(--line-soft); }
.sec-head { max-width: 60ch; margin-bottom: clamp(28px, 4vw, 44px); }
.sec-head h2 {
  font-family: var(--f-display); font-weight: 600;
  font-size: clamp(1.9rem, 4vw, 2.7rem); line-height: 1.1; letter-spacing: -.015em;
  margin: 0 0 14px; text-wrap: balance;
}
.sec-head p { color: var(--text-2); margin: 0; }

.grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); }
/* Four services, and plain auto-fit puts three on the first row and strands
   the fourth alone across the full width. An explicit 2x2 from tablet up
   keeps the block square at every size that matters. */
.grid.two-up { grid-template-columns: 1fr; }
@media (min-width: 720px) { .grid.two-up { grid-template-columns: 1fr 1fr; } }
/* Three services, 2026-09-07: Digital now leads, repair stays distinct, and
   everything mobile-dependent (TV/AV, computer & office setup, old kit) folds
   into one "Other" card - so the homepage grid genuinely has three items, not
   four squeezed into a 2x2. Explicit 3-up from desktop, never wraps to a lone
   card on tablet the way plain auto-fit would with an odd count. */
.grid.three-up { grid-template-columns: 1fr; }
@media (min-width: 720px) { .grid.three-up { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1040px) { .grid.three-up { grid-template-columns: 1fr 1fr 1fr; } }

.svc {
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: 14px; padding: 26px 24px;
}
.svc h3 { margin: 0 0 10px; font-size: 1.12rem; letter-spacing: -.01em; }
.svc p { color: var(--text-2); margin: 0 0 14px; font-size: .97rem; }
.svc ul { margin: 0; padding-left: 18px; color: var(--text-3); font-size: .92rem; }
.svc li { margin-bottom: 5px; }
.svc .num {
  font-family: var(--f-mono); font-size: 11px; color: var(--cyan);
  letter-spacing: .12em; display: block; margin-bottom: 14px;
}

/* ── writing cards ──────────────────────────────────────────────────── */
.cards { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.card {
  display: block; background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: 14px; padding: 24px; text-decoration: none; color: inherit;
  transition: border-color .18s ease, transform .18s ease;
}
.card:hover { border-color: var(--cyan); transform: translateY(-2px); }
.card h2 { font-size: 1.14rem; margin: 0 0 10px; line-height: 1.3; letter-spacing: -.01em; }
.card p { color: var(--text-2); font-size: .95rem; margin: 0 0 16px; }
.card-meta, .card-tags { font-family: var(--f-mono); font-size: 11.5px; color: var(--text-3); }
.card-tags { margin-bottom: 12px; display: flex; gap: 6px; flex-wrap: wrap; }
.tag {
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .04em;
  background: var(--cyan-deep); color: var(--cyan);
  padding: 3px 9px; border-radius: 100px; white-space: nowrap;
}
.tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 18px; }

/* ── blog search & filter, 2026-09-07 ──────────────────────────────────
   Client-side only, on purpose - this is a static site with no backend, and
   the post count is small enough that filtering a JSON index in the browser
   is instant and needs nothing running server-side. blog-search.js does the
   work; these rules are just the chrome, and every one of them degrades
   safely with JS off since .post-card[hidden] simply never applies. */
.blog-controls {
  display: flex; flex-wrap: wrap; gap: 14px; align-items: center;
  margin-bottom: clamp(24px, 4vw, 36px);
}
.blog-search {
  flex: 1 1 240px; font: inherit; font-size: .96rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: 10px;
  padding: 11px 14px;
}
.blog-search:focus-visible { outline: 2px solid var(--cyan); outline-offset: 1px; }
.blog-filters { display: flex; flex-wrap: wrap; gap: 8px; }
.blog-filter {
  font-family: var(--f-mono); font-size: 12px; letter-spacing: .03em;
  background: none; color: var(--text-2); border: 1px solid var(--line);
  border-radius: 100px; padding: 7px 13px; cursor: pointer;
}
.blog-filter:hover { border-color: var(--cyan); color: var(--text); }
.blog-filter[aria-pressed="true"] { background: var(--cyan-deep); color: var(--cyan); border-color: var(--cyan); }
.blog-empty { color: var(--text-3); padding: 8px 0 24px; display: none; }
.blog-empty.is-visible { display: block; }
#blog-clear {
  background: none; border: none; padding: 0; font: inherit;
  color: var(--cyan); text-decoration: underline; cursor: pointer;
}

/* The latest post, given real weight instead of sitting the same size as
   everything else - "make it look more bloggy" was partly about this: a
   blog reads as alive when the newest thing is the first thing you see. */
.card.featured {
  grid-column: 1 / -1; display: grid; gap: 20px; padding: clamp(24px, 4vw, 34px);
}
@media (min-width: 720px) { .card.featured { grid-template-columns: 1.3fr .7fr; align-items: center; } }
.card.featured h2 { font-family: var(--f-display); font-weight: 600; font-size: clamp(1.5rem, 3vw, 2rem); }
.card.featured p { font-size: 1rem; }
.card.featured .card-meta { order: 3; }

/* ── open source ────────────────────────────────────────────────────── */
/* Two columns rather than flex: a repo name is an identifier and breaking
   `mcp-server-core` across three lines makes it unreadable. The name column is
   fixed so every row lines up, and it collapses to stacked rows on a phone
   rather than squeezing the name to nothing. */
/* The list used to be capped at 640px, which forced every description onto
   two or three lines on a full-width screen for no reason. It gets the whole
   column now. */
.repo-list { margin: 0; }
/* The name column is in rem rather than px deliberately. The longest name
   (pinterest-mcp-server) measures 183px at the default root size, and a px
   column would be outgrown the moment somebody raises their browser's base
   font - which is exactly how that name ended up touching its description.
   In rem the column grows with the text it holds. */
.repo {
  display: grid; grid-template-columns: 14.5rem 1fr; gap: 4px 32px;
  align-items: baseline; padding: 15px 0;
  border-bottom: 1px solid var(--line-soft); text-decoration: none; color: inherit;
}
@media (max-width: 760px) { .repo { grid-template-columns: 1fr; gap: 6px 0; } }
.repo:last-child { border-bottom: none; }
.repo code {
  font-family: var(--f-mono); color: var(--cyan); font-size: .95rem;
  white-space: nowrap; padding-right: 8px;
}
.repo span { color: var(--text-2); font-size: .93rem; }

/* ── post ───────────────────────────────────────────────────────────── */
.page-head { padding-bottom: 0; }
.page-head h1, .post h1 {
  font-family: var(--f-display); font-weight: 600;
  font-size: clamp(2.1rem, 5vw, 3.2rem); line-height: 1.08; letter-spacing: -.02em;
  margin: 0 0 16px; text-wrap: balance;
}
.lede { color: var(--text-2); font-size: 1.1rem; margin: 0 0 24px; }
.post { padding: clamp(36px, 6vw, 72px) 0; }
.back { font-family: var(--f-mono); font-size: 12.5px; text-decoration: none; display: inline-block; margin-bottom: 22px; }
.byline { display: flex; align-items: center; gap: 12px; font-size: .9rem; color: var(--text-3); margin: 0 0 8px; }
.byline img { border-radius: 50%; }
.byline strong { color: var(--text); font-weight: 600; }

.prose { padding-top: 8px; }
.prose > * + * { margin-top: 1.15em; }
.prose h2 {
  font-family: var(--f-display); font-weight: 600; font-size: 1.85rem;
  line-height: 1.2; letter-spacing: -.015em; margin-top: 2em; scroll-margin-top: 80px;
}
.prose h3 { font-size: 1.18rem; margin-top: 1.7em; letter-spacing: -.005em; }
.prose p, .prose li { color: #dfe5ee; }
.prose ul, .prose ol { padding-left: 22px; }
.prose li { margin-bottom: .45em; }
.prose li::marker { color: var(--cyan); }
.prose code {
  font-family: var(--f-mono); font-size: .88em;
  background: var(--surface-2); border: 1px solid var(--line-soft);
  padding: .12em .4em; border-radius: 5px; color: var(--amber);
  overflow-wrap: anywhere;
}
.prose pre {
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--cyan);
  border-radius: 10px; padding: 18px 20px; overflow-x: auto; font-size: .88rem; line-height: 1.6;
}
.prose pre code { background: none; border: none; padding: 0; color: #cfe8f2; font-size: 1em; }
.prose blockquote {
  border-left: 3px solid var(--amber); padding: 4px 0 4px 20px; margin-left: 0;
  color: var(--text-2); font-style: italic;
}
.prose figure { margin: 2em 0; }
.prose img { border-radius: 12px; border: 1px solid var(--line-soft); }
.prose a { overflow-wrap: anywhere; }

.post-end {
  margin-top: 56px; padding-top: 32px; border-top: 1px solid var(--line);
}
.post-end h2 { font-family: var(--f-display); font-weight: 600; font-size: 1.6rem; margin: 0 0 10px; }
.post-end p { color: var(--text-2); margin: 0 0 20px; }

/* ── contact ────────────────────────────────────────────────────────── */
.contact-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 18px; padding: clamp(28px, 5vw, 48px);
  display: grid; gap: 28px; align-items: center;
}
@media (min-width: 800px) { .contact-card { grid-template-columns: 1.4fr .6fr; } }
.contact-card h2 { font-family: var(--f-display); font-weight: 600; font-size: clamp(1.8rem, 3.5vw, 2.4rem); margin: 0 0 12px; }
.contact-card p { color: var(--text-2); margin: 0 0 22px; }
.contact-card .portrait { max-width: 220px; margin-left: auto; }

/* ══════════════════════════════════════════════════════════════════════
   LOCAL SERVICES — 2026-09-07

   The site stopped selling AI security auditing to a national technical
   audience and started selling PC repair, TV and AV installation and
   local digital work, to people who live within about half an hour of
   New Holland.

   Everything below exists because that audience needs things the old
   site never had to provide: a phone number they can press, prices they
   can read before ringing, and the names of their own towns. The
   research behind the numbers found exactly three traders across
   Grimsby, Scunthorpe, Hull, Barton and Brigg who publish any price at
   all - so a plain rate card is the differentiator here, not decoration,
   and it gets styled like it matters.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── phone, which is the actual conversion event ────────────────────── */
/* Local trade buyers ring. The number is a real tel: link everywhere it
   appears, sized so it never has to be hunted for, and it is the one
   nav item that survives every breakpoint. */
/* Masked rather than an inline <svg> or a background-image, so the glyph
   inherits currentColor and stays correct on both the cyan button and the
   ghost one without a second copy. */
.btn.call {
  --phone-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6.6 10.8a15.1 15.1 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25 11.4 11.4 0 0 0 3.6.58 1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1 11.4 11.4 0 0 0 .58 3.6 1 1 0 0 1-.25 1z'/%3E%3C/svg%3E");
  display: inline-flex; align-items: center; gap: 9px;
  font-variant-numeric: tabular-nums;
}
.btn.call::before {
  content: "";
  width: 16px; height: 16px; flex: none;
  background: currentColor;
  -webkit-mask: var(--phone-glyph) center / contain no-repeat;
  mask: var(--phone-glyph) center / contain no-repeat;
}
/* The phone number is the last thing to go on a narrow screen, so it is
   deliberately absent from the hide rules further up this file. */

/* Ko-fi, the same ghost-button treatment everywhere else uses, coloured to
   Ko-fi's own brand red rather than the site's cyan or amber - it is an
   outbound link to somebody else's platform, not a call to action for NAS
   Digital itself, and it should read that way at a glance. */
.btn.kofi { border-color: #ff5e5b; color: #ff5e5b; }
.btn.kofi:hover { background: #ff5e5b; color: #1a0e0d; }

.call-line {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-family: var(--f-mono); font-size: 13px; color: var(--text-3);
  margin: 18px 0 0;
}
.call-line strong { color: var(--text-2); font-weight: 500; }

/* ── rate card ──────────────────────────────────────────────────────── */
.rate-callout {
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--cyan);
  border-radius: 14px; padding: 24px 26px; margin-bottom: 26px;
}
.rate-callout p { margin: 0 0 8px; color: var(--text-2); }
.rate-callout p:last-child { margin-bottom: 0; }
.rate-callout b { color: var(--text); font-weight: 650; }

.rates { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.rate-group {
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: 14px; padding: 24px;
}
.rate-group h3 { margin: 0 0 16px; font-size: 1.08rem; letter-spacing: -.01em; }
.rate-note { color: var(--text-3); font-size: .86rem; margin: 16px 0 0; }

/* ── service area ───────────────────────────────────────────────────── */
.areas { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.area-group h3 {
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--cyan); margin: 0 0 12px;
}
.area-group ul { list-style: none; margin: 0; padding: 0; columns: 2; column-gap: 18px; }
.area-group li { color: var(--text-2); font-size: .94rem; padding: 3px 0; break-inside: avoid; }

/* ── enquiry form ───────────────────────────────────────────────────── */
/* Scoped under .form throughout. There is no other input on the site, but
   an unscoped `input {}` here is exactly the kind of rule that quietly
   breaks something added later. */
.form { display: grid; gap: 15px; }
.form-row { display: grid; gap: 15px; }
@media (min-width: 620px) { .form-row { grid-template-columns: 1fr 1fr; } }
.form label { display: block; font-size: .88rem; color: var(--text-2); margin-bottom: 6px; }
.form input, .form select, .form textarea {
  width: 100%; font: inherit; font-size: .96rem;
  background: var(--ink); color: var(--text);
  border: 1px solid var(--line); border-radius: 10px;
  padding: 11px 13px;
}
.form textarea { min-height: 128px; resize: vertical; }
.form input:focus-visible, .form select:focus-visible, .form textarea:focus-visible {
  outline: 2px solid var(--cyan); outline-offset: 1px; border-color: transparent;
}
.form input::placeholder, .form textarea::placeholder { color: var(--text-3); }
.form button { border: none; cursor: pointer; font: inherit; justify-self: start; }
/* Honeypot. Off-screen rather than display:none, because some bots skip
   hidden fields and fill everything else. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-msg {
  border-radius: 10px; padding: 14px 16px; font-size: .93rem; margin: 0;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--text-2);
}
.form-msg.ok { border-color: color-mix(in srgb, var(--cyan) 45%, var(--line)); color: var(--text); }
/* A failed send must never look like a sent message. Amber, and it always
   carries the phone number so the enquiry is not simply lost. */
.form-msg.bad { border-color: var(--amber); color: var(--text); }
.form-msg a { color: var(--amber); }
.form-msg.ok a { color: var(--cyan); }

/* ── the other brand ────────────────────────────────────────────────── */
/* With Nate is the creative half of the same one-person business. It gets
   the amber rather than the cyan, so it reads as related-but-separate on
   sight - and amber is genuinely the neighbouring hue to With Nate's own
   ember #e8612a, so the two sites rhyme rather than clash. */
.brandcard {
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  border-radius: 18px; padding: clamp(26px, 4vw, 40px);
  display: grid; gap: 26px; align-items: center;
}
@media (min-width: 760px) { .brandcard { grid-template-columns: 1fr auto; } }
.brandcard h2 {
  font-family: var(--f-display); font-weight: 600;
  font-size: clamp(1.6rem, 3vw, 2.1rem); margin: 0 0 12px;
}
.brandcard p { color: var(--text-2); margin: 0 0 20px; }
.brandcard .btn.ghost:hover { border-color: var(--amber); color: var(--amber); }
.brandmark {
  background: var(--paper-warm, #fbf4e9); border-radius: 14px;
  padding: 20px 24px; width: 100%; max-width: 240px; margin-left: auto;
}
.brandmark img { width: 100%; }

/* ── footer ─────────────────────────────────────────────────────────── */
.site-foot {
  /* The positioning context for the footer wordmark's dust: its canvas is
     promoted to cover this whole element, so the particles can travel to the
     cursor wherever in the footer it happens to be. */
  position: relative;
  border-top: 1px solid var(--line); margin-top: 40px;
  padding: 44px var(--pad) 56px;
  max-width: var(--wrap); margin-left: auto; margin-right: auto;
  display: grid; gap: 22px;
}
@media (min-width: 760px) { .site-foot { grid-template-columns: 1fr auto; align-items: start; } }
.site-foot p { color: var(--text-3); font-size: .9rem; margin: 6px 0 0; }
.foot-links { display: flex; gap: 18px; flex-wrap: wrap; font-size: .92rem; }
.foot-note { grid-column: 1 / -1; color: var(--text-3); font-size: .86rem; margin-top: 4px; }

/* ══════════════════════════════════════════════════════════════════════
   MOTION LAYER — lunar rebuild

   The old layer parallaxed three planes against scroll. Nathan wanted the
   background locked instead: it does not move when you scroll, it responds to
   the cursor. So the whole thing is fixed and driven by pointer position, and
   scroll no longer touches it at all.

   Everything here is decorative. Nothing the page says or does depends on any
   of it running - see the reduced-motion block at the end, which turns the
   layer off entirely and leaves a site that still reads correctly. That is
   also why the starfield is 2D canvas rather than WebGL: same result at a
   fraction of the weight, and no shader-compile stall on a mid-range phone.
   ═══════════════════════════════════════════════════════════════════════ */

.fx { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; }

/* The moon. One soft off-centre glow doing the work the two old cyan glows
   did, in the warm accent so it reads as light falling rather than a colour
   wash. Fixed, like everything else in this layer. */
.fx-moon {
  position: absolute; inset: 0;
  background:
    radial-gradient(46% 38% at 78% 8%, rgba(224, 145, 90, .16), transparent 68%),
    radial-gradient(40% 34% at 12% 88%, rgba(150, 170, 210, .07), transparent 70%);
  filter: blur(40px);
}
#fx-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Scroll reveals. Opacity + a small rise; anything bigger turns reading a
   page into waiting for a page.

   Note the `.js` gate. Content must NEVER need JavaScript to become visible:
   if fx.js fails, is blocked, or simply has not run yet, an unqualified
   `[data-reveal] { opacity: 0 }` leaves the entire hero blank with no error
   anywhere. That happened during an earlier build, and it is invisible in
   testing precisely because it produces no console output. The class is set by
   a tiny inline script in <head>, so hiding only ever happens once something is
   definitely running to unhide it again. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .7s cubic-bezier(.22, .8, .3, 1), transform .7s cubic-bezier(.22, .8, .3, 1);
}
.js [data-reveal].is-in { opacity: 1; transform: none; }
.js [data-reveal][data-delay="1"] { transition-delay: .08s; }
.js [data-reveal][data-delay="2"] { transition-delay: .16s; }
.js [data-reveal][data-delay="3"] { transition-delay: .24s; }

/* Cards lift and light their edge on hover - the one place the accent moves. */
.svc { transition: transform .28s cubic-bezier(.22,.8,.3,1), border-color .28s ease, background .28s ease; }
.svc:hover { transform: translateY(-4px); border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; animation: none !important; }
  /* Content must never be stuck invisible waiting for an observer that the JS
     deliberately never starts. */
  .js [data-reveal], [data-reveal] { opacity: 1; transform: none; }
  /* The starfield stays - fx.js paints it once, static, with no drift and no
     cursor reaction. A dark page with nothing on it is not the accessible
     option, it is just an empty one. The moon glow is pure CSS and never
     moved anyway. */
  .nav-dock { transition: none; }
}

/* Some people want the layout without the frosting - blur is also genuinely
   expensive on low-end GPUs. Solid surfaces, everything else intact. */
@media (prefers-reduced-transparency: reduce) {
  .head-row, .nav-pill, .nav-chip, .head-call, .glass {
    -webkit-backdrop-filter: none; backdrop-filter: none;
    background: var(--surface);
  }
}

/* Safari before 18 and Firefox before 103 have no backdrop-filter. Without
   this the "glass" is a nearly-transparent panel with text on top of moving
   stars, which is unreadable rather than merely less pretty. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .head-row, .nav-pill, .nav-chip, .head-call, .glass { background: var(--surface); }
}


/* ══════════════════════════════════════════════════════════════════════
   LUNAR COMPONENTS — 2026-09-07
   ═══════════════════════════════════════════════════════════════════════ */

/* ── glass ──────────────────────────────────────────────────────────────
   One recipe, used everywhere. The specular lip is the part that sells it:
   a real pane of glass catches light on its top edge and shades on its
   bottom, and without that an "acrylic" panel just looks like a grey box
   somebody set to 40% opacity. */
.glass {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-edge);
  border-top-color: rgba(255, 255, 255, .16);
  border-bottom-color: rgba(0, 0, 0, .35);
  box-shadow: var(--glass-shadow), inset 0 1px 0 rgba(255, 255, 255, .06);
}

/* The existing panels become glass rather than flat surfaces. Kept as their
   own rule instead of adding .glass to every element in the markup, so the
   generated pages inherit it without build-script changes. */
.svc, .rate-group, .contact-card, .card, .pkg, .qa details {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-edge);
  border-top-color: rgba(255, 255, 255, .14);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05);
}

/* ── hero ───────────────────────────────────────────────────────────────
   The mark gets its size back here, where a big logo costs nothing, rather
   than in a sticky header where it cost a fifth of the screen on every
   scroll. */
/* The mark is a square box that reserves layout and holds the fallback image.
   The dots themselves are NOT in here - they live in a fixed, viewport-sized
   layer (see mark.js) so they can leave the hero entirely and fall to the
   bottom of the screen without any re-parenting mid-flight. */
.hero-mark {
  width: clamp(84px, 11vw, 132px); aspect-ratio: 1; margin: 0 0 26px;
  display: block; padding: 0; border: 0; background: none;
  cursor: pointer; border-radius: 50%;
}
.hero-mark img { width: 100%; height: 100%; display: block; }
/* Once the real dots exist the image steps aside, but keeps its box so the
   hero never reflows at the moment JS takes over. */
.hero-mark.is-live img { visibility: hidden; }
.hero-mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; }

.mark-layer {
  /* Page coordinates, not viewport: the dots fall down the whole document and
     stay on whatever they landed on when you scroll. Zero height so the layer
     never adds to the page's own scroll length. */
  position: absolute; top: 0; left: 0; width: 100%; height: 0;
  pointer-events: none; overflow: visible;
  /* Above the zero-G bench (40), below the nav dock (55) and header (60), so
     a pile of dots on the floor gathers BEHIND the nav rather than over it. */
  z-index: 45;
}
.mark-dot {
  position: absolute; top: 0; left: 0;
  border-radius: 50%; background: var(--mark);
}
/* Promotion only while something is actually moving - fifty permanently
   promoted layers is a real cost for an idle page. */
.mark-layer.is-active .mark-dot { will-change: transform; }
/* Loose dots can be picked up and thrown. Only while loose - an assembled mark
   must keep letting the hover through to the logo underneath. */
.mark-layer.is-active .mark-dot { pointer-events: auto; cursor: grab; touch-action: none; }
.mark-layer.is-active .mark-dot.is-held { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) { .hero-mark { cursor: default; } }

/* The particle wordmark. The canvas is decorative and aria-hidden; the real
   <h1> sits underneath it as actual text, visible whenever JS has not taken
   over, so search engines and screen readers always get the words. */
.wordmark-stage { position: relative; margin: 0 0 22px; max-width: 620px; }
.wordmark-stage canvas { display: block; width: 100%; height: auto; }
/* The real heading, shown until the canvas has genuinely sampled something,
   and sized to roughly match it so the swap is not a jolt. */
.wordmark-text {
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(2.3rem, 7.4vw, 3.9rem); line-height: 1;
  letter-spacing: -.03em; margin: 0;
}
/* The hero wordmark is clickable, so it says so with the cursor and nothing
   else - no button, no label. While playing it owns the pointer, or a drag to
   steer selects the heading text underneath it. */
.wordmark-stage.is-live { cursor: pointer; }
.wordmark-stage.is-playing { touch-action: none; user-select: none; cursor: crosshair; }

/* The footer wordmark: the same particle scatter at a smaller size, where it
   reads as a sign-off rather than a greeting. */
.foot-mark { max-width: 260px; margin: 0 0 14px; }
/* Promoted: the canvas leaves the flow and spans the footer, while the stage
   keeps the height it had so nothing below it moves. It never takes a click -
   the whole point of orbiting the cursor rather than sitting under it is that
   the footer stays usable, and pointer-events makes that literally true rather
   than merely likely. */
/* The stage is position:relative by default, which would make IT the canvas's
   containing block and pin the dust inside a 260px box. Static hands that job
   to the footer, which is the whole point. */
.foot-mark.is-dust { position: static; }
.foot-mark.is-dust canvas {
  position: absolute; left: 0; top: 0;
  pointer-events: none; z-index: 3;
}
.foot-mark .wordmark-text { font-size: 1.4rem; }

.wordmark-stage.is-live .wordmark-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* The nav dock is fixed at the bottom, so without this the last thing on the
   page sits permanently underneath it. Enough room for the pill plus its own
   offset, and it only applies where the dock is actually on screen. */


/* ── telemetry strip ────────────────────────────────────────────────────
   Nathan liked that the reference site published its own local time and that
   the number was real. This is the same instinct, computed rather than
   fetched: local time plus the actual moon phase, worked out from the
   synodic month. No API, no third party, so /privacy/ stays true. */
.telemetry {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px;
  font-family: var(--f-mono); font-size: 12px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
  padding: 12px 0; margin: 0;
}
.telemetry b { color: var(--text-2); font-weight: 500; }
.telemetry .sep { color: var(--line); }
.telemetry .moon-glyph { font-size: 15px; line-height: 1; letter-spacing: 0; text-transform: none; }

/* ── packages ───────────────────────────────────────────────────────────
   "From" prices, deliberately. Nathan has not costed this work properly yet,
   and thirty published figures nobody has stood behind is a liability rather
   than a differentiator. */
.pkgs { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
.pkg { border-radius: 16px; padding: 28px 26px; display: flex; flex-direction: column; }
.pkg.is-featured { border-color: color-mix(in srgb, var(--accent) 55%, var(--glass-edge)); }
.pkg h3 { font-family: var(--f-display); font-weight: 600; font-size: 1.15rem; margin: 0 0 6px; letter-spacing: -.01em; }
.pkg .pkg-for { color: var(--text-3); font-size: .9rem; margin: 0 0 20px; }
.pkg .price {
  font-family: var(--f-display); font-weight: 700;
  font-size: 2.1rem; letter-spacing: -.03em; color: var(--text);
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px;
}
.pkg .price small { font-family: var(--f-mono); font-size: 11px; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; color: var(--text-3); }
.pkg ul { list-style: none; margin: 20px 0 24px; padding: 0; color: var(--text-2); font-size: .95rem; }
.pkg li { padding: 7px 0 7px 22px; position: relative; border-top: 1px solid var(--line-soft); }
.pkg li:first-child { border-top: none; }
.pkg li::before { content: "+"; position: absolute; left: 0; color: var(--accent); font-family: var(--f-mono); }
.pkg .btn { margin-top: auto; }
.pkg-note { color: var(--text-3); font-size: .86rem; margin: 22px 0 0; }

/* ── Q&A ────────────────────────────────────────────────────────────────
   Native <details>, so it opens with no JS and is keyboard-operable for
   free - same reasoning as the blog filter chips. */
.qa { display: grid; gap: 10px; }
.qa details { border-radius: 14px; overflow: hidden; }
.qa summary {
  cursor: pointer; list-style: none; padding: 20px 24px;
  font-family: var(--f-display); font-weight: 500; font-size: 1.05rem;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary::after {
  content: "+"; font-family: var(--f-mono); color: var(--accent);
  font-size: 1.2rem; flex: none; transition: transform .2s ease;
}
.qa details[open] summary::after { transform: rotate(45deg); }
.qa summary:hover { color: var(--accent); }
.qa .qa-body { padding: 0 24px 22px; color: var(--text-2); max-width: 68ch; }
.qa .qa-body p { margin: 0 0 12px; }
.qa .qa-body p:last-child { margin-bottom: 0; }

/* ── small print ────────────────────────────────────────────────────────
   The network line. Deliberately quiet: it settles the question of whether
   Nathan is treading on another local trader's toes without naming anyone,
   and tells a bigger customer that one person is not a ceiling. */
.smallprint {
  font-size: .82rem; font-style: italic; color: var(--text-3);
  max-width: 74ch; margin: 26px 0 0; line-height: 1.6;
}

/* ── zero-G ─────────────────────────────────────────────────────────────
   The easter egg. Bench contents in freefall - his trade, in his theme, and
   nothing like the floating animals that prompted it. */
.zero-g-btn {
  font-family: var(--f-mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer;
  background: none; color: var(--text-3);
  border: 1px dashed var(--line); border-radius: 100px;
  padding: 9px 16px;
}
.zero-g-btn:hover { color: var(--accent); border-color: var(--accent); border-style: solid; }
#zero-g-stage {
  position: fixed; inset: 0; z-index: 40; pointer-events: none; overflow: hidden;
}
#zero-g-stage .drift { position: absolute; will-change: transform; font-size: 26px; line-height: 1; }

/* ── about ──────────────────────────────────────────────────────────────
   At the bottom, per Nathan: the least urgent thing on the page for somebody
   who needs a phone number, and it reads better as what you find once you
   already like the look of him. */
.about-card { border-radius: 18px; padding: clamp(28px, 5vw, 44px); }
.about-card h2 { font-family: var(--f-display); font-weight: 600; font-size: clamp(1.7rem, 3.4vw, 2.3rem); margin: 0 0 16px; letter-spacing: -.02em; }
.about-card p { color: var(--text-2); max-width: 64ch; }
.about-card .initials { color: var(--text); font-weight: 500; }


/* Attribution for the arcade soundtrack, under the game it belongs to and
   shown only while that game is running - which is exactly when the track is
   playing and the credit is owed. Quiet, but a real link with a real name on
   it: a credit nobody can read is not a credit. */
.music-credit {
  display: none;
  margin: 2px 0 20px;
  font-family: var(--f-mono); font-size: .7rem; letter-spacing: .03em;
  color: var(--text-3);
}
.wordmark-stage.is-playing ~ .music-credit { display: block; }
.music-credit a { color: var(--text-2); text-decoration: none; border-bottom: 1px solid var(--line); }
.music-credit a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* The UFO and whatever it is carrying. Fixed, above the page, and completely
   untouchable - it passes over live links and must never intercept one. */
.ufo-layer { position: fixed; inset: 0; pointer-events: none; z-index: 58; overflow: hidden; }
.ufo, .ufo-cargo {
  position: absolute; left: 0; top: 0;
  will-change: transform;
}
.ufo { line-height: 1; opacity: 0; }
.ufo-craft { display: block; }
/* The beam only burns while it is actually lifting something. On a passing
   crossing there is nothing to beam at, and a craft lighting its way over the
   copy for no reason just looks like a mistake. */
.ufo-beam {
  position: absolute; left: 50%; top: 17px;
  width: 58px; height: 78px; transform: translateX(-50%);
  background: linear-gradient(to bottom, rgba(224,145,90,.5), rgba(224,145,90,0));
  clip-path: polygon(36% 0, 64% 0, 100% 100%, 0 100%);
  display: none;
}
.ufo.is-beaming .ufo-beam { display: block; }
.ufo.is-beaming .ufo-craft { filter: drop-shadow(0 3px 9px rgba(224,145,90,.55)); }
/* The stand-in for the button it steals: dressed like the real one, because
   the joke only works if you recognise what has just been taken. */
.ufo-cargo {
  font-family: var(--f-mono); font-size: 11.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-2);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 7px 13px; background: var(--surface); white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) { .ufo-layer { display: none; } }

/* The drag itself. Cursor and touch-action are set here rather than only in
   JS, so the grab affordance is correct even before ufo.js has finished
   parsing on a slow connection. */
.wordmark img { cursor: grab; touch-action: none; }
.wordmark:active img { cursor: grabbing; }

/* The floating clone dragged from the header. Fixed, not tied to any layer's
   z-index, because it has to sit ABOVE the sticky header (60) while it is
   being dragged across it - the whole point is that it visibly leaves the
   header bar. Same border-radius as the real icon so nothing looks different
   about it while it travels. */
.wordmark-drag-clone {
  position: fixed; left: 0; top: 0; z-index: 65;
  border-radius: 7px; pointer-events: none; will-change: transform;
}

/* Only this one scripted run needs to fly above the header rather than
   behind it - every ambient crossing stays at its usual 58, which is what
   keeps the header and nav reliably on top the rest of the time. */
.ufo-layer.is-header-run { z-index: 61; }

/* A wider, shallower cone, for this run only.
   The default beam is a narrow cone sized to lift a wide button from the
   middle of the page: it needs about 43px of depth before it is wide enough
   to hold a 30px ball. The header sits flush against the top of the viewport,
   so a craft hovering 43px above the logo is entirely off screen - measured,
   and it left a beam and a floating ball with no ship attached to them.
   Opening the cone out lets the ball ride safely inside it barely 28px down,
   which keeps the craft itself in frame.
   ⚠ ufo.js mirrors these numbers in HEADER_CONE - change both together. */
.ufo.is-header-run .ufo-beam {
  top: 14px;
  width: 64px;
  height: 78px;
  clip-path: polygon(14% 0, 86% 0, 100% 100%, 0 100%);
}

/* The craft sits ABOVE its own beam. They are siblings and the beam comes
   second in the markup, so by default it paints over the hull and washes it
   out - the light should come from under the craft, not across it. */
.ufo-craft { position: relative; z-index: 1; }
.ufo-beam { z-index: 0; }

/* A heading that is also a link. Amber so it reads as one - a heading that
   navigates and looks exactly like a heading that does not is a trap. The
   underline waits for hover so the block still reads as headings first. */
.head-link { color: var(--accent); text-decoration: none; }
.head-link:hover { text-decoration: underline; text-underline-offset: 4px; }
.head-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 4px; }

/* Landing on /page/#section must show the section's OWN heading, not start
   the reading halfway down it. Sections already carry 48-92px of top padding,
   which is enough on its own at most sizes; this makes it true under the
   sticky header at every size rather than most of them. */
section[id] { scroll-margin-top: 84px; }

/* A hero whose first line is a particle wordmark and whose second is ordinary
   heading text. The stage has to be a block for its canvas to take the full
   measured width - buildWordmark sizes the type from stage.clientWidth. */
.hero-wm-h1 .wordmark-stage {
  display: block; margin: 0 0 4px;
  /* fx.js sizes the type from this width, so this is what keeps the particle
     line the same size as the ordinary line under it. See the note in
     scripts/build-pages.mjs - the 5.8 assumes a ten-character wordmark. */
  max-width: clamp(302px, 34.8vw, 510px);
}
.hero-wm-rest { display: block; }

/* Three cards in a two-column grid leave the fourth slot empty. The mark sits
   in it rather than the grid just trailing off. The dots themselves live in a
   fixed viewport layer (mark.js), so this box only reserves the space and
   holds the fallback image - which is why the margin has to go: .hero-mark
   carries the hero's own spacing, and here it is centred in a cell instead. */
.svc-mark { display: grid; place-items: center; min-height: 180px; }
.svc-mark .hero-mark { margin: 0; }

.nav-kofi { display: flex; align-items: center; flex: none; border-radius: 10px; }
.nav-kofi img { display: block; height: 38px; width: auto; transition: transform .15s, filter .15s; }
.nav-kofi:hover img { transform: translateY(-1px); filter: brightness(1.08); }
.nav-kofi:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (max-width: 860px) { .nav-kofi { display: none; } }
.foot-side { display: flex; flex-direction: column; align-items: flex-end; gap: 18px; }
.foot-kofi { display: inline-flex; margin-right: 12px; }
.foot-kofi img { display: block; width: 130px; height: 130px; transition: transform .2s; }
@media (max-width: 759px) { .foot-side { align-items: flex-start; } }
.foot-kofi:hover img { transform: rotate(-8deg) scale(1.1); }
.nav-end { display: flex; align-items: center; gap: 10px; }
@media (min-width: 760px) { .site-foot { grid-template-columns: minmax(400px, 1fr) minmax(0, auto); } .foot-links { justify-content: flex-end; } }
.site-foot a[href^="tel:"], .site-foot a[href^="mailto:"] { white-space: nowrap; }
.telemetry .moon-glyph { font-size: 28px; }
@media (max-width: 1140px) { .nav-kofi { display: none; } }
