/* =============================================================
   [[ COMPANY NAME ]] — site styles
   One stylesheet for the whole site. Sections, in order:
     1. Tokens      2. Reset      3. Base       4. Layout
     5. Header      6. Footer     7. Buttons    8. Cards
     9. Forms      10. Utilities 11. Responsive
   ============================================================= */

/* ---- 1. Tokens ---------------------------------------------
   Every colour and size the site uses, defined once. Change a
   value here and it updates everywhere. Never hard-code a hex
   code further down the file. */
:root {
  /* Backgrounds */
  --void: #05070d;
  --surface: #0f1320;
  --raised: #151a2b;
  --line: #202741;

  /* Text */
  --frost: #e8ecf8;
  --mist: #7f89ab;

  /* Aurora ribbon */
  --aurora-green: #34e0a1;
  --aurora-cyan: #22d3ee;
  --aurora-blue: #4f7fe8;
  --aurora-violet: #8b5cf6;
  --aurora-magenta: #d946ef;

  --aurora-sweep: linear-gradient(
    120deg,
    var(--aurora-green) 0%,
    var(--aurora-cyan) 28%,
    var(--aurora-blue) 52%,
    var(--aurora-violet) 74%,
    var(--aurora-magenta) 100%
  );

  /* Spacing scale — multiples of 4px, nothing in between */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;
  --space-16: 64px;
  --space-24: 96px;

  --radius: 12px;
  --radius-sm: 8px;

  --container: 1100px;

  color-scheme: dark;
}

/* ---- 2. Reset ----------------------------------------------
   Browsers apply their own margins and sizing. These four rules
   flatten the differences so your CSS behaves predictably. */
*,
*::before,
*::after {
  box-sizing: border-box; /* padding counts inside the width, not on top of it */
}

body,
h1, h2, h3, p, ul, ol, figure {
  margin: 0;
}

img {
  max-width: 100%; /* an image can never burst out of its container */
  height: auto;
  display: block;
}

ul[class] {
  list-style: none;
  padding: 0;
}

/* ---- 3. Base ------------------------------------------------ */
/* The dark ground lives on <html>, not on <body>. The aurora
   canvas is a fixed z-index:-1 layer inside <body>, and an opaque
   body background would paint straight over it. */
html { background-color: var(--void); }

body {
  background-color: transparent;
  color: var(--frost);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;

  /* Two soft aurora blooms bled into the top corners. Fixed, so
     they stay put while the page scrolls under them. */
  background-image:
    radial-gradient(60rem 40rem at 12% -10%, rgba(52, 224, 161, 0.10), transparent 70%),
    radial-gradient(55rem 38rem at 95% 5%, rgba(139, 92, 246, 0.12), transparent 70%);
  background-attachment: fixed;
  background-repeat: no-repeat;
}

h1, h2, h3 {
  line-height: 1.2;
  font-weight: 700;
}

h1 { font-size: clamp(2rem, 5vw, 3.5rem); }
h2 { font-size: clamp(1.5rem, 3vw, 2.25rem); }
h3 { font-size: 1.25rem; font-weight: 600; }

p { max-width: 65ch; } /* long lines are hard to read — cap them */

a { color: inherit; }

.link {
  color: var(--aurora-cyan);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 150ms ease;
}
.link:hover { border-bottom-color: var(--aurora-cyan); }

/* Visible keyboard focus everywhere. Do not remove this. Tab
   through the site — if you can't see where you are, it's broken. */
:focus-visible {
  outline: 2px solid var(--aurora-cyan);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

::selection {
  background: rgba(34, 211, 238, 0.35);
}

/* ---- 4. Layout ---------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--space-6);
}

.section {
  padding: var(--space-24) 0;
}

.section + .section {
  border-top: 1px solid var(--line);
}

.section-head {
  margin-bottom: var(--space-12);
}

.section-head p {
  color: var(--mist);
  margin-top: var(--space-4);
}

/* ---- 5. Header ---------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(5, 7, 13, 0.85);
  /* Prefixed as well: WebKit only dropped the prefix in Safari 18,
     and without it the header is flat on every older iPhone. */
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px); /* the page blurs as it scrolls underneath */
  border-bottom: 1px solid var(--line);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 68px;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-decoration: none;
  font-weight: 700;
  font-size: 1.05rem;
}

/* The mark is the round logo (images/logo.png, the master) cut
   to its ring on transparency and shrunk to 128px, four times
   its shown size, so it stays crisp on any screen without
   shipping the 1.5 MB original. */
.brand-mark {
  display: block;
  height: 32px;
  width: auto;
}

.site-footer .brand-mark { height: 28px; }

/* The company name in aurora gradient. The three lines together
   clip the gradient to the shape of the letters. */
.brand-name {
  background: var(--aurora-sweep);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.nav a {
  text-decoration: none;
  color: var(--mist);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  transition: color 150ms ease, background-color 150ms ease;
}

.nav a:hover {
  color: var(--frost);
  background: var(--surface);
}

.nav a.is-active {
  color: var(--frost);
  background: var(--surface);
}

/* Hidden on desktop; the responsive block at the bottom shows it. */
.nav-toggle {
  display: none;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
}

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 20px;
  height: 2px;
  background: var(--frost);
  content: "";
}
.nav-toggle-bars::before { transform: translateY(-6px); }
.nav-toggle-bars::after { transform: translateY(4px); }

/* ---- 6. Footer ---------------------------------------------- */
.site-footer {
  border-top: 1px solid var(--line);
  padding: var(--space-12) 0;
  margin-top: var(--space-24);
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-4);
}

/* Email and phone sit side by side with space between them rather than
   a punctuation mark doing the separating. */
.footer-contact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}

/* ---- 7. Buttons --------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-6);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: transparent;
  color: var(--frost);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 150ms ease, background-color 150ms ease, opacity 150ms ease;
}

.btn:hover {
  border-color: var(--aurora-cyan);
  background: var(--surface);
}

/* The aurora button. One per section, at most — it's the thing we
   most want clicked, and two of them means neither stands out. */
.btn-primary {
  background: var(--aurora-sweep);
  border-color: transparent;
  color: #05070d; /* dark text on a bright gradient */
}

.btn-primary:hover {
  background: var(--aurora-sweep);
  opacity: 0.9;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-8);
}

/* ---- 8. Cards ----------------------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-8);
}

.card h3 { margin-bottom: var(--space-3); }
.card p { color: var(--mist); }

/* Cards reflow 3 → 2 → 1 across as the screen narrows, with no
   media query. auto-fit fits as many 260px+ columns as it can. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-6);
}

/* Four cards in an auto-fit grid wrap 3-then-1, which looks like a
   mistake. Where there are exactly four, force a 2x2 instead. */
.card-grid-2 {
  grid-template-columns: repeat(2, 1fr);
}

@media (max-width: 900px) {
  .card-grid-2 { grid-template-columns: 1fr; }
}

/* A work card stacks: description, tech tags, then the link pinned
   to the bottom so the links line up across a row of uneven cards. */
.work-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.work-card p:last-child { margin-top: auto; }

.work-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  margin: 0;
}

.tag {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 0.15rem var(--space-3);
  font-size: 0.8125rem;
  color: var(--mist);
}

/* ---- 9. Forms ----------------------------------------------- */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
}

.label {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--mist);
}

.input,
.select,
.textarea {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--frost);
  font: inherit;
  transition: border-color 150ms ease;
}

.input::placeholder,
.textarea::placeholder { color: var(--mist); }

.input:focus,
.select:focus,
.textarea:focus { border-color: var(--aurora-cyan); }

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

/* Shown after the form is submitted — see doc 07. */
.form-status {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface);
}
.form-status[hidden] { display: none; }
.form-status.is-error { border-color: var(--aurora-magenta); }

/* ---- 10. Utilities ------------------------------------------ */
.muted { color: var(--mist); }
.lead { font-size: 1.125rem; color: var(--mist); }
.center { text-align: center; }
.center p { margin-inline: auto; } /* centres the 65ch paragraph too */

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--aurora-cyan);
  margin-bottom: var(--space-3);
}

/* Visible to screen readers, invisible on screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: var(--space-2);
  background: var(--surface);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  z-index: 100;
}
.skip-link:focus { left: var(--space-4); }

/* ---- 11. Responsive ----------------------------------------
   One breakpoint. Below 720px the nav collapses behind the menu
   button and the vertical rhythm tightens up. Add more
   breakpoints only when something actually breaks — not on
   principle. */
@media (max-width: 720px) {
  .section { padding: var(--space-16) 0; }
  .site-footer { margin-top: var(--space-16); }

  .nav-toggle { display: block; }

  .nav {
    display: none;
    position: absolute;
    top: 68px;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--void);
    border-bottom: 1px solid var(--line);
    padding: var(--space-2);
  }

  /* The JS adds this class when the menu button is pressed. */
  .nav.is-open { display: flex; }

  .nav a { padding: var(--space-3) var(--space-4); }

  .footer-inner { flex-direction: column; }
}

/* Respect the OS "reduce motion" setting. */
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

.hero {
  padding-top: var(--space-24);
  padding-bottom: var(--space-24);
  position: relative;
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: var(--aurora-sweep);
  opacity: 0.6;
}

.hero .lead {
  margin-top: var(--space-4);
  max-width: 52ch;
}

.cta p {
  margin-top: var(--space-4);
}

.cta-buttons {
  justify-content: center;
}

/* ---- 12. Media ----------------------------------------------
   Everything below exists so the page is not six identical card
   grids stacked on top of each other. Each block is one idea:
   a hero with a picture, a screenshot in a browser frame, a work
   row, a full-width image band, a timeline, an FAQ. */

/* The hero: words on the left, a picture on the right. Collapses
   to one column before the text gets too narrow to read. */
.hero-grid {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: var(--space-16);
  align-items: center;
}

.hero-grid .lead { max-width: 46ch; }

.hero-media {
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line);
  position: relative;
}

/* A hairline of aurora along the top edge of the hero picture —
   the same accent as .card-glow, used once. */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--aurora-sweep);
}

/* 2:1, matching the banner's own shape. The mark sits in the right
   third of the artwork, so any crop takes the company name off it. */
.hero-media img {
  width: 100%;
  aspect-ratio: 2 / 1;
  object-fit: contain;
}

/* A screenshot pretending to be a browser window. The dots are
   decorative, so they are drawn in CSS rather than markup. */
.browser-frame {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}

.browser-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--raised);
  border-bottom: 1px solid var(--line);
}

.browser-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--line);
  flex: none;
}

.browser-url {
  margin-left: var(--space-2);
  font-size: 0.75rem;
  color: var(--mist);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.browser-frame img {
  width: 100%;
  display: block;
}

/* One piece of work: picture one side, the story the other. Every
   second row swaps them so the eye zig-zags down the page instead
   of running down a single column. */
.case {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: var(--space-16);
  align-items: center;
}

.case + .case { margin-top: var(--space-24); }

.case:nth-child(even) .case-media { order: 2; }

.case-body h3 {
  font-size: clamp(1.35rem, 2.4vw, 1.75rem);
  margin-bottom: var(--space-4);
}

/* The problem / what we built / outcome trio inside a case. */
.case-points {
  list-style: none;
  padding: 0;
  margin: var(--space-6) 0 0;
  display: grid;
  gap: var(--space-4);
}

.case-points strong {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--aurora-cyan);
  margin-bottom: var(--space-1);
}

.case-points span { color: var(--mist); }

.case .work-tags { margin-top: var(--space-6); }

/* A piece of work with no screenshot to show, so it runs full width
   under the cases rather than sitting in a half-empty two-column row. */
.feature-card {
  margin-top: var(--space-24);
}

.feature-card h3 {
  font-size: clamp(1.35rem, 2.4vw, 1.75rem);
  margin-bottom: var(--space-4);
}

.feature-card p + p { margin-top: var(--space-4); }

.feature-card .work-tags { margin-top: var(--space-6); }

@media (max-width: 900px) {
  .feature-card { margin-top: var(--space-16); }
}

/* Three steps across, for work we can describe but cannot show. Built
   from real elements rather than one wide SVG, because an SVG holding
   three panels side by side has to shrink its own labels to about six
   pixels on a phone, and these can simply stack instead. */
.flow-figure {
  margin-top: var(--space-8);
}

.flow {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-8);
  list-style: none;
  padding: 0;
  margin: 0;
}

.flow-step {
  position: relative;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--space-6);
}

/* The arrow sits in the gap between two steps, so it belongs to the
   step it points at rather than needing markup of its own. */
.flow-step + .flow-step::before {
  content: "→";
  position: absolute;
  left: calc(-1 * var(--space-4)); /* half the gap, so it sits in the middle of it */
  top: 50%;
  transform: translate(-50%, -50%);
  color: var(--mist);
}

.flow-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--aurora-cyan);
  margin-bottom: var(--space-2);
}

.flow-step h4 {
  margin: 0 0 var(--space-2);
  font-size: 1rem;
}

.flow-step p:last-child {
  color: var(--mist);
  font-size: 0.9375rem;
}

.flow-figure figcaption {
  margin-top: var(--space-4);
  font-size: 0.875rem;
  color: var(--mist);
}

@media (max-width: 900px) {
  .flow { grid-template-columns: 1fr; }

  /* Pointing down the stack rather than across it. */
  .flow-step + .flow-step::before {
    content: "↓";
    left: 50%;
    top: calc(-1 * var(--space-4));
    transform: translate(-50%, -50%);
  }
}

/* Why a piece of work has no "visit the site" button. Set apart from
   the description so it reads as a note rather than more sales copy. */
.case-note {
  margin-top: var(--space-6);
  padding-left: var(--space-4);
  border-left: 2px solid var(--line);
  font-size: 0.9375rem;
  color: var(--mist);
}

/* A full-width picture with text over it, used to break up long
   runs of cards. The gradient keeps the text readable whatever
   the photograph is doing underneath. */
.band {
  position: relative;
  padding: var(--space-24) 0;
  background-size: cover;
  background-position: center;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    rgba(5, 7, 13, 0.94) 0%,
    rgba(5, 7, 13, 0.86) 45%,
    rgba(5, 7, 13, 0.55) 100%
  );
}

.band .container {
  position: relative; /* lift the words above the ::before wash */
}

.band p { color: var(--frost); }

/* Numbered steps, left to right, with a rule running behind them. */
.timeline {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-8);
}

.timeline li {
  position: relative;
  padding-top: var(--space-8);
}

.timeline li::before {
  content: "";
  position: absolute;
  top: 13px;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
}

.timeline li:last-child::before { right: 50%; }
.timeline li:first-child::before { left: 0; }

.step-number {
  position: absolute;
  top: 0;
  left: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--void);
  background: var(--aurora-sweep);
}

.timeline h3 { margin-bottom: var(--space-2); }
.timeline p { color: var(--mist); }

/* FAQ as native accordions — no JavaScript needed for any of it. */
.faq {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.faq + .faq { margin-top: var(--space-4); }

.faq summary {
  cursor: pointer;
  padding: var(--space-6);
  font-weight: 600;
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  align-items: center;
}

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

/* The chevron is drawn from a rotated square so there is no icon
   font and no SVG to load. */
.faq summary::after {
  content: "";
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--mist);
  border-bottom: 2px solid var(--mist);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 150ms ease;
  flex: none;
}

.faq[open] summary::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

.faq summary:hover { background: var(--raised); }

.faq-body {
  padding: 0 var(--space-6) var(--space-6);
}

.faq-body p { color: var(--mist); }

/* Contact page: the form gets the room, the details sit beside it. */
.contact-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: var(--space-8);
  align-items: start;
}

.contact-detail + .contact-detail { margin-top: var(--space-6); }

@media (max-width: 900px) {
  .hero-grid,
  .case,
  .contact-grid {
    grid-template-columns: 1fr;
    gap: var(--space-12);
  }

  /* Picture above the words on every row, including the swapped ones. */
  .case:nth-child(even) .case-media { order: 0; }

  .case + .case { margin-top: var(--space-16); }

  /* Horizontal rules between steps make no sense once stacked. */
  .timeline li::before { display: none; }
}

/* ---- 12. The background ---------------------------------------
   Two generated layers over the void, fixed behind the entire
   page. Underneath: images/bg-aurora.svg, the aurora as a still
   with a few stars. Over it: the binary rain, the logo's own
   bits falling in the logo's colours and fading out over the
   lake at the bottom of the screen — the mark, turned into
   weather. No video, no canvas, no script: a few KB of SVG and
   transform-only CSS animations, so the text keeps its contrast
   everywhere, phone included.

   Readability is bought twice: by the wash below, and by the
   section panels in 12b and 21. */

.site-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  background:
    url("../images/bg-aurora.svg") center / cover no-repeat,
    var(--void);
}

/* The readability wash. The artwork keeps its full strength out
   at the edges and is eased down through the middle of the
   screen, where the words are.

   **This is the knob.** Raise the middle stop for more artwork
   behind the text, lower it for more contrast — and read the page
   in daylight before keeping it. Dimming the video itself instead
   would cost the effect everywhere to fix a problem that only
   exists in the middle column. */
.site-bg-wash {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      90deg,
      rgba(5, 7, 13, 0.10) 0%,
      rgba(5, 7, 13, 0.62) 26%,
      rgba(5, 7, 13, 0.62) 74%,
      rgba(5, 7, 13, 0.10) 100%
    );
}

/* The binary rain: the logo's bits, falling in the logo's
   colours, over the still aurora. The markup is generated by
   design/make-bg-rain.mjs; everything below is only the motion.

   Each column holds the same one-screen strip twice, end to end,
   and translates exactly one strip height per cycle, so the loop
   never jumps. Speed, phase and strength arrive per column as
   custom properties from the markup. Transforms only — this runs
   all day on the compositor and never touches layout. */
.site-bg-rain {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.rain-col {
  animation: bg-rain-fall var(--rain-dur, 24s) linear infinite;
  animation-delay: var(--rain-delay, 0s);
}

@keyframes bg-rain-fall {
  to { transform: translateY(1000px); }
}

/* The lake glints, where the rain lands. Each glint carries its
   own resting brightness as --glint-op; the shimmer breathes
   around it rather than every streak blinking in step. */
.site-bg-glint {
  animation: bg-glint-shimmer var(--glint-dur, 8s) ease-in-out var(--glint-delay, 0s) infinite alternate;
}

@keyframes bg-glint-shimmer {
  from { opacity: calc(var(--glint-op, 0.18) * 0.45); }
  to { opacity: var(--glint-op, 0.18); }
}

@media (prefers-reduced-motion: reduce) {
  /* The rain stops falling; the bits and the lake stay, still —
     which is exactly what the logo looks like. */
  .rain-col,
  .site-bg-glint {
    animation: none;
  }

  .site-bg-glint { opacity: var(--glint-op, 0.18); }
}



/* ---- 12b. Panels over the background -------------------------
   Every section below the hero gets a near-opaque wash so the
   aurora reads as depth behind the page rather than as noise
   under the words. Nudge these alphas and the whole page changes
   character, so they live here and nowhere else. */

.section {
  position: relative;
  background: rgba(5, 7, 13, 0.86);
}

/* The first section under the hero fades in from transparent, so
   the scene is not cut off by a hard edge. */
#build {
  background: linear-gradient(
    180deg,
    rgba(5, 7, 13, 0.55) 0%,
    rgba(5, 7, 13, 0.86) 160px,
    rgba(5, 7, 13, 0.86) 100%
  );
}

.site-header,
.site-footer {
  position: relative;
  background: rgba(5, 7, 13, 0.72);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.hero-scene {
  position: relative;
  padding: var(--space-24) 0 var(--space-16);
  min-height: 86vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* The hero gets no panel, or the scene would be muffled exactly
   where it should be strongest. Instead: a wash that is heavy on
   the left, where the words are, and clears to nothing on the
   right, where the aurora has the stage to itself. */
.hero-scene::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    rgba(5, 7, 13, 0.88) 0%,
    rgba(5, 7, 13, 0.72) 38%,
    rgba(5, 7, 13, 0.18) 72%,
    rgba(5, 7, 13, 0) 100%
  );
  pointer-events: none;
}

.hero-inner { position: relative; }

.hero-inner .lead { margin-top: var(--space-4); }

/* ---- 13. The hero tiles --------------------------------------
   Three services, one per tile, each jumping to its block below.
   They replaced a looping demo that duplicated section 03.

   Laid out with auto-fit rather than three fixed columns, so they
   go 3 → 1 on their own without a breakpoint being written for
   them. */

.hero-tiles {
  list-style: none;
  margin: var(--space-12) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--space-4);
}

.hero-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  height: 100%;
  /* --space-6, not --space-5: the scale runs 1 2 3 4 6 8 12 16 24
     and has no 5. An undefined token makes the whole declaration
     invalid, which computed to padding: 0 and jammed the text flat
     against the borders. It looked like a broken card, not a
     missing variable. */
  padding: var(--space-6);

  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* Translucent, not solid: the aurora has to stay visible behind
     them or the hero turns into three grey boxes on a picture. */
  background: rgba(9, 12, 22, 0.62);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);

  text-decoration: none;
  overflow: hidden;
  transition:
    transform 380ms cubic-bezier(0.16, 1, 0.3, 1),
    border-color 380ms ease,
    background-color 380ms ease;
}

/* The aurora rule along the top edge, wiped in on hover. Same
   gesture as the feature cards further down, so the page has one
   vocabulary rather than four. */
.hero-tile::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: var(--aurora-sweep);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
}

.hero-tile:hover,
.hero-tile:focus-visible {
  transform: translateY(-4px);
  border-color: rgba(52, 224, 161, 0.34);
  background: rgba(13, 18, 32, 0.72);
}

.hero-tile:hover::after,
.hero-tile:focus-visible::after { transform: scaleX(1); }

.hero-tile-num {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--mist);
}

.hero-tile-name {
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--frost);
}

.hero-tile-line {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--mist);
}

/* ---- 14. Services and their demos ---------------------------- */

.service {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: var(--space-12);
  align-items: center;
  padding: var(--space-16) 0;
  border-top: 1px solid var(--line);
}

.service-copy h3 {
  font-size: 1.75rem;
  margin: var(--space-2) 0 var(--space-4);
  letter-spacing: -0.015em;
}

.service-copy p { color: var(--mist); }
.service-copy .work-tags { margin-top: var(--space-6); }

/* On a wide screen the second service flips, so the page does not
   become a column of identical left-right rows. */
.service-reverse .service-copy { order: 2; }

.demo {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--void);
  overflow: hidden;
  min-height: 340px;
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  justify-content: center;
}

.demo-tag {
  position: absolute;
  right: var(--space-3);
  top: var(--space-3);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mist);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.1rem var(--space-3);
  background: rgba(5, 7, 13, 0.7);
}

.demo-foot {
  margin-top: var(--space-2);
  font-size: 0.8125rem;
  color: var(--mist);
}

/* ---- 15. Demo 01: the panel you can turn --------------------- */

.tilt-stage {
  perspective: 1200px;
  align-items: center;
  cursor: grab;
}

.tilt-panel {
  width: min(420px, 100%);
  transform: rotateY(-19deg) rotateX(7deg);
  transform-style: preserve-3d;
  will-change: transform;
}

.tilt-panel .browser-frame { margin: 0; }

/* ---- 16. Demo 02: the request -------------------------------- */

.api-demo { justify-content: center; }

.api-line {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 1.6em;
}

.api-method {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--void);
  background: var(--aurora-green);
  border-radius: var(--radius-sm);
  padding: 0.1rem var(--space-2);
}

.api-path,
.api-block {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-size: 0.8125rem;
  color: var(--mist);
}

.api-block {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: rgba(5, 7, 13, 0.75);
  padding: var(--space-3) var(--space-4);
  line-height: 1.7;
  min-height: 3.4em;
  white-space: pre;
  overflow-x: auto;
}

.api-arrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--aurora-cyan);
}

.api-rule {
  flex-grow: 1;
  height: 1px;
  background: var(--line);
}

.api-status {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-size: 0.8125rem;
  color: var(--frost);
}

.api-latency {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.8125rem;
  color: var(--aurora-green);
}

/* ---- 17. Demo 03: ask it something --------------------------- */

.ask-form {
  display: flex;
  gap: var(--space-2);
}

.ask-input { flex-grow: 1; }

.ask-send { flex-shrink: 0; }

.ask-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  margin: 0;
}

.ask-chip {
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--mist);
  font: inherit;
  font-size: 0.75rem;
  padding: 0.15rem var(--space-3);
  cursor: pointer;
  transition: border-color 150ms ease, color 150ms ease;
}

.ask-chip:hover {
  border-color: var(--aurora-cyan);
  color: var(--frost);
}

.ask-result {
  border-top: 1px solid var(--line);
  padding-top: var(--space-4);
  margin-top: var(--space-2);
}

.ask-matched {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: 0.8125rem;
  color: var(--mist);
}

.ask-matched svg { color: var(--aurora-green); flex-shrink: 0; }

.ask-query {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  color: var(--aurora-violet);
}

.ask-label {
  margin: var(--space-2) 0 0;
  font-size: 0.875rem;
  color: var(--mist);
}

.ask-answer {
  margin: var(--space-1) 0 0;
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 1.625rem;
  line-height: 1.2;
  color: var(--aurora-green);
}

/* An unmatched question is not a failure to hide — it is the
   safety model working, so it gets its own colour. */
.ask-demo[data-state="unmatched"] .ask-query,
.ask-demo[data-state="unmatched"] .ask-answer { color: var(--mist); }

.ask-demo[data-state="unmatched"] .ask-matched svg { color: var(--mist); }

/* ---- 18. Work cards with a picture --------------------------- */

.work-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
  gap: 0;
}

.work-card > img {
  width: 100%;
  border-bottom: 1px solid var(--line);
}

.work-card-body {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex-grow: 1;
}

.work-card-body h3 { margin: 0; }
.work-card-body p { color: var(--mist); margin: 0; }
.work-card-body .link { margin-top: auto; }

/* ---- 19. Responsive for the new sections --------------------- */

@media (max-width: 900px) {
  .service {
    grid-template-columns: 1fr;
    gap: var(--space-8);
  }

  /* The flip only makes sense side by side. Stacked, copy always
     comes first. */
  .service-reverse .service-copy { order: 0; }

}

@media (max-width: 767px) {
  /* No WebGL on a phone battery. The still carries the hero and
     the page loses nothing. */
  .hero-scene { min-height: 0; }

  .tilt-panel { transform: none; }
  .tilt-stage { perspective: none; cursor: default; }
}

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

/* ---- 20. Mock product screens --------------------------------
   For the internal systems, which have no public address to send
   anyone to. Built as markup rather than screenshots so they stay
   crisp on any display and cost no image weight.

   Everything shown in them is invented: no real client, person,
   staff number, ticket or figure appears anywhere. */

.mock {
  background: var(--void);
  border-bottom: 1px solid var(--line);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  aspect-ratio: 16 / 10;
  overflow: hidden;
  font-size: 0.75rem;
}

.mock-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--line);
}

.mock-dot {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex-shrink: 0;
}

.mock-dot-cyan { background: var(--aurora-cyan); }
.mock-dot-green { background: var(--aurora-green); }

.mock-title {
  font-weight: 600;
  color: var(--frost);
}

.mock-meta {
  margin-left: auto;
  color: var(--mist);
}

.mock-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}

.mock-stat {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.mock-stat span { color: var(--mist); font-size: 0.6875rem; }

.mock-stat b {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 1rem;
  color: var(--frost);
}

.mock-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.mock-rows li {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--line);
}

.mock-rows li:last-child { border-bottom: 0; }

.mock-rows code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  color: var(--mist);
  flex-shrink: 0;
}

.mock-rows span {
  color: var(--frost);
  flex-grow: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mock-rows em {
  font-style: normal;
  color: var(--mist);
  flex-shrink: 0;
}

.is-good { color: var(--aurora-green); }
.is-warn { color: #fbbf24; }
.is-bad { color: #fda4af; }

/* The camera panel. A silhouette and a detection box — enough to
   read as a face scan without pretending to be a photograph of
   somebody. */
.mock-camera {
  position: relative;
  flex-grow: 1;
  min-height: 96px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: linear-gradient(160deg, #0d1424 0%, #080b14 70%);
  overflow: hidden;
}

.mock-head,
.mock-body {
  position: absolute;
  background: var(--raised);
  left: 50%;
  transform: translateX(-50%);
}

.mock-head {
  width: 34px;
  height: 40px;
  border-radius: 999px;
  top: 18%;
}

.mock-body {
  width: 72px;
  height: 44px;
  bottom: 0;
  border-radius: 40px 40px 0 0;
}

.mock-frame {
  position: absolute;
  left: 50%;
  top: 12%;
  width: 58px;
  height: 62px;
  transform: translateX(-50%);
  border: 2px solid var(--aurora-green);
  border-radius: 4px;
  /* Corners only — a full box reads as a picture frame. */
  clip-path: polygon(
    0 0, 34% 0, 34% 8%, 8% 8%, 8% 34%, 0 34%,
    0 66%, 8% 66%, 8% 92%, 34% 92%, 34% 100%, 0 100%,
    100% 100%, 66% 100%, 66% 92%, 92% 92%, 92% 66%, 100% 66%,
    100% 34%, 92% 34%, 92% 8%, 66% 8%, 66% 0, 100% 0
  );
}

.mock-scan {
  position: absolute;
  left: 50%;
  top: 40%;
  width: 58px;
  height: 1px;
  transform: translateX(-50%);
  background: var(--aurora-green);
  opacity: 0.75;
}

.mock-match {
  position: absolute;
  left: var(--space-2);
  bottom: var(--space-2);
  font-family: ui-monospace, "Cascadia Mono", Menlo, monospace;
  font-size: 0.6875rem;
  color: var(--aurora-green);
  background: rgba(5, 7, 13, 0.8);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 1px var(--space-2);
}

/* ---- 21. Section separation ---------------------------------
   With a live background running behind everything, sections had
   started to read as one long scroll of text on one long field of
   numbers. Three things separate them, in order of how much work
   each does:

   1. Alternating panel depth — odd sections sit a shade lighter
      than even ones, so the boundary is visible before you read a
      word of it.
   2. An aurora hairline on the top edge, fading out at both ends
      so it reads as a seam rather than a border.
   3. Real space. The padding does most of it. */

.section {
  /* Replaces the flat 0.86 wash. The edges fade out entirely now,
     so one section dissolves into the next instead of ending in a
     hard line: the page reads as one continuous scene with
     clearings in it, which is what makes the scroll feel like a
     journey rather than a stack of panels. */
  background:
    linear-gradient(
      180deg,
      rgba(9, 12, 22, 0) 0%,
      rgba(9, 12, 22, 0.88) 18%,
      rgba(9, 12, 22, 0.88) 82%,
      rgba(9, 12, 22, 0) 100%
    );
  /* No backdrop-filter here, deliberately: it would make the
     section a stacking context, and the thread has to be able to
     paint between this background and the words. See 30. */

  /* The nav jumps to these. Without it the header lands on top of
     whatever it just scrolled to. */
  scroll-margin-top: 72px;
}

/* Every other one a shade darker, so the rhythm is visible even
   with the hairline turned off. */
.section:nth-of-type(even) {
  background:
    linear-gradient(
      180deg,
      rgba(5, 7, 13, 0) 0%,
      rgba(5, 7, 13, 0.92) 18%,
      rgba(5, 7, 13, 0.92) 82%,
      rgba(5, 7, 13, 0) 100%
    );
}

/* The seam. Replaces the plain 1px --line border, which was
   invisible against a moving background. */
.section + .section {
  border-top: 0;
}

.section::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;

  background: var(--aurora-sweep);
  /* Fades to nothing at both ends. A rule that runs edge to edge
     reads as a table; one that fades reads as a horizon. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
  opacity: 0.55;
  pointer-events: none;
}

/* The first section under the hero keeps its soft entry — the
   scene should not stop at a hard line. */
#build {
  background: linear-gradient(
    180deg,
    rgba(5, 7, 13, 0.30) 0%,
    rgba(9, 12, 22, 0.88) 220px,
    rgba(9, 12, 22, 0.88) 82%,
    rgba(9, 12, 22, 0) 100%
  );
}

#build::before { opacity: 0; }

/* The section head gets its own small marker, so the eye finds
   the start of a section without reading the heading. */
.section-head .eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.section-head .eyebrow::before {
  content: "";
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: var(--aurora-sweep);
}

/* Anchor jumps glide instead of teleporting. Paired with the
   reduced-motion block at the bottom of the file. */
html { scroll-behavior: smooth; }


/* ---- 22. The tour ---------------------------------------------
   Set up in js/tour.js. The `js-tour` class on <html> is the
   safety catch: it is added by the script, so the hidden states
   below can only ever apply on a page that is also able to
   un-hide them. No script, no blank page. */

/* ---- the homepage introducing itself ---- */
.js-tour .hero-scene .enter {
  opacity: 0;
  transform: translateY(18px);
}

.js-tour .hero-scene.is-entered .enter {
  opacity: 1;
  transform: none;
  /* Long and eased hard out of the gate: it should feel like the
     page settling, not like an animation playing. */
  transition:
    opacity 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--enter-delay, 0ms),
    transform 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--enter-delay, 0ms);
}

/* The headline gets a little more travel than the rest — it is
   the thing arriving, everything else is following it in. */
.js-tour .hero-scene h1.enter { transform: translateY(26px); }
.js-tour .hero-scene.is-entered h1.enter { transform: none; }

/* ---- the focus pull -------------------------------------------
   A section arrives out of focus and sharpens as it approaches
   the middle of the screen. Nothing slides; things become clear
   where they stand. Desktop only — the blur is a GPU cost a
   phone should not pay, so narrow screens skip straight to the
   tour. Measured against the screen, not the section: a section
   sharpens once its top has climbed 40% of the way up, so the
   words you are reading, in the middle, are never the blurred
   ones. */
.js-tour-focus .tour-section > * {
  /* On the section's children, not the section: filter and
     opacity make a stacking context, and on the section that
     would lift its background and its words together above the
     thread until it sharpened, so the line would drop behind the
     text as each section came into focus. */
  opacity: 0.45;
  filter: blur(6px);
  transition: opacity 500ms ease, filter 500ms ease;
}

.js-tour-focus .tour-section.tour-near > * {
  opacity: 0.8;
  filter: blur(2px);
}

.js-tour-focus .tour-section.tour-here > * {
  opacity: 1;
  filter: none;
}

/* ---- the guided tour ------------------------------------------
   Inside a section, each piece appears at its step on the way
   through. A soft rise and fade, once. The transition lives on
   the shown state so the hidden state is instant to apply and
   the reveal is the only thing anyone sees. */
.js-tour .tour-piece {
  opacity: 0;
  transform: translateY(14px);
}

.js-tour .tour-piece.tour-shown {
  opacity: 1;
  transform: none;
  transition:
    opacity 650ms cubic-bezier(0.16, 1, 0.3, 1),
    transform 650ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* ---- 23. The mock screens, running ---------------------------
   Driven by js/mocks.js. The rule here is the same as everywhere
   else on this page: the markup already renders a sensible still
   screen, and everything below is what happens on top of it. No
   script, no problem.

   Rows are prepended, so a new one has to open its own space
   before it can slide in — hence the grid-template-rows trick
   rather than a height animation. Animating a real height means
   knowing it first, and these rows wrap differently at every
   width. */

.mock-rows li.is-new {
  animation: mock-row-in 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
  /* The row opens from nothing, so its content has to be clipped
     on the way rather than spilling over the row below. */
  overflow: hidden;
}

@keyframes mock-row-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
    /* Squeezed flat, so the rows below are pushed down rather
       than jumped down. */
    padding-top: 0;
    padding-bottom: 0;
    max-height: 0;
  }
  40% { opacity: 0.4; }
  to {
    opacity: 1;
    transform: none;
    max-height: 3rem;
  }
}

/* A new ticket flashes its number, so the eye is taken to the
   thing that changed rather than having to hunt for it. */
.mock-rows li.is-new code {
  animation: mock-code-flash 900ms ease-out both;
}

@keyframes mock-code-flash {
  0%   { color: var(--aurora-cyan); }
  100% { color: var(--mist); }
}

/* The counters. Their value is animated in JavaScript; this is
   only the nudge that says one moved. */
.mock-stat b {
  transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* ---- the attendance camera ----
   Three states on the mock itself, so the CSS owns the showing
   and hiding and mocks.js only ever writes one of three words. */

.mock[data-mock="attendance"] .mock-frame,
.mock[data-mock="attendance"] .mock-match {
  transition: opacity 260ms ease, border-color 260ms ease, background-color 260ms ease;
}

/* Scanning: the detection box has not locked on yet. */
.mock[data-state="scanning"] .mock-frame { opacity: 0.35; }
.mock[data-state="scanning"] .mock-match {
  color: var(--mist);
  border-color: var(--line);
}

/* Matched: the box locks and the badge turns green. */
.mock[data-state="matched"] .mock-frame,
.mock[data-state="logged"] .mock-frame { opacity: 1; }

.mock[data-state="matched"] .mock-match {
  animation: mock-lock 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes mock-lock {
  0%   { transform: scale(0.94); opacity: 0.5; }
  60%  { transform: scale(1.03); opacity: 1; }
  100% { transform: none; opacity: 1; }
}

/* The scan line only sweeps while it is actually scanning —
   a sweep continuing after a match reads as a system that did
   not notice it had found somebody. */
.mock[data-state="matched"] .mock-scan,
.mock[data-state="logged"] .mock-scan { opacity: 0; animation: none; }

/* The silhouette lifts very slightly on a match. It is the only
   thing in the panel that could plausibly be a person, so it is
   the thing that should react. */
.mock[data-mock="attendance"] .mock-head,
.mock[data-mock="attendance"] .mock-body {
  transition: background-color 320ms ease;
}

.mock[data-state="matched"] .mock-head,
.mock[data-state="logged"] .mock-head,
.mock[data-state="matched"] .mock-body,
.mock[data-state="logged"] .mock-body {
  background: #1b2438;
}

/* ---- honour the setting ----
   mocks.js already returns before it starts either loop, so
   these only have to switch off what the markup carries. */
@media (prefers-reduced-motion: reduce) {
  .mock-rows li.is-new,
  .mock-rows li.is-new code,
  .mock[data-state] .mock-match {
    animation: none;
  }

  .mock-stat b,
  .mock[data-mock="attendance"] .mock-frame,
  .mock[data-mock="attendance"] .mock-match,
  .mock[data-mock="attendance"] .mock-head,
  .mock[data-mock="attendance"] .mock-body {
    transition: none;
  }
}

/* Each service block is a jump target from a hero tile, so it
   needs the same clearance the sections get — otherwise the
   sticky header lands on top of the heading it just scrolled to. */
.service { scroll-margin-top: 96px; }


/* ---- 25. The stacked case -----------------------------------
   `.case` is a two-column "copy beside a picture" grid. The
   platform block has no picture — it has a three-step flow — and
   in a half-width column those three steps came out about 135px
   each, wrapping one word per line, with the other half of the
   page empty. So this variant stacks instead.

   Its own class rather than an override on #platform: the shape
   belongs to "a case with a diagram instead of a screenshot",
   not to one section id. */

.case-stacked {
  display: block;
}

/* Copy is held to a readable measure even though the block is
   full width. A 1100px line of body text is not a line anybody
   finishes. */
.case-stacked .case-body {
  max-width: 62ch;
}

.case-stacked .case-body h2 {
  margin-bottom: var(--space-4);
}

.case-stacked .case-body p + p {
  margin-top: var(--space-4);
}

.case-stacked .flow-figure {
  margin-top: var(--space-12);
}

/* The flow now gets the whole width, so the three steps are real
   columns rather than slivers. */
.case-stacked .flow {
  gap: var(--space-6);
}

/* Tags on the left, the note on the right, on one line under the
   flow — rather than both floating loose in grid cells the way
   they were. */
.case-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: var(--space-8);
  align-items: start;
  margin-top: var(--space-12);
}

.case-foot .work-tags {
  margin: 0;
}

.case-foot .case-note {
  margin-top: 0;
}

@media (max-width: 900px) {
  /* Stacked, the three steps become three rows; the arrows point
     across a gap that no longer exists. */
  .case-stacked .flow {
    grid-template-columns: 1fr;
  }

  .case-stacked .flow-step + .flow-step::before {
    content: "↓";
    left: 50%;
    top: calc(-1 * var(--space-3));
  }

  .case-foot {
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }
}

/* ---- the mock inside a demo frame -----------------------------
   The workflow mock moved out of its work card and into a demo
   panel, so the frame comes from .demo and the mock loses its own
   bottom rule. Room at the top keeps the demo tag clear of the
   mock's title bar. */
.demo-flush {
  min-height: 0;
  padding: var(--space-6) var(--space-4) var(--space-4);
}

.demo-flush .mock {
  border-bottom: 0;
}


/* ---- 26a. The drawing budget ---------------------------------
   js/main.js sets data-power="lean" on a machine that is likely
   to feel the decoration: reduced motion, a coarse pointer, four
   cores or less, or four gigabytes or less. What follows is what
   gets dropped there.

   It drops effects, never content and never layout. A lean
   machine sees the same page, minus the things it would have paid
   for in dropped frames: the live blur behind the header, and the
   binary rain. Nothing here changes what the page says. */

/* backdrop-filter is a re-blur of everything behind the element,
   every frame it moves. The header is sticky, so on a lean
   machine that is a full-width blur recomputed for the whole
   scroll. The flat panel behind it reads almost the same. */
[data-power="lean"] .site-header,
[data-power="lean"] .nav,
[data-power="lean"] .hero-tile {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

[data-power="lean"] .site-header {
  background: rgba(5, 7, 13, 0.96);
}

/* Eleven columns translating for ever. Cheap per column on a
   discrete GPU, not cheap on eleven layers of integrated one. */
[data-power="lean"] .site-bg-rain,
[data-power="lean"] .site-bg-glint {
  display: none;
}

/* The hero aurora keeps its shape but stops breathing: the glow
   is a blurred ellipse, and animating it re-runs the blur. */
[data-power="lean"] .hero-aurora svg,
[data-power="lean"] .hero-aurora-glow,
[data-power="lean"] .hero-stream {
  animation: none;
}

/* ---- 26b. The AI enablement panel ----------------------------
   The permission list, not a chat window. The refusals are the
   point of the panel, so they are styled as firmly as the
   approvals rather than as errors - a refused request is the
   system working, not failing. */
.guard-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.guard-result {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}

.guard-verdict {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 650;
  color: var(--aurora-green, #34e0a1);
}

.guard-demo[data-verdict="refused"] .guard-verdict {
  color: var(--aurora-magenta);
}

.guard-why {
  margin: var(--space-2) 0 0;
  color: var(--mist);
  font-size: 0.875rem;
}

/* The rule that decided it, quoted in the same monospace the
   approved queries use: this is a setting somebody agreed, not an
   opinion the model formed. */
.guard-rule {
  margin: var(--space-3) 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  color: var(--mist);
  opacity: 0.85;
}

.guard-rule:empty { display: none; }

/* ---- 27. The IBV explainer ----------------------------------
   One question making the trip through the platform: asked,
   matched, answered. js/ibv.js moves the container through its
   data-state values; everything visual is here. The markup is
   the finished frame, so no-JS and reduced motion both read
   complete. */
.ibv-demo {
  display: grid;
  gap: var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--void);
  padding: var(--space-6);
}

.ibv-step {
  position: relative;
  padding-left: var(--space-6);
}

/* The connector down the left edge: the trip the question takes. */
.ibv-step::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  bottom: 4px;
  width: 2px;
  border-radius: 2px;
  background: var(--line);
  transition: background 400ms ease;
}

.ibv-demo[data-state="asking"] .ibv-step:nth-child(1)::before,
.ibv-demo[data-state="matching"] .ibv-step:nth-child(2)::before,
.ibv-demo[data-state="joining"] .ibv-step:nth-child(3)::before,
.ibv-demo[data-state="answering"] .ibv-step:nth-child(4)::before,
.ibv-demo[data-state="done"] .ibv-step::before {
  background: var(--aurora-green, #34e0a1);
}

.ibv-question {
  margin: var(--space-2) 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.0625rem;
  min-height: 1.6em;
}

/* The caret only blinks while the question is being asked. */
.ibv-caret {
  display: inline-block;
  width: 0.55em;
  height: 1.1em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: currentColor;
  opacity: 0;
}

.ibv-demo[data-state="asking"] .ibv-caret {
  opacity: 1;
  animation: ibv-blink 900ms steps(1) infinite;
}

@keyframes ibv-blink {
  50% { opacity: 0; }
}

.ibv-queries {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.ibv-queries li {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.2rem var(--space-3);
  font-size: 0.8125rem;
  color: var(--mist);
  transition: border-color 240ms ease, color 240ms ease, background 240ms ease;
}

/* The scan passing over each approved query in turn. */
.ibv-queries li.is-scan {
  border-color: var(--aurora-cyan, #38e1ff);
  color: var(--frost);
}

/* The lock: the matched query keeps the green. */
.ibv-queries li.is-hit {
  border-color: var(--aurora-green, #34e0a1);
  background: rgba(52, 224, 161, 0.12);
  color: var(--frost);
}

.ibv-note {
  margin: var(--space-3) 0 0;
  font-size: 0.8125rem;
  color: var(--mist);
}

.ibv-answer {
  margin: var(--space-2) 0 0;
  font-size: 2rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--mist);
  transition: color 400ms ease;
}

.ibv-demo[data-state="answering"] .ibv-answer,
.ibv-demo[data-state="done"] .ibv-answer {
  color: var(--aurora-green, #34e0a1);
}

/* The money line under the answer arrives with the answer, not
   before it - a figure sitting there while the question is still
   being typed reads as part of the question. */
.ibv-answer-note {
  opacity: 0;
  transition: opacity 400ms ease;
}

.ibv-demo[data-state="answering"] .ibv-answer-note,
.ibv-demo[data-state="done"] .ibv-answer-note {
  opacity: 1;
}

/* ---- The join ------------------------------------------------
   Two counts from two systems. The numbers matter more than the
   labels, so they lead and the source sits under them in caps. */
.ibv-sets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin-top: var(--space-3);
}

.ibv-set-count {
  margin: 0;
  font-size: 1.375rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--frost);
}

.ibv-set-name {
  margin: 0;
  font-size: 0.875rem;
  color: var(--mist);
}

.ibv-set-src {
  margin: 2px 0 0;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mist);
  opacity: 0.7;
}

/* ---- The graphs ----------------------------------------------
   One per question. A set difference wants a split bar, a decline
   wants a line, a ranking wants bars - drawing all three the same
   way would be tidier and would say less. js/ibv.js sets
   data-graph and unhides the matching figure; hidden figures reset
   their own animation, so each one draws fresh when its question
   comes round again. */
.ibv-graph {
  margin: var(--space-4) 0 0;
}

.ibv-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: var(--space-3) 0 0;
  font-size: 0.8125rem;
  color: var(--mist);
}

.ibv-key::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 0.5em;
  border-radius: 3px;
  vertical-align: baseline;
}

.ibv-key-have::before { background: rgba(255, 255, 255, 0.16); }
.ibv-key-miss::before { background: var(--aurora-green, #34e0a1); }
.ibv-key-miss { color: var(--frost); }

/* The split bar: everyone, and then the part of everyone that is
   the opportunity. */
.ibv-gap {
  display: flex;
  height: 14px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
}

.ibv-gap span {
  width: var(--w);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
}

.ibv-gap-have { background: rgba(255, 255, 255, 0.16); }

/* The opportunity waits for the rest of the bar to arrive, so the
   eye sees the whole first and the remainder second - that order
   is the point being made. */
.ibv-gap-miss {
  background: linear-gradient(90deg, #38e1ff, #34e0a1);
  transition-delay: 620ms;
}

/* The trend line draws itself left to right: a decline you watch
   happen reads as a decline, where the same line already drawn is
   just a shape. */
.ibv-trend {
  display: block;
  width: 100%;
  height: 90px;
  overflow: visible;
}

.ibv-trend-line {
  fill: none;
  stroke: #38e1ff;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 420;
  stroke-dashoffset: 420;
  transition: stroke-dashoffset 1400ms ease-out;
}

/* The ranking: longest first, so the bar lengths carry the order
   and the numbers only confirm it. */
.ibv-rank {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-3);
}

.ibv-rank li {
  display: grid;
  grid-template-columns: 8.5rem 1fr auto;
  align-items: center;
  gap: var(--space-3);
  font-size: 0.8125rem;
  color: var(--mist);
}

.ibv-rank-bar {
  height: 10px;
  width: var(--w);
  border-radius: 999px;
  background: linear-gradient(90deg, #38e1ff, #34e0a1);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
}

.ibv-rank li:nth-child(2) .ibv-rank-bar { transition-delay: 110ms; }
.ibv-rank li:nth-child(3) .ibv-rank-bar { transition-delay: 220ms; }
.ibv-rank li:nth-child(4) .ibv-rank-bar { transition-delay: 330ms; }

.ibv-rank-val {
  font-variant-numeric: tabular-nums;
  color: var(--frost);
}

/* Narrow: the name column and the bar cannot both have room, and
   a 40px bar ranks nothing. Give the bar the full width and put
   the name and figure on the line above it. */
@media (max-width: 720px) {
  .ibv-rank li {
    grid-template-columns: 1fr auto;
    gap: var(--space-2) var(--space-3);
  }

  .ibv-rank-bar {
    grid-column: 1 / -1;
    grid-row: 2;
  }

  .ibv-rank-val { grid-row: 1; }
}

/* Every graph draws on the same cue: the answer arriving. */
.ibv-demo[data-state="answering"] .ibv-gap span,
.ibv-demo[data-state="done"] .ibv-gap span,
.ibv-demo[data-state="answering"] .ibv-rank-bar,
.ibv-demo[data-state="done"] .ibv-rank-bar {
  transform: scaleX(1);
}

.ibv-demo[data-state="answering"] .ibv-trend-line,
.ibv-demo[data-state="done"] .ibv-trend-line {
  stroke-dashoffset: 0;
}

@media (prefers-reduced-motion: reduce) {
  .ibv-caret { display: none; }
  .ibv-gap span,
  .ibv-rank-bar {
    transition: none;
    transform: scaleX(1);
  }
  .ibv-trend-line {
    transition: none;
    stroke-dashoffset: 0;
  }
}

/* ---- 28. The landing animation -------------------------------
   Three streams of light flowing through the aurora colours on
   the right of the hero, where the wash clears. Pure CSS over
   inline SVG: the streams are dashed strokes whose offsets
   cycle, the glow breathes, and the whole scene drifts a few
   pixels so it reads as weather, not as a screensaver.

   It sits between the wash and the words by DOM order, so no
   z-index is involved, and it never sits behind a paragraph:
   the left of the hero keeps its heavy wash. */
.hero-aurora {
  position: absolute;
  inset: 0 0 0 auto;
  width: 58%;
  pointer-events: none;
  overflow: hidden;

  /* The streams are dashes, and a dash crossing a sentence reads
     as part of the sentence for the half second it takes to
     realise it is not. The thread solved this by masking the
     words out of itself; this is the same idea from the other
     side - the aurora fades out towards the copy and is only
     itself well clear of it. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, rgba(0, 0, 0, 0.25) 34%, #000 64%);
  mask-image: linear-gradient(to right, transparent 0, rgba(0, 0, 0, 0.25) 34%, #000 64%);
}

.hero-aurora svg {
  width: 100%;
  height: 100%;
  animation: hero-drift 11s ease-in-out infinite alternate;
}

.hero-stream {
  stroke-dasharray: 2 14;
  animation: hero-flow 2.8s linear infinite;
}

.hero-stream-2 { animation-duration: 3.6s; opacity: 0.8; }
.hero-stream-3 { animation-duration: 4.4s; opacity: 0.6; }

.hero-aurora-glow {
  animation: hero-breathe 7s ease-in-out infinite alternate;
}

@keyframes hero-flow {
  to { stroke-dashoffset: -32; }
}

@keyframes hero-breathe {
  from { opacity: 0.07; }
  to { opacity: 0.14; }
}

@keyframes hero-drift {
  from { transform: translateY(-6px); }
  to { transform: translateY(8px); }
}

/* Narrow screens: the hero stacks and the words need the stage,
   so the streams dim to a whisper behind everything. */
@media (max-width: 720px) {
  .hero-aurora {
    width: 100%;
    opacity: 0.4;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-aurora svg,
  .hero-stream,
  .hero-aurora-glow {
    animation: none;
  }
}

/* ---- 30. The thread ------------------------------------------
   The journey made visible: not a ruler, a squiggle. One aurora
   line wanders the whole page, looping beside each section and
   twisting between them, and fills with colour as you scroll.
   The path itself is drawn by js/thread.js to fit the real page;
   the stylesheet only decides how it looks. The ghost is the
   whole route, faint; the fill is the part you have travelled. */
.thread {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

/* The thread goes under the words and over the section
   backgrounds. It is a positioned layer at z-index 0, so it
   paints after every section's in-flow background; the content
   containers are lifted to z-index 1, so they paint after it.
   That is why .section must not be a stacking context: one would
   glue its background and its words together, and the thread
   could only be above both or below both. */
.section > .container,
.hero-inner {
  position: relative;
  z-index: 1;
}

.thread-svg {
  display: block;
  overflow: visible;
}

.thread-path-fill {
  /* Under the words, so a little quieter than it was above them.
     The lit length steps forward a section at a time; the
     transition is what makes that a glide rather than a jump. */
  stroke-opacity: 0.8;
  filter: drop-shadow(0 0 5px rgba(34, 211, 238, 0.3));
  transition: stroke-dashoffset 900ms ease;
}

/* The marker on each section's loop. Dark until its section is
   the one crossing the middle of the screen; then it fills with
   cyan and glows, so the thread tells you where you are, not
   just how far you have come. */
.thread-marker {
  fill: #05070d;
  stroke: rgba(233, 240, 255, 0.25);
  stroke-width: 1.5;
  transition: fill 300ms ease, stroke 300ms ease, filter 300ms ease;
}

/* The arrowhead that ends the journey, pointing at the contact
   details. It arrives with the last section rather than sitting
   there from the start. */
.thread-arrow {
  fill: #22d3ee;
  opacity: 0;
  transition: opacity 600ms ease;
  filter: drop-shadow(0 0 7px rgba(34, 211, 238, 0.7));
}

.thread-arrow.is-shown { opacity: 1; }

.thread-marker.is-current {
  fill: #22d3ee;
  stroke: #22d3ee;
  filter: drop-shadow(0 0 7px rgba(34, 211, 238, 0.8));
}

/* Narrow screens: the squiggle would sit on top of the words, so
   it steps out of the way. The journey on a phone is a thumb
   scroll; it does not need a map. */
@media (max-width: 1100px) {
  .thread { display: none; }
}
