/* ============================================================
   Tyko Tech, Circuit Grove component library
   Consumes site/assets/css/tokens.css exclusively: every color,
   size, radius, and duration below references a token. The only
   raw values are the media-query breakpoints (880px / 560px),
   because custom properties cannot appear in media conditions,
   plus zeros, unitless ratios, and gradient stop percentages.
   ============================================================ */

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

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--color-bg-base);
  color: var(--color-text-primary);
  font: 400 var(--text-body)/var(--leading-body) var(--font-body);
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--color-text-primary);
  margin: 0;
}

p { margin: 0; }

:focus-visible {
  outline: var(--focus-width) solid var(--color-accent);
  outline-offset: var(--focus-offset);
}

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

/* ---- Skip link: hidden until keyboard focus ---- */
.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 30;
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-4);
  border-radius: var(--radius);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font: 600 var(--text-ui)/1 var(--font-display);
  text-decoration: none;
  transform: translateY(calc(-1 * (var(--touch-target) + var(--space-5))));
}
.skip-link:focus-visible {
  transform: none;
  outline-color: var(--color-text-primary);
}

/* ---- Header ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--color-bg-raised);
  border-bottom: var(--hairline) solid var(--color-border);
}
.site-header-inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-5);
  min-height: var(--space-7);
  padding-block: var(--space-2);
}
/* The lockup never shrinks: as a flex item it would otherwise be
   squeezed narrower than its aspect ratio on small screens, which
   distorts the wordmark rather than scaling it. */
.site-logo {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  min-height: var(--touch-target);
}
.site-logo img { height: var(--space-5); width: auto; }

.site-nav { margin-left: auto; }
.site-nav ul {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  flex-wrap: wrap;
}
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-1);
  font: 500 var(--text-ui)/1 var(--font-display);
  color: var(--color-text-primary);
  text-decoration: none;
  border-bottom: var(--rule-weight) solid transparent;
  transition: color var(--duration-base) ease, border-color var(--duration-base) ease;
}
.site-nav a:hover { color: var(--color-accent); }
.site-nav a[aria-current="page"] {
  color: var(--color-accent);
  border-bottom-color: var(--color-accent);
}

/* Theme toggle: a bare icon control carrying a sun and a moon,
   with no border or fill of its own (owner decision, revision 8).
   The visible icon names the theme you would GET by pressing it,
   so the moon shows while the default light theme is rendered.
   The hit area keeps the full touch target; only the glyph is
   drawn. Like the nav toggle it ships with the hidden attribute
   and is revealed by theme.js, because with no script it cannot
   work and a dead control is worse than no control. */
.theme-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: var(--touch-target);
  height: var(--touch-target);
  flex-shrink: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-text-primary);
  cursor: pointer;
  transition: color var(--duration-base) ease;
}
.js .theme-toggle:not([hidden]) { display: inline-flex; }
.theme-toggle:hover { color: var(--color-accent); }
.theme-icon {
  /* 80% of the former --space-4 glyph (owner decision, revision 8) */
  width: calc(var(--space-4) * 0.8);
  height: calc(var(--space-4) * 0.8);
  display: block;
}
/* Default (light rendered): offer the moon. An explicit dark choice
   swaps in the sun. Both icons ship in the markup so the swap is
   pure CSS and never flashes the wrong one. */
.theme-icon-sun { display: none; }
:root[data-theme="dark"] .theme-icon-moon { display: none; }
:root[data-theme="dark"] .theme-icon-sun { display: inline; }

/* Mobile nav toggle: hidden attribute in markup keeps it out of
   no-JS flows entirely; theme.js reveals and wires it. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text-primary);
  font: 600 var(--text-ui-sm)/1 var(--font-display);
  cursor: pointer;
  transition: border-color var(--duration-base) ease, color var(--duration-base) ease;
}
.nav-toggle:hover { border-color: var(--color-accent); color: var(--color-accent); }

/* ---- Hero (landing page) ---- */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-6);
  align-items: center;
  padding-block: var(--space-7) var(--space-8);
}
.hero-lede {
  margin-top: var(--space-3);
  font-size: var(--text-body-lg);
  color: var(--color-text-secondary);
  max-width: var(--measure-lede);
}
.hero-art {
  justify-self: center;
  width: 100%;
  max-width: var(--measure-field);
}

/* ---- Themed assets (light/dark photograph swap) ----
   A .themed-img holds two slots classed .if-light / .if-dark
   (the class sits on the <img>; <picture> wrappers are fine).
   Visibility follows the EFFECTIVE theme with the same two-state
   resolution as tokens.css: bare root is dark (the default), and
   an explicit light choice flips it. */
.themed-img .if-light { display: block; }
.themed-img .if-dark { display: none; }
:root[data-theme="dark"] .themed-img .if-light { display: none; }
:root[data-theme="dark"] .themed-img .if-dark { display: block; }

/* Hero photograph: radius and the system's one soft shadow. */
.hero-photo {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-soft);
}
.hero-photo img {
  display: block;
  width: 100%;
  height: auto;
}

/* Ambient hero loop: sits above the themed stills. theme.js
   reveals and plays it only when the visitor's system allows
   motion; with no JS, or under reduced motion, it stays hidden
   and (preload="none") never downloads. */
.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero-video[hidden] { display: none; }

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-height);
  padding: 0 var(--space-4);
  border-radius: var(--radius);
  border: var(--hairline) solid transparent;
  font: 600 var(--text-ui)/1 var(--font-display);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--duration-base) ease,
    color var(--duration-base) ease,
    border-color var(--duration-base) ease,
    transform var(--duration-fast) ease;
}
.btn:active:not(:disabled) { transform: translateY(var(--press)); }
.btn:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }

.btn-primary { background: var(--color-accent); color: var(--color-on-accent); }
.btn-primary:hover:not(:disabled) { background: var(--color-accent-hover); }

.btn-secondary {
  background: transparent;
  color: var(--color-text-primary);
  border-color: var(--color-border-strong);
}
.btn-secondary:hover:not(:disabled) {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.btn-quiet { background: transparent; color: var(--color-accent); }
.btn-quiet:hover:not(:disabled) {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}

/* ---- Text links ---- */
.link,
.prose a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: var(--underline-offset);
  text-decoration-thickness: var(--hairline);
  transition: color var(--duration-base) ease;
}
.link:hover,
.prose a:hover { color: var(--color-accent-hover); }

/* ---- Badges ---- */
.badge {
  display: inline-flex;
  align-items: center;
  min-height: var(--badge-height);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  font: 500 var(--text-caption)/1 var(--font-display);
  color: var(--color-text-primary);
}

/* ---- Cards ---- */
.cards {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-4);
  align-items: start;
}
.card {
  background: var(--color-bg-base);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition:
    transform var(--duration-base) ease,
    border-color var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}
.card:hover,
.card:focus-within {
  transform: translateY(calc(-1 * var(--lift)));
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-soft);
}
.card-featured { grid-column: span 7; }
.card-compact { grid-column: span 5; }
.card-half { grid-column: span 6; }
.card-media {
  aspect-ratio: 16 / 7;
  display: flex;
  align-items: flex-end;
  padding: var(--space-4);
  background:
    radial-gradient(120% 150% at 85% 0%, color-mix(in srgb, var(--color-accent) 22%, transparent) 0%, transparent 55%),
    linear-gradient(150deg, color-mix(in srgb, var(--color-accent) 12%, var(--color-bg-raised)) 0%, var(--color-bg-base) 100%);
  border-bottom: var(--hairline) solid var(--color-border);
}
.card-wordmark {
  font: 700 var(--text-display-m)/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--color-accent);
}

/* Photo media variant: an image replaces the gradient; a bottom
   scrim keeps the wordmark legible on any photo in both themes
   (measured 9.87:1 worst case for on-photo type over the scrim). */
.card-media-photo { position: relative; }
.card-photo {
  position: absolute;
  inset: 0;
}
.card-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.card-media-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--photo-scrim);
  pointer-events: none;
}
.card-media-photo .card-wordmark,
.card-media-photo .card-logo {
  position: relative;
  z-index: 1;
}
.card-media-photo .card-wordmark { color: var(--color-on-photo); }

/* Project-logo slots. Graceful fallback contract: a page emits the
   logo markup ONLY once its files exist on disk; until then the
   wordmark treatment above IS the component's fallback state, so
   no broken images can ever ship. The card slot sits bottom-left,
   in the calm corner every card photograph was composed to leave
   empty, so the mark never lands on the subject. */
.card-logo {
  display: block;
  margin-right: auto;
  max-height: var(--space-6);
  max-width: 62%;
  width: auto;
}
.project-logo {
  display: block;
  max-height: var(--space-8);
  width: auto;
  margin-bottom: var(--space-3);
}
.card-body { padding: var(--space-4); }
.card-body h3 {
  font-size: var(--text-title);
  font-weight: 600;
  margin: var(--space-2) 0;
}
.card-compact .card-body h3 { font-size: var(--text-title-sm); }
.card-title-link {
  color: var(--color-text-primary);
  text-decoration: none;
}
.card-title-link:hover { color: var(--color-accent); }
.card-text { color: var(--color-text-secondary); font-size: var(--text-ui); }
.card-meta {
  margin-top: var(--space-3);
  font: 500 var(--text-ui-sm)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
}

/* ---- Whole-card click target ----
   The title link stretches an invisible overlay across the whole
   card, so a click anywhere opens the project. It stays ONE link
   in the accessibility tree (no duplicate announcements, keyboard
   focus lands on the title), and every other link inside the card
   is lifted above the overlay so it keeps its own destination.
   Text remains selectable outside the overlay's own area. */
.card,
.project-row { position: relative; }
.card-title-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
.card a:not(.card-title-link),
.project-row a:not(.card-title-link) {
  position: relative;
  z-index: 2;
}

/* ---- Capability rail (.rail) ----
   Wide, hairline-separated rows: the name of a thing on the left,
   what it is on the right. Replaces stacked card grids wherever a
   set of capabilities was reading as two tall narrow columns of
   prose. Rows are full container width, so the text measure stays
   comfortable and the eye scans down one edge. */
.rail {
  margin-top: var(--space-5);
  border-top: var(--hairline) solid var(--color-border);
}
.rail-item {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  gap: var(--space-3) var(--space-6);
  padding-block: var(--space-5);
  border-bottom: var(--hairline) solid var(--color-border);
}
.rail-name h3 {
  font: 600 var(--text-title)/var(--leading-subhead) var(--font-display);
  letter-spacing: var(--tracking-snug);
}
.rail-name .meta-chips { margin-top: var(--space-3); }
.rail-detail p {
  color: var(--color-text-secondary);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  max-width: var(--measure-body);
}
.rail-detail p + p { margin-top: var(--space-3); }
.rail-detail .link { font-family: var(--font-display); font-size: var(--text-ui); }
.rail-cta { margin-top: var(--space-3); }

/* ---- Feature panel ----
   One wide photograph carrying a short block of type in its calm
   half. Used sparingly: it is the loudest device on the site, so
   a page gets at most one. The whole panel is a single link. */
.feature-panel {
  position: relative;
  display: flex;
  align-items: center;
  min-height: clamp(19rem, 32vw, 25rem);
  margin-top: var(--space-5);
  border-radius: var(--radius);
  overflow: hidden;
  color: var(--color-on-photo);
  text-decoration: none;
  transition:
    transform var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}
.feature-panel:hover,
.feature-panel:focus-visible {
  transform: translateY(calc(-1 * var(--lift)));
  box-shadow: var(--shadow-soft);
}
.feature-panel-photo,
.feature-panel-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.feature-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--photo-scrim-left);
  pointer-events: none;
}
.feature-panel-body {
  position: relative;
  z-index: 1;
  max-width: 36rem;
  padding: var(--space-6);
}
.feature-panel-eyebrow {
  font: 600 var(--text-ui-sm)/1 var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.82;
}
/* The heading colour is set explicitly: the global h1-h4 rule would
   otherwise beat the panel's inherited on-photo colour and paint
   dark type onto a dark scrim. */
.feature-panel-title {
  margin-top: var(--space-3);
  font: 600 var(--text-display-m)/var(--leading-heading) var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--color-on-photo);
}
.feature-panel-text {
  margin-top: var(--space-3);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  opacity: 0.92;
}
.feature-panel-cue {
  display: inline-block;
  margin-top: var(--space-4);
  font: 600 var(--text-ui)/1 var(--font-display);
  border-bottom: var(--rule-weight) solid currentColor;
  padding-bottom: var(--space-1);
}

/* ---- Project rows ----
   The portfolio entry as a wide horizontal card: photograph on one
   side, the writing on the other, sides alternating down the page.
   Same surface language as .card, but the text sits in a short
   wide column instead of a tall narrow one. */
.project-rows {
  display: grid;
  gap: var(--space-5);
  margin-top: var(--space-5);
}
.project-row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  align-items: stretch;
  background: var(--color-bg-base);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition:
    transform var(--duration-base) ease,
    border-color var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}
.project-row:hover,
.project-row:focus-within {
  transform: translateY(calc(-1 * var(--lift)));
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-soft);
}
.project-row-media {
  display: flex;
  align-items: flex-end;
  min-height: 15rem;
  padding: var(--space-4);
  border-right: var(--hairline) solid var(--color-border);
}
.project-rows-lead .project-row-media { min-height: 18rem; }
.project-row:nth-child(even) > .project-row-media {
  grid-column: 2;
  grid-row: 1;
  border-right: 0;
  border-left: var(--hairline) solid var(--color-border);
}
.project-row:nth-child(even) > .project-row-body {
  grid-column: 1;
  grid-row: 1;
}
.project-row-body {
  padding: var(--space-5);
  align-self: center;
}
.project-row-body h3 {
  font-size: var(--text-title);
  font-weight: 600;
  margin-bottom: var(--space-2);
}
.project-rows-lead .project-row-body h3 { font-size: var(--text-display-m); }

/* ---- Form field: label + input + hint + error/success message ---- */
.field { max-width: var(--measure-field); }
.field label {
  display: block;
  font: 600 var(--text-ui)/var(--leading-ui) var(--font-display);
  margin-bottom: var(--space-2);
}
.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: var(--control-height);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  background: var(--color-bg-base);
  color: var(--color-text-primary);
  font: 400 var(--text-body)/var(--leading-ui) var(--font-body);
  transition: border-color var(--duration-base) ease;
}
.field input::placeholder,
.field textarea::placeholder {
  color: var(--color-text-secondary);
  opacity: 1;
}
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: var(--focus-width) solid var(--color-accent);
  outline-offset: var(--focus-offset);
  border-color: var(--color-accent);
}
.field input:disabled,
.field select:disabled,
.field textarea:disabled {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
}
.field-hint {
  margin-top: var(--space-2);
  font: 400 var(--text-caption)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
}
.field-error,
.field-success {
  margin-top: var(--space-2);
  font: 500 var(--text-caption)/var(--leading-ui) var(--font-display);
}
.field-error { color: var(--color-error); }
.field-success { color: var(--color-success); }
.field.is-error input,
.field.is-error select,
.field.is-error textarea { border-color: var(--color-error); }
.field.is-error input:focus-visible,
.field.is-error select:focus-visible,
.field.is-error textarea:focus-visible { outline-color: var(--color-error); }

/* ---- Contact form ----
   Progressive enhancement: the form carries a real action and
   method, so with no JS it validates natively and posts to the
   form service. contact-form.js takes over to validate inline and
   submit in the background. The recipient address lives only in
   the service's opaque key, never in the page. */
.contact-form {
  display: grid;
  gap: var(--space-4);
  margin-top: var(--space-5);
  max-width: var(--measure-lede);
}
.contact-form .field { max-width: none; }
/* Name and email share a row so the form reads as a block of work
   rather than a tall stack of full-width strips. */
.contact-form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}
.contact-form textarea {
  min-height: 9rem;
  resize: vertical;
  line-height: var(--leading-body);
}
.contact-form .btn { justify-self: start; }

/* ---- Contact panel ----
   The form is the point of the Contact page, so it sits at the top
   of the body on its own plate, at full column width, with the
   short practical notes beside it rather than stacked above it.
   The plate uses the PAGE ground inside the raised band, which is
   what lifts it off the surrounding surface. */
.contact-panel {
  display: grid;
  /* 8fr, not the 7fr this panel carried through revision 8: the hosted
     form is an iframe that cannot reflow to a narrow column the way the
     native form's own fields did, and Microsoft's embed snippet asks for
     640px. This split lands the frame at 643px. */
  grid-template-columns: minmax(0, 8fr) minmax(0, 4fr);
  gap: var(--space-6);
  align-items: start;
}
.contact-panel-main {
  background: var(--color-bg-base);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-5);
  box-shadow: var(--shadow-soft);
}
.contact-panel-main .contact-form { max-width: none; }
.contact-panel-side {
  display: grid;
  gap: var(--space-4);
  align-content: start;
}
.form-status {
  font: 500 var(--text-ui-sm)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
}
.form-status:empty { display: none; }
.form-status.is-error { color: var(--color-error); }
.form-status.is-success { color: var(--color-success); }

/* ---- External form embed (.form-embed) ----
   The contact form is hosted by Microsoft Forms and rendered in an
   iframe, so nothing inside it can be styled from here: its colour is
   set in the Forms theme instead. What this plate does is make the
   embed read as a deliberate card in BOTH themes rather than a bright
   rectangle dropped on the dark grove, using the brand radius, the
   system's one soft shadow, and a ground that stays light because the
   embed itself always is. */
.form-embed {
  border-radius: var(--radius);
  overflow: hidden;
  border: var(--hairline) solid var(--color-border);
  background: var(--color-embed-ground);
}
/* The height is FIXED, not viewport-relative, because what has to fit
   is the form's content and that does not care how tall the window is.
   Measured against the live form: 1251px at this column width, 1334px
   at the mobile width below, plus room for validation messages. Too
   short and Microsoft's own scrollbar appears inside the frame, which
   hides the Submit button behind a nested scroll almost nobody looks
   for. Re-measure if the form gains or loses questions. */
.form-embed iframe {
  display: block;
  width: 100%;
  height: 1320px;
  border: 0;
}
.form-embed-note { margin-top: var(--space-3); }

/* ---- Quote rotator ----
   A set of situations shown one at a time on the stone tablet.
   These are illustrative situations written in a client's voice,
   never attributed quotes, so nothing here reads as a testimonial.

   The panel is deliberately quiet: no controls, no accent bar, the
   quote set in the body italic rather than in display weight, and
   the stone held under a heavy scrim so it reads as grain rather
   than as a photograph. It changes on its own and asks nothing.

   FAIL-OPEN: with no JS, and under reduced motion, every item is
   visible as a plain stacked list, so the panel is complete and
   readable and nothing moves. theme.js adds .is-live to switch to
   one-at-a-time only when motion is welcome. */
.quote-rotator {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  padding: var(--space-4);
  border: var(--hairline) solid var(--color-border);
}
/* One plate in both themes: the stone tablet, veiled by the
   theme-toned scrim above it. */
.quote-rotator::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../img/quote-plate.webp") center / cover no-repeat;
}
.quote-rotator::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--quote-plate-scrim);
}
.quote-rotator > * { position: relative; z-index: 1; }
.quote-eyebrow {
  font: 600 var(--text-caption)/1 var(--font-display);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-3);
}
.quote-items { display: grid; gap: var(--space-4); }
.quote-item blockquote {
  margin: 0;
  font: italic 500 var(--text-body-lg)/var(--leading-subhead) var(--font-body);
  color: var(--color-text-primary);
}
.quote-answer {
  margin-top: var(--space-2);
  font-size: var(--text-ui-sm);
  line-height: var(--leading-body);
  color: var(--color-text-secondary);
}

/* Live mode: every item stacks in one grid cell, so the plate is
   as tall as its longest situation and never resizes mid-cycle.
   The cross-fade runs in two halves: the outgoing item fades out
   over --duration-fade, then the incoming one fades in over the
   same span, which is why .is-current carries the matching delay.

   The off-screen items are faded, NOT hidden: no display:none and
   no visibility:hidden. There are no controls on this panel, so
   hiding them would leave a screen reader with one situation out
   of five and no way to reach the rest. At opacity 0 every one of
   them stays in the accessibility tree, which is the same complete
   list the no-JS and reduced-motion visitor sees. pointer-events
   and user-select keep them from catching a mouse. */
.quote-rotator.is-live .quote-item {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;
  user-select: none;
  transition: opacity var(--duration-fade) ease;
}
.quote-rotator.is-live .quote-item.is-current {
  opacity: 1;
  pointer-events: auto;
  user-select: auto;
  transition: opacity var(--duration-fade) ease var(--duration-fade);
}
.field.is-success input,
.field.is-success select,
.field.is-success textarea { border-color: var(--color-success); }

/* ---- Prose: long-form pages ---- */
.prose { max-width: var(--measure-body); }
.prose h2 {
  font-size: var(--text-display-m);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-heading);
  margin: var(--space-6) 0 var(--space-3);
}
.prose h3 {
  font-size: var(--text-title);
  font-weight: 600;
  line-height: var(--leading-subhead);
  margin: var(--space-5) 0 var(--space-3);
}
.prose p { margin: var(--space-3) 0; }
.prose ul,
.prose ol {
  margin: var(--space-3) 0;
  padding-left: var(--space-4);
}
.prose li { margin-bottom: var(--space-2); }
.prose blockquote {
  margin: var(--space-5) 0;
  padding-left: var(--space-4);
  border-left: var(--accent-rule) solid var(--color-accent);
  font-style: italic;
  color: var(--color-text-secondary);
}
.prose blockquote p { padding-bottom: var(--space-1); }
.table-scroll {
  overflow-x: auto;
  margin: var(--space-4) 0;
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
}
.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-ui);
}
.prose th {
  text-align: left;
  font: 600 var(--text-ui-sm)/var(--leading-ui) var(--font-display);
  padding: var(--space-3);
  border-bottom: var(--hairline) solid var(--color-border-strong);
  background: color-mix(in srgb, var(--color-accent) 6%, transparent);
}
.prose td {
  padding: var(--space-3);
  border-bottom: var(--hairline) solid var(--color-border);
  vertical-align: top;
}
.prose tr:last-child td { border-bottom: none; }
.prose code,
.prose kbd {
  font: 400 var(--text-ui-sm)/var(--leading-ui) var(--font-mono);
  border-radius: calc(var(--radius) / 2);
  padding: var(--space-1) var(--space-2);
}
.prose code {
  background: color-mix(in srgb, var(--color-text-primary) 7%, transparent);
}
.prose kbd {
  border: var(--hairline) solid var(--color-border-strong);
  background: var(--color-bg-raised);
}

/* ---- Section rhythm and the grove band ---- */
.section { padding-block: var(--space-8); }
.section-title {
  font-size: var(--text-section);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-heading);
}
.section-lede {
  margin-top: var(--space-2);
  color: var(--color-text-secondary);
  max-width: var(--measure-lede);
}

/* Page-level title and intro patterns (canonical for every page's
   single h1, including the landing hero and project pages). */
.page-title {
  font-size: var(--text-h1);
  font-weight: 600;
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-display);
}
.badge + .page-title { margin-top: var(--space-3); }
.page-intro { padding-block: var(--space-7) var(--space-8); }
.section .cards { margin-top: var(--space-5); }

/* ---- Ambient media plates ----
   A full-width framed still between a page's intro and its first
   band, optionally carrying a quiet video loop above it. The still
   is the ground truth: videos ship with the hidden attribute and
   preload="none", and theme.js reveals and plays them only when
   the visitor's system allows motion. Reduced-motion and no-JS
   visitors get the photograph and never download a frame. */
/* ---- Page banner (.page-banner) ----
   A full-bleed photographic slide that carries a page opening: the
   title, its lede and an aside all sit ON the photograph rather than
   beside it. Full-bleed like .band, so it holds its own .wrap and the
   positioned content sits above the scrim.
   The photograph is the SAME in both themes, exactly like the scrims
   in tokens.css, which is why every colour inside comes from the
   on-photo tokens rather than the theme ones. Headings must set their
   colour explicitly: the global h1-h4 rule beats anything inherited,
   which is the same trap .feature-panel fell into in revision 5.
   The image itself belongs to a modifier, so the component stays
   reusable for another page's opening. */
.page-banner {
  position: relative;
  overflow: hidden;
  background-color: var(--color-photo-ground);
  background-size: cover;
  background-position: center;
  color: var(--color-on-photo);
}
.page-banner::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--photo-scrim-left);
  pointer-events: none;
}
.page-banner > .wrap { position: relative; z-index: 2; }
/* A banner may carry an ambient loop above its still. The still is
   the CSS background on the modifier, so with no JS or under reduced
   motion the slide is simply the photograph; theme.js reveals and
   plays the video, which then sits under the scrim like the still. */
.page-banner-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.page-banner-video[hidden] { display: none; }
.page-banner .page-title,
.page-banner h2,
.page-banner h3 { color: var(--color-on-photo); }
.page-banner .section-lede { color: var(--color-on-photo); }
/* Inline links in the lede otherwise take the THEME accent, which is
   a dark green on a dark photograph in light mode. On-photo type gets
   an on-photo link: same colour as the text, underline does the work. */
.page-banner .link { color: var(--color-on-photo); }
.page-banner .link:hover { color: var(--color-on-photo-muted); }
/* The landing banner carries the two calls to action. The primary
   button is accent-on-accent and reads fine on the scrim; the secondary
   one is outlined in THEME colours and would vanish on the photograph
   in light mode, so it borrows the on-photo set. */
.page-banner .btn-secondary {
  color: var(--color-on-photo);
  border-color: var(--photo-panel-border);
}
.page-banner .btn-secondary:hover:not(:disabled) {
  color: var(--color-on-photo);
  border-color: var(--color-on-photo);
}
/* The photograph is busiest exactly where the aside sits, so the aside
   carries its own ground instead of trusting the scrim to protect it. */
.page-banner .aside-panel {
  background: var(--photo-panel-ground);
  border-color: var(--photo-panel-border);
}
/* Both of these otherwise resolve to THEME colours, which on a fixed
   dark photograph means the list turns near-invisible in light mode
   and only light mode. Every colour inside the banner has to come
   from the on-photo set for exactly this reason. */
.page-banner .aside-panel li { color: var(--color-on-photo-muted); }
.page-banner .aside-panel li + li { border-top-color: var(--photo-panel-border); }
.page-banner-projects {
  background-image: url("../img/projects-banner.jpg");
  background-image: image-set(url("../img/projects-banner.webp") type("image/webp"),
                              url("../img/projects-banner.jpg") type("image/jpeg"));
}
.page-banner-competence {
  background-image: url("../img/competence-header.jpg");
  background-image: image-set(url("../img/competence-header.webp") type("image/webp"),
                              url("../img/competence-header.jpg") type("image/jpeg"));
}
.page-banner-services {
  background-image: url("../img/poster-project.jpg");
  background-image: image-set(url("../img/poster-project.webp") type("image/webp"),
                              url("../img/poster-project.jpg") type("image/jpeg"));
}
.page-banner-about {
  background-image: url("../img/about.jpg");
  background-image: image-set(url("../img/about.webp") type("image/webp"),
                              url("../img/about.jpg") type("image/jpeg"));
}
/* The landing still is the loop's own poster, so the frame the video
   fades in over is the one already on screen. JPEG only: no WebP was
   cut for the poster. */
.page-banner-home { background-image: url("../img/poster-hero.jpg"); }
.page-banner-contact {
  background-image: url("../img/poster-contact.jpg");
  background-image: image-set(url("../img/poster-contact.webp") type("image/webp"),
                              url("../img/poster-contact.jpg") type("image/jpeg"));
}

.page-media { padding-block: 0 var(--space-8); }
.media-frame {
  position: relative;
  aspect-ratio: 5 / 2;
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-soft);
}
.media-frame-tall { aspect-ratio: 2 / 1; }
.media-frame img,
.media-frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.media-frame video[hidden] { display: none; }
@media (max-width: 720px) {
  .media-frame,
  .media-frame-tall { aspect-ratio: 16 / 10; }
}

/* CTA helpers: a wrapping button row after a lede, and a CTA block
   separated from preceding prose or card grids. Both sit outside
   .prose so .prose a link styling cannot restyle buttons. */
.cta-row {
  margin-top: var(--space-5);
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.section-cta { margin-top: var(--space-6); }

/* ---- Split section: body left, aside right (7/5) ---- */
.section-split {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-5);
}
.aside-panel {
  background: var(--color-bg-raised);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
}
.aside-panel h3 {
  font-size: var(--text-title-sm);
  font-weight: 600;
  margin-bottom: var(--space-3);
}
.aside-panel ul,
.aside-panel ol {
  list-style: none;
  margin: 0;
  padding: 0;
}
.aside-panel li {
  padding-block: var(--space-2);
  font-size: var(--text-ui);
  color: var(--color-text-secondary);
}
.aside-panel li + li { border-top: var(--hairline) solid var(--color-border); }
.aside-panel li strong { color: var(--color-text-primary); }

/* ---- Steps: numbered process rail (ol.steps) ---- */
.steps {
  list-style: none;
  counter-reset: step;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-5);
  margin: var(--space-5) 0 0;
  padding: 0;
}
.steps li {
  counter-increment: step;
  border-top: var(--rule-weight) solid var(--color-border);
  padding-top: var(--space-3);
}
.steps li::before {
  content: counter(step);
  display: block;
  font: 600 var(--text-title)/1 var(--font-display);
  color: var(--color-accent);
  margin-bottom: var(--space-2);
}
.steps h3 {
  font-size: var(--text-title-sm);
  font-weight: 600;
  margin-bottom: var(--space-2);
}
.steps p {
  font-size: var(--text-ui);
  color: var(--color-text-secondary);
}

/* ---- Problems: problem-and-answer pairs ----
   The problem line speaks in the customer's voice against an
   accent hairline; the answer follows in body color. Even items
   drop by one spacing step for asymmetric rhythm, so the grid
   never reads as identical cards. */
.problems {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6);
  margin-top: var(--space-5);
}
.problem {
  border-left: var(--accent-rule) solid var(--color-accent);
  padding-left: var(--space-4);
}
.problem:nth-child(even) { margin-top: var(--space-6); }
.problem-lead {
  font: 600 var(--text-title)/var(--leading-subhead) var(--font-display);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-primary);
}
.problem p {
  margin-top: var(--space-2);
  color: var(--color-text-secondary);
}

/* ---- Feature grid: definition-style term + description ---- */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-6);
  margin: var(--space-5) 0 0;
}
.feature-grid dt {
  font: 600 var(--text-ui)/var(--leading-ui) var(--font-display);
  color: var(--color-text-primary);
}
.feature-grid dd {
  margin: var(--space-1) 0 0;
  font-size: var(--text-ui);
  color: var(--color-text-secondary);
}

/* ---- Notes page: jump pills, article entries, meta chips ---- */
.toc-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}
.toc-pills a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  font: 500 var(--text-ui-sm)/1 var(--font-display);
  color: var(--color-text-primary);
  text-decoration: none;
  transition: border-color var(--duration-base) ease, color var(--duration-base) ease;
}
.toc-pills a:hover { border-color: var(--color-accent); color: var(--color-accent); }

.article {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-3) var(--space-6);
  padding-block: var(--space-5);
  scroll-margin-top: var(--space-8);
}
.article + .article { border-top: var(--hairline) solid var(--color-border); }
.article:nth-of-type(even) { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
.article h3 {
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
}
.article-body p { color: var(--color-text-secondary); }
.article-body p + p { margin-top: var(--space-2); }

.meta-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
}
.meta-chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--badge-height);
  padding: 0 var(--space-2);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border);
  background: color-mix(in srgb, var(--color-text-primary) 4%, transparent);
  font: 500 var(--text-caption)/1 var(--font-display);
  color: var(--color-text-secondary);
}

/* Raised ground with the approved asymmetric curved transition.
   ::before lays a whisper of stone grain over the surface color;
   the tile is a mirrored 2x2 cut, so it repeats seamlessly, and
   the texture swap follows the same two-state theme resolution as
   the tokens. Content sits above it via the positioned .wrap. */
.band {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-raised);
  border-top: var(--hairline) solid var(--color-border);
  border-radius: var(--band-curve);
  padding-block: var(--space-8);
}
/* On pages that CLOSE on a band, the footer shares this grain so
   band and footer read as ONE surface (owner decision, revision 8):
   same ground, same texture, no rule and no gap between them. Pages
   that close on the page ground get an untextured footer on that
   same ground, so the junction is invisible either way. */
.band::before,
main:has(> .band:last-child) + .site-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../img/texture-light.webp") repeat;
  background-size: 560px;
  opacity: 0.4;
  pointer-events: none;
}
:root[data-theme="dark"] .band::before,
:root[data-theme="dark"] main:has(> .band:last-child) + .site-footer::before {
  background-image: url("../img/texture-dark.webp");
}
.band > .wrap { position: relative; }
.band .section { padding-block: 0; }
.band .section + .section { margin-top: var(--space-8); }

/* ---- Reveal on scroll, FAIL-OPEN ----
   Base state is fully visible: with no JS, without
   IntersectionObserver, or under reduced motion the page renders
   complete and static. theme.js only ADDS the transient .anim
   class; the keyframes exist only when motion is welcome and end
   at the element's natural visible state. */
@media (prefers-reduced-motion: no-preference) {
  .reveal.anim {
    animation: reveal-in var(--duration-slow) var(--ease-organic) both;
    animation-delay: var(--d, 0s);
  }
  @keyframes reveal-in {
    from { opacity: 0; transform: translateY(var(--reveal-shift)); }
    to { opacity: 1; transform: none; }
  }
}

/* ---- Footer ----
   The footer is CONTINUOUS with the page above it (owner decision,
   revision 8): no gold rule and no gap, so the closing band flows
   straight into the footer as one unbroken surface. The band's own
   bottom padding supplies the breathing room the removed margin
   used to add. Do not reintroduce a border or a margin here. */
.site-footer {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-base);
  padding: var(--space-4) 0; /* rev 8: shorter footer */
}
/* A page that ends on a band hands the footer that band's surface. */
main:has(> .band:last-child) + .site-footer {
  background: var(--color-bg-raised);
}
.site-footer > .wrap { position: relative; }
/* Everything in the footer sits on ONE baseline: the standing line
   the copyright sets. The row aligns on baselines rather than on
   centres, so the strapline and "(c) 2026" beside it, both pushed
   to the right (rev 8), read as a single line of type, with the
   mark rising above it. */
.site-footer-inner {
  display: flex;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  flex-wrap: wrap;
}
.site-footer p {
  font: 400 var(--text-ui)/var(--leading-ui) var(--font-body);
  color: var(--color-text-secondary);
  margin: 0 0 0 auto; /* strapline sits immediately left of the copyright */
}
/* The copyright line ends in the full mark: the logo IS the
   company name here, so it carries the alt text and no wordmark
   is repeated beside it. Baseline alignment sits the mark's foot
   on the copyright line and lets it rise above, rather than
   centring it across the line. */
.site-footer-copy {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font: 500 var(--text-ui)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.site-footer-mark {
  height: calc((var(--space-7) + 10px) * 0.75); /* rev 8: +10px; rev 9: reduced by 25% */
  width: auto;
}

/* ---- Utility ---- */
.visually-hidden {
  position: absolute;
  width: var(--space-1);
  height: var(--space-1);
  margin: calc(-1 * var(--space-1));
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Responsive: tablet collapse ---- */
@media (max-width: 880px) {
  .section { padding-block: var(--space-7); }
  .band { padding-block: var(--space-7); }
  .band .section + .section { margin-top: var(--space-7); }
  .hero {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }
  .hero-art {
    order: -1;
    max-width: calc(var(--measure-field) / 2);
  }
  .cards { grid-template-columns: 1fr; }
  .card-featured,
  .card-compact,
  .card-half { grid-column: auto; }
  .section-split,
  .steps,
  .problems,
  .rail-item,
  .contact-panel,
  .contact-form-row,
  .feature-grid { grid-template-columns: 1fr; }

  /* Project rows stack: photograph on top, writing beneath, and
     the alternating placement resets so every entry reads the
     same way down a single column. */
  .project-row { grid-template-columns: 1fr; }
  .project-row-media,
  .project-rows-lead .project-row-media {
    min-height: 0;
    aspect-ratio: 16 / 9;
    border-right: 0;
    border-bottom: var(--hairline) solid var(--color-border);
  }
  .project-row:nth-child(even) > .project-row-media {
    grid-column: 1;
    grid-row: 1;
    border-left: 0;
    border-bottom: var(--hairline) solid var(--color-border);
  }
  .project-row:nth-child(even) > .project-row-body {
    grid-column: 1;
    grid-row: 2;
  }
  .feature-panel-body { padding: var(--space-5); }
  .site-header-inner { gap: var(--space-3); }
  .steps { gap: var(--space-4); }
  .problems { gap: var(--space-5); }
  .problem:nth-child(even) { margin-top: 0; }
  .article,
  .article:nth-of-type(even) { grid-template-columns: 1fr; }

  /* With JS present, the nav list collapses behind the toggle.
     Without JS the root never gets the .js class, the toggle stays
     hidden, and the list simply wraps: degraded but usable. */
  .js .nav-toggle:not([hidden]) { display: inline-flex; margin-left: auto; }
  .js .site-nav {
    order: 10;
    flex-basis: 100%;
    margin-left: 0;
  }
  .js .site-nav ul {
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-block: var(--space-2) var(--space-3);
  }
  .js .site-nav ul.is-open { display: flex; }
  .js .site-nav a {
    border-bottom: none;
    padding: var(--space-2) var(--space-1);
  }
  .js .site-nav a[aria-current="page"] {
    border-left: var(--rule-weight) solid var(--color-accent);
    padding-left: var(--space-3);
  }
}

/* ---- Responsive: mobile collapse ---- */
@media (max-width: 560px) {
  .site-header-inner { gap: var(--space-3); }
  /* The lockup never shrinks, so on the narrowest screens it steps
     down a size rather than pushing the controls onto a new row. */
  .site-logo img { height: var(--space-4); }
  .site-nav ul { gap: var(--space-3); }
  .site-footer-inner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }
  .site-footer-copy { margin-left: 0; }
  /* The row is a column here, so the auto margin that pulls the
     strapline against the copyright must not push it to the far edge. */
  .site-footer p { margin-left: 0; }
  .contact-panel { gap: var(--space-5); }
  /* Give the hosted form the width the card padding was taking: an
     iframe cannot reflow the way the native form's own fields did. */
  .contact-panel-main { padding: var(--space-3); }
  /* Narrower column, so the questions wrap and the form grows taller. */
  .form-embed iframe { height: 1420px; }
}
