/* ==========================================================================
   Components. One block per thing on the page, in roughly the order you meet
   them: surfaces → nav → buttons → hero → sections → cards → forms → loader.
   ========================================================================== */

/* --- Surfaces ------------------------------------------------------------- */

/* The DOM echo of the MeshPhysicalMaterial used in the 3D scene. */
.glass {
  background: linear-gradient(
    160deg,
    rgb(var(--vinyl) / 0.07) 0%,
    rgb(var(--vinyl) / 0.025) 55%,
    rgb(var(--ink-2) / 0.72) 100%
  );
  border: 1px solid rgb(var(--vinyl) / 0.1);
  border-radius: var(--radius);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
}

/* Text over the live canvas must never depend on frame luminance for its
   contrast. This backstop holds >= 4.5:1 even against the white lens flare. */
.scrim {
  position: relative;
  isolation: isolate;
}

.scrim::before {
  content: '';
  position: absolute;
  /* The horizontal bleed is what pushed the document 4-24px wider than a
     phone viewport. It exists to soften the halo's edge, and on a narrow
     screen there is no room for it to soften into. */
  inset: -2rem 0;
  z-index: -1;
  border-radius: 2rem;
  pointer-events: none;
  background: radial-gradient(
    120% 100% at 50% 50%,
    rgb(var(--ink) / 0.95) 0%,
    rgb(var(--ink) / 0.88) 45%,
    rgb(var(--ink) / 0) 100%
  );
}

@media (min-width: 900px) {
  .scrim::before { inset: -2rem -1.5rem; }
}

.hairline {
  height: 1px;
  background: linear-gradient(
    90deg,
    rgb(var(--ink-3)) 0%,
    rgb(var(--signal) / 0.4) 30%,
    rgb(var(--ink-3)) 100%
  );
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(var(--nova));
}

.label {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(var(--text-3));
}

.muted { color: rgb(var(--text-2)); }
.dim { color: rgb(var(--text-3)); }
.mint { color: rgb(var(--nova)); }
.coral { color: rgb(var(--signal)); }

/* --- Skip link ------------------------------------------------------------ */

.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  z-index: 200;
}

.skip-link:focus {
  position: fixed;
  left: var(--space-4);
  top: var(--space-4);
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-pill);
  background: rgb(var(--nova));
  color: rgb(var(--ink));
  font-size: var(--size-sm);
  font-weight: 500;
}

/* --- Nav ------------------------------------------------------------------ */

.nav {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: var(--z-nav);
  transition: background-color var(--dur-base) var(--ease-enter),
    border-color var(--dur-base) var(--ease-enter);
  border-bottom: 1px solid transparent;
}

.nav[data-lifted='true'] {
  background: rgb(var(--ink) / 0.82);
  border-bottom-color: rgb(var(--ink-3));
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  height: 72px;
}

.nav__brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.nav__links {
  display: none;
  align-items: center;
  gap: var(--space-1);
}

@media (min-width: 900px) {
  .nav__links { display: flex; }
}

.nav__link {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  font-size: var(--size-sm);
  color: rgb(var(--text-2));
  transition: color var(--dur-micro) var(--ease-enter);
}

.nav__link:hover { color: rgb(var(--vinyl)); }

.nav__link[aria-current='page'] {
  color: rgb(var(--nova));
}

.nav__link[aria-current='page']::after {
  content: '';
  position: absolute;
  inset-inline: var(--space-4);
  bottom: var(--space-2);
  height: 1px;
  background: rgb(var(--nova));
}

.nav__toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -0.5rem;
  border-radius: var(--radius-pill);
  color: rgb(var(--vinyl));
}

/* Declared after .nav__toggle so it actually wins — a media block placed
   above the base rule loses on source order, which is how the hamburger
   ended up visible on desktop. */
@media (min-width: 900px) {
  .nav__toggle { display: none; }
}

.nav__bars {
  position: relative;
  display: block;
  width: 20px;
  height: 14px;
}

.nav__bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transition: transform var(--dur-base) var(--ease-enter),
    opacity var(--dur-micro) linear, top var(--dur-base) var(--ease-enter);
}

.nav__bars span:nth-child(1) { top: 0; }
.nav__bars span:nth-child(2) { top: 6px; }
.nav__bars span:nth-child(3) { top: 12px; }

.nav__toggle[aria-expanded='true'] .nav__bars span:nth-child(1) { top: 6px; transform: rotate(45deg); }
.nav__toggle[aria-expanded='true'] .nav__bars span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded='true'] .nav__bars span:nth-child(3) { top: 6px; transform: rotate(-45deg); }

.nav__panel {
  border-top: 1px solid rgb(var(--ink-3));
  background: rgb(var(--ink) / 0.96);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

@media (min-width: 900px) {
  .nav__panel { display: none; }
}

.nav__panel ul {
  display: flex;
  flex-direction: column;
  padding-block: var(--space-3);
}

.nav__panel a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 52px;
  border-bottom: 1px solid rgb(var(--ink-3));
  font-size: var(--size-body);
}

.nav__panel a[aria-current='page'] { color: rgb(var(--nova)); }

.nav__panel li:last-child { padding-top: var(--space-4); }
.nav__panel li:last-child a { border-bottom: 0; }

/* --- Buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 52px;
  padding-inline: var(--space-6);
  border-radius: var(--radius-pill);
  font-size: var(--size-body);
  font-weight: 500;
  transition: transform var(--dur-micro) var(--ease-enter),
    border-color var(--dur-micro) var(--ease-enter),
    color var(--dur-micro) var(--ease-enter);
}

.btn--sm {
  min-height: 44px;
  padding-inline: var(--space-5);
  font-size: var(--size-sm);
}

/* Mint is the only interactive colour, so there is exactly one filled button
   style and it marks the single primary action on a screen. */
.btn--primary {
  background: rgb(var(--nova));
  color: rgb(var(--ink));
}

.btn--primary:hover { transform: scale(1.03); }
.btn--primary:active { transform: scale(0.98); }

.btn--ghost {
  border: 1px solid rgb(var(--vinyl) / 0.18);
  color: rgb(var(--vinyl));
}

.btn--ghost:hover {
  border-color: rgb(var(--nova));
  color: rgb(var(--nova));
}

.btn--disabled,
.btn[aria-disabled='true'] {
  border: 1px solid rgb(var(--ink-3));
  color: rgb(var(--text-3));
  opacity: 0.6;
  cursor: not-allowed;
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  font-size: var(--size-sm);
  color: rgb(var(--nova));
  transition: opacity var(--dur-micro) var(--ease-enter);
}

.link-arrow:hover { opacity: 0.8; }

/* --- Hero: the answer block ----------------------------------------------
   Unova sells findability, so the page presents itself as a search result.
   The ANSWER label is structural, not decorative: it states that the block
   below is the response to the query above. */

.hero {
  position: relative;
  display: flex;
  align-items: center;
  /* `safe` falls back to flex-start when the content is taller than the box.
     Plain `center` pushes the overflow ABOVE the viewport, where it can't be
     scrolled to — which is exactly what happened in landscape. */
  align-items: safe center;
  min-height: 100dvh;
}

/* Short viewports (landscape phones): stop forcing a full-height hero and
   drop the scroll cue, which would otherwise sit on top of the buttons. */
@media (max-height: 620px) and (orientation: landscape) {
  .hero {
    min-height: auto;
    padding-bottom: var(--space-12);
  }
  }

.answer {
  width: 100%;
  max-width: 56rem;
}

.answer__source {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-8);
  padding-top: var(--space-5);
  border-top: 1px solid rgb(var(--ink-3));
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(var(--text-3));
}

@keyframes drift {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50% { transform: translate3d(0, -0.5rem, 0); }
}

/* --- Cards ---------------------------------------------------------------- */

.card {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: var(--space-6);
  transition: transform var(--dur-base) var(--ease-enter),
    border-color var(--dur-base) var(--ease-enter);
}

.card:hover,
.card:focus-visible {
  transform: translateY(-6px);
  border-color: rgb(var(--nova) / 0.4);
}

.card__query {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  line-height: 1.6;
  color: rgb(var(--text-3));
}

.card__title {
  margin-top: var(--space-5);
}

.card__body {
  flex: 1;
  margin-top: var(--space-3);
  font-size: var(--size-sm);
  line-height: 1.65;
  color: rgb(var(--text-2));
}

.card__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-6);
  font-size: var(--size-sm);
  color: rgb(var(--nova));
}

.card:hover .card__more .arrow {
  transform: translateX(4px);
}

.arrow {
  display: inline-block;
  transition: transform var(--dur-base) var(--ease-enter);
}

/* Chips: tiers, flavours, stack badges. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.chip {
  padding: 0.375rem 0.75rem;
  border: 1px solid rgb(var(--vinyl) / 0.12);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgb(var(--text-2));
}

.chip--wide {
  padding: 0.5rem 1rem;
  font-family: var(--font-body);
  font-size: var(--size-sm);
  letter-spacing: normal;
  text-transform: none;
  border-color: rgb(var(--signal) / 0.32);
  color: rgb(var(--vinyl));
}

/* --- Metrics -------------------------------------------------------------- */

.metric__value {
  font-family: var(--font-display);
  font-weight: 700;
  color: rgb(var(--nova));
}

.metric__label {
  margin-top: var(--space-2);
  font-size: var(--size-xs);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgb(var(--text-3));
}

/* --- Tiers ---------------------------------------------------------------- */

.tier {
  padding: var(--space-6);
}

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

.tier__points {
  margin-top: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

.tier__points li {
  display: flex;
  gap: var(--space-3);
  font-size: var(--size-sm);
  line-height: 1.65;
}

.tier__points li::before {
  content: '';
  flex-shrink: 0;
  width: 4px;
  height: 4px;
  margin-top: 0.62em;
  border-radius: 50%;
  background: rgb(var(--nova));
}

/* --- Process: pinned horizontal act on desktop, plain list on mobile ------ */

.process {
  position: relative;
  overflow: hidden;
  padding-block: var(--space-section);
}

.process__track {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-14);
  padding-inline: var(--gutter);
  will-change: transform;
}

@media (min-width: 1100px) {
  .process__track {
    flex-direction: row;
    gap: var(--space-6);
    width: max-content;
  }
  .process__step {
    flex-shrink: 0;
    width: min(30rem, 78vw);
  }
}

.process__step {
  padding: var(--space-8);
}

.process__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

/* --- Project card --------------------------------------------------------
   The hover distortion is an SVG filter, NOT a second WebGL canvas: browsers
   cap live contexts at roughly 16 per page and the persistent scene already
   owns ours. A six-card grid of canvases would silently evict it. */

.project {
  position: relative;
}

.project__frame {
  position: relative;
  overflow: hidden;
  transition: transform var(--dur-base) var(--ease-enter),
    border-color var(--dur-base) var(--ease-enter);
}

.project:hover .project__frame,
.project:focus-within .project__frame {
  transform: translateY(-6px);
  border-color: rgb(var(--nova) / 0.4);
}

/* Only the visual plate is filtered — never the text, which stays pin-sharp. */
.project__plate {
  position: relative;
  height: 11rem;
  overflow: hidden;
  border-bottom: 1px solid rgb(var(--vinyl) / 0.08);
}

@media (min-width: 640px) {
  .project__plate { height: 13rem; }
}

.project__wash,
.project__grid {
  position: absolute;
  inset: 0;
}

.project__grid {
  opacity: 0.35;
  background-image: linear-gradient(rgb(243 235 220 / 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgb(243 235 220 / 0.1) 1px, transparent 1px);
  background-size: 38px 38px;
}

.project__caption {
  position: absolute;
  z-index: 2;
  inset: auto 0 0 0;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-10) var(--space-5) var(--space-4);
  /* A scrim rather than a bar: the live site fades into it, so the caption
     reads as part of the card instead of a strip pasted over someone else's
     design. */
  background: linear-gradient(to top, rgb(4 6 13 / 0.92) 0%, rgb(4 6 13 / 0.72) 42%, rgb(4 6 13 / 0) 100%);
}

.project__name {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.2vw, 1.75rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.03em;
  /* Fixed to the light casing colour: the scrim behind it is always dark,
     whichever surface the card itself sits on. */
  color: rgb(243 235 220 / 0.95);
}

.project__year {
  flex-shrink: 0;
  color: rgb(243 235 220 / 0.6);
}

/* --- Live preview ---------------------------------------------------------
   The real deployed site, rendered at desktop width and scaled into the plate.
   It sits ON TOP of the poster, which is why a site that refuses to be framed
   degrades to something designed rather than to a white rectangle. */

.live__stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  transition: opacity var(--dur-scene) var(--ease-enter);
}

[data-live-state='ready'] .live__stage { opacity: 1; }

.live__frame {
  display: block;
  border: 0;
  transform: scale(var(--live-scale, 0.3));
  transform-origin: 0 0;
  /* Inert on purpose: an embedded third-party page must never be able to
     capture a scroll, a tap or a keyboard focus from the page hosting it. */
  pointer-events: none;
}

.live__badge {
  position: absolute;
  z-index: 2;
  left: var(--space-4);
  top: var(--space-4);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-pill);
  background: rgb(4 6 13 / 0.62);
  backdrop-filter: blur(8px);
  color: rgb(243 235 220 / 0.9);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-enter);
}

.live__badge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgb(var(--pulse));
  box-shadow: 0 0 8px rgb(var(--pulse));
}

[data-live-state='ready'] .live__badge { opacity: 1; }

/* The frame never loaded — almost always the site declining to be embedded.
   Say so in the corner instead of pretending the poster is a preview. */
[data-live-state='manual'] .live__badge {
  opacity: 1;
  background: rgb(4 6 13 / 0.62);
}

[data-live-state='manual'] .live__badge::before {
  background: rgb(var(--signal));
  box-shadow: none;
}

.project__body {
  padding: var(--space-5);
}

@media (min-width: 640px) {
  .project__body { padding: var(--space-6); }
}

.project__result {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.dot-sep {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: rgb(var(--ink-3));
}

/* --- Forms ---------------------------------------------------------------- */

.field + .field,
.field + .form__actions {
  margin-top: var(--space-6);
}

.field label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--size-sm);
  font-weight: 500;
  color: rgb(var(--vinyl));
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 48px;
  padding: 0.75rem var(--space-4);
  border: 1px solid rgb(var(--vinyl) / 0.14);
  border-radius: 12px;
  background: rgb(var(--ink) / 0.7);
  font-size: var(--size-sm);
  color: rgb(var(--vinyl));
  transition: border-color var(--dur-micro) var(--ease-enter);
}

.field textarea {
  min-height: auto;
  line-height: 1.65;
  resize: vertical;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: rgb(var(--nova));
}

.field__error {
  margin-top: var(--space-2);
  font-size: var(--size-sm);
  color: rgb(var(--signal));
}

.field__help {
  margin-top: var(--space-2);
  font-size: var(--size-xs);
  color: rgb(var(--text-3));
}

.req { color: rgb(var(--signal)); }

fieldset:disabled { opacity: 0.7; }

/* --- Nova ----------------------------------------------------------------- */

.nova {
  display: flex;
  flex-direction: column;
  height: min(70vh, 560px);
  overflow: hidden;
}

.nova__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid rgb(var(--vinyl) / 0.08);
}

.nova__status {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgb(var(--text-3));
}

.nova__status--live {
  background: rgb(var(--nova));
  box-shadow: 0 0 10px rgb(var(--nova));
}

.nova__status--busy { background: rgb(var(--pulse)); }
.nova__status--error { background: rgb(var(--signal)); }

.nova__log {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.nova__msg {
  display: flex;
}

.nova__msg--user { justify-content: flex-end; }

.nova__bubble {
  max-width: 85%;
  padding: 0.75rem var(--space-4);
  border-radius: 1rem;
  font-size: var(--size-sm);
  line-height: 1.65;
  white-space: pre-wrap;
}

.nova__msg--user .nova__bubble {
  background: rgb(var(--vinyl));
  color: rgb(var(--ink));
}

.nova__msg--assistant .nova__bubble {
  border: 1px solid rgb(var(--vinyl) / 0.1);
  background: rgb(var(--ink-2) / 0.8);
}

.nova__form {
  padding: var(--space-4);
  border-top: 1px solid rgb(var(--vinyl) / 0.08);
}

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

.nova__row input {
  flex: 1;
  min-height: 44px;
  padding-inline: var(--space-4);
  border: 1px solid rgb(var(--vinyl) / 0.14);
  border-radius: var(--radius-pill);
  background: rgb(var(--ink) / 0.8);
  font-size: var(--size-sm);
  color: rgb(var(--vinyl));
}

/* --- Progress bar (loader + Nova download) -------------------------------- */

.bar {
  height: 1px;
  width: 100%;
  overflow: hidden;
  background: rgb(var(--ink-3));
}

.bar__fill {
  height: 100%;
  background: rgb(var(--nova));
  box-shadow: 0 0 12px rgb(var(--nova) / 0.9);
  transition: width 120ms linear;
}

/* --- Loader ---------------------------------------------------------------
   The curtain is a SEPARATE element below the canvas (z-curtain < z-canvas-
   loading). Put an opaque backdrop in front of the canvas and Nova's entire
   performance renders where nobody can see it. */

.curtain {
  position: fixed;
  inset: 0;
  z-index: var(--z-curtain);
  background-image: radial-gradient(
    120% 90% at 50% 30%,
    rgb(17 24 46) 0%,
    rgb(11 16 32) 48%,
    rgb(4 6 13) 100%
  );
}

.loader {
  position: fixed;
  inset: 0;
  z-index: var(--z-loader);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  overflow: hidden;
  pointer-events: none;
}

.loader__copy {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--shell);
  padding-inline: var(--gutter);
  padding-bottom: 14vh;
  text-align: center;
  pointer-events: auto;
}

.loader__mark {
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 6vw, 3.75rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
}

.loader__tagline {
  max-width: 34ch;
  margin: var(--space-4) auto 0;
  font-size: var(--size-sm);
  line-height: 1.65;
  color: rgb(var(--text-2));
}

.loader__meter {
  width: 100%;
  max-width: 20rem;
  margin: var(--space-10) auto 0;
}

/* One line of narration, not a percentage. It has to reserve its own height or
   every swap between a one-word and a four-word line nudges the bar. */
.loader__readout {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 1.5rem;
  margin-bottom: var(--space-3);
  text-align: center;
}

.loader__line {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(var(--nova));
}

.loader__skip {
  min-height: 44px;
  margin-top: var(--space-8);
  padding-inline: var(--space-5);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(var(--text-3));
  transition: color var(--dur-micro) var(--ease-enter);
}

.loader__skip:hover { color: rgb(var(--vinyl)); }

/* Starts as a point at Nova's eye, ends as the whole screen. */
.loader__flare {
  position: absolute;
  left: 50%;
  top: 46%;
  width: 10rem;
  height: 10rem;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  /* Was a pure-white core at full opacity, which is genuinely painful in a
     dark room. Now a mint-tinted bloom that never reaches white and never
     reaches full opacity — it still reads as a burst, it just doesn't sting. */
  background: radial-gradient(
    circle,
    rgb(214 248 238 / 0.92) 0%,
    rgb(150 235 214 / 0.8) 30%,
    rgb(79 227 196 / 0.5) 56%,
    rgb(79 227 196 / 0) 74%
  );
}

/* --- Route sweep ---------------------------------------------------------- */

.sweep {
  position: fixed;
  inset: 0;
  z-index: var(--z-transition);
  display: flex;
  flex-direction: column;
  pointer-events: none;
  /* The bars park off-screen at x:+101% when the sweep finishes. Without this
     they widen the document and create a horizontal scrollbar on mobile. */
  overflow: hidden;
}

/* THE RESTING STATE IS INVISIBLE, AND THAT IS THE WHOLE SAFETY ARGUMENT.
   These three bars are the only thing on the site that can cover the entire
   viewport. If anything at all goes wrong — no JS, no animation support, a
   throttled tab, a device at two frames a second — this is what holds, and it
   shows the page. Never give these a default that covers. */
.sweep div {
  width: 100%;
  height: 33.34%;
  opacity: 0;
}

.sweep div:nth-child(1) { background: rgb(12 17 34); }
.sweep div:nth-child(2) { background: rgb(6 9 19); }
.sweep div:nth-child(3) {
  background: linear-gradient(90deg, rgb(12 17 34), rgb(79 227 196 / 0.2), rgb(12 17 34));
}

/* Both halves of the transition run on the compositor rather than through
   GSAP. GSAP's clock stalls on slow devices; this one can't. See the long
   note above initRouteSweep in js/ui/motion.js. */
@media (prefers-reduced-motion: no-preference) {
  /* ARRIVAL — a class in the markup, so it starts at first paint with no
     JavaScript involved and no gap between the two pages' halves. */
  .sweep--enter div {
    animation: sweep-out 420ms cubic-bezier(0.65, 0, 0.35, 1) both;
  }
  .sweep--enter div:nth-child(2) { animation-delay: 55ms; }
  .sweep--enter div:nth-child(3) { animation-delay: 110ms; }

  /* DEPARTURE — added on click, after navigation has already been allowed to
     start. It ends covered on purpose: the page underneath is leaving. */
  .sweep--leave div {
    animation: sweep-in 300ms cubic-bezier(0.65, 0, 0.35, 1) both;
  }
  .sweep--leave div:nth-child(2) { animation-delay: 50ms; }
  .sweep--leave div:nth-child(3) { animation-delay: 100ms; }
}

@keyframes sweep-out {
  from { opacity: 1; transform: translate3d(0, 0, 0); }
  99%  { opacity: 1; transform: translate3d(101%, 0, 0); }
  to   { opacity: 0; transform: translate3d(101%, 0, 0); }
}

@keyframes sweep-in {
  from { opacity: 1; transform: translate3d(-101%, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* --- Breadcrumbs ---------------------------------------------------------- */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(var(--text-3));
}

/* Breadcrumbs stay visually small, but the tap area is padded out to 44px —
   the hit area is allowed to exceed the visual bounds, and on a phone a
   14px-tall link is not a target anyone can reliably hit. */
.crumbs a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  transition: color var(--dur-micro) var(--ease-enter);
}
.crumbs a:hover { color: rgb(var(--nova)); }
.crumbs [aria-current='page'] { color: rgb(var(--vinyl)); }

/* --- Footer --------------------------------------------------------------- */

.footer {
  position: relative;
  z-index: var(--z-content);
  margin-top: var(--space-section);
  border-top: 1px solid rgb(var(--ink-3));
  background: rgb(var(--ink) / 0.72);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

.footer__grid {
  display: grid;
  gap: var(--space-10);
  padding-block: var(--space-14);
}

@media (min-width: 640px) {
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1100px) {
  .footer__grid { grid-template-columns: 2fr 1fr 1fr; }
}

.footer__nav a {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--size-sm);
  color: rgb(var(--text-2));
  transition: color var(--dur-micro) var(--ease-enter);
}

.footer__nav a:hover { color: rgb(var(--nova)); }

.footer__base {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-block: var(--space-6);
  border-top: 1px solid rgb(var(--ink-3));
  font-size: var(--size-xs);
  color: rgb(var(--text-3));
}

@media (min-width: 640px) {
  .footer__base {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

/* --- Nova dock ------------------------------------------------------------
   Rebuilt after the first version came back as "too crowded", and it was: a
   23rem panel held SEVEN bordered rectangles — header bar, message bubble,
   four full-width chips, input, send button, dashed upgrade box — plus a
   chunky native scrollbar with arrow buttons. Every one of them drew an edge,
   so the eye had nowhere to rest and the input, the only thing a visitor
   actually needs, sat below four louder buttons.

   Now it is three zones and two rules: header, conversation, composer. The
   suggestions moved inside the conversation as quiet inline text (they are a
   one-time offer, not a toolbar). The upgrade became a single line instead of
   a dashed panel — progressive disclosure, since it is the least urgent thing
   here and it explains itself the moment you press it. The status label only
   appears when it has something to say.
   ------------------------------------------------------------------------- */

.dock {
  position: fixed;
  right: var(--gutter);
  bottom: var(--gutter);
  z-index: var(--z-dock);
  display: flex;
  /* Reverse, so the launcher stays pinned in the corner and the panel grows
     UPWARD from it. In normal order the panel opens downward and runs straight
     off the bottom of the viewport. */
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: var(--space-3);
}

.dock__launcher {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  min-height: 48px;
  padding: 0 1.125rem 0 0.875rem;
  border: 1px solid rgb(var(--vinyl) / 0.14);
  border-radius: var(--radius-pill);
  background: rgb(15 21 38 / 0.9);
  backdrop-filter: blur(14px);
  box-shadow: 0 12px 40px rgb(4 6 13 / 0.6);
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #F3EBDC;
  transition: transform var(--dur-micro) var(--ease-enter),
              border-color var(--dur-micro) var(--ease-enter);
}

.dock__launcher:hover {
  transform: translateY(-2px);
  border-color: rgb(79 227 196 / 0.5);
}

.dock__ping {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #7CF5B0;
  box-shadow: 0 0 10px #7CF5B0;
  animation: dock-ping 2.4s var(--ease-enter) infinite;
}

@keyframes dock-ping {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* The panel floats over the whole document, not inside a section, so it is
   pinned to the dark palette by literal values — a light band must not invert
   it, and it must stay opaque over a bright bloom in the 3D scene. */
.dock__panel {
  display: flex;
  flex-direction: column;
  width: min(24rem, calc(100vw - var(--gutter) * 2));
  /* Height follows the conversation instead of being fixed. A fixed 32rem
     panel opened onto a two-line greeting and a row of suggestions, leaving a
     third of itself as a blank hole above the input — which read as something
     failing to load. Now it opens compact and grows as you talk, until it hits
     the cap and the log starts scrolling. */
  height: auto;
  max-height: min(32rem, calc(100dvh - 9rem));
  overflow: hidden;
  border: 1px solid rgb(243 235 220 / 0.13);
  border-radius: 20px;
  background: #10162A;
  box-shadow: 0 28px 80px rgb(4 6 13 / 0.72);
  color: #F3EBDC;
}

.dock__panel[hidden] { display: none; }

/* --- Header: three things, and the third is usually absent --------------- */

.dock__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-5);
}

.dock__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #4FE3C4;
  box-shadow: 0 0 10px rgb(79 227 196 / 0.8);
}

.dock__title {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.dock__mode {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  color: rgb(138 149 180 / 0.9);
}

.dock__mode[hidden] { display: none; }

.dock__close {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  margin-left: auto;
  margin-right: -4px;
  border-radius: 50%;
  color: rgb(138 149 180);
  transition: color var(--dur-micro) var(--ease-enter),
              background-color var(--dur-micro) var(--ease-enter);
}

/* When the status IS showing, it takes the auto margin and the close button
   must not also claim one, or they separate instead of sitting together. */
.dock__mode:not([hidden]) ~ .dock__close { margin-left: 0.25rem; }

.dock__close:hover {
  color: #F3EBDC;
  background: rgb(243 235 220 / 0.08);
}

/* --- Conversation -------------------------------------------------------- */

.dock__log {
  /* 1 1 auto, not 1: the log must be free to be short. With flex-basis 0 it
     would always claim the full remaining height, which is the blank hole the
     auto-height panel above is there to avoid. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* The default scrollbar on Windows is a wide grey trough with arrow buttons
     at both ends — a chunk of OS chrome sitting inside a dark glass panel. */
  scrollbar-width: thin;
  scrollbar-color: rgb(243 235 220 / 0.18) transparent;
}

.dock__log::-webkit-scrollbar { width: 6px; }
.dock__log::-webkit-scrollbar-track { background: transparent; }
.dock__log::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgb(243 235 220 / 0.16);
}
.dock__log::-webkit-scrollbar-thumb:hover { background: rgb(243 235 220 / 0.28); }

.dock__log .nova__msg { display: flex; }
.dock__log .nova__msg--user { justify-content: flex-end; }

.dock__log .nova__bubble {
  max-width: 88%;
  padding: 0.7rem 0.95rem;
  border-radius: 14px;
  font-size: 0.9375rem;
  line-height: 1.6;
  white-space: pre-wrap;
}

/* No border on Nova's own messages. A filled bubble and a bordered bubble say
   the same thing twice; the fill alone is enough to separate the two voices. */
.dock__log .nova__msg--assistant .nova__bubble {
  border: 0;
  background: rgb(243 235 220 / 0.06);
  border-bottom-left-radius: 5px;
}

.dock__log .nova__msg--user .nova__bubble {
  background: #4FE3C4;
  color: #090D1A;
  border-bottom-right-radius: 5px;
}

.dock__link {
  display: block;
  margin-top: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: #4FE3C4;
}

.dock__link:hover { text-decoration: underline; }

/* --- Suggestions: an offer, not a toolbar -------------------------------- */

.dock__suggest {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: -0.4rem;
}

.dock__chip {
  padding: 0.4rem 0.7rem;
  border: 1px solid rgb(243 235 220 / 0.14);
  border-radius: var(--radius-pill);
  /* Sentence case in the body face, not uppercase mono. These are questions a
     person might ask, so they should look like language rather than like four
     system buttons. */
  font-size: 0.8125rem;
  line-height: 1.3;
  text-align: left;
  color: rgb(168 176 200);
  transition: border-color var(--dur-micro) var(--ease-enter),
              color var(--dur-micro) var(--ease-enter);
}

.dock__chip:hover {
  border-color: rgb(79 227 196 / 0.55);
  color: #4FE3C4;
}

/* Coarse pointers need a 44px target even though the visual pill is smaller —
   padding the box would make them loud again, so the hit area is extended
   underneath instead. */
@media (pointer: coarse) {
  .dock__chip {
    position: relative;
    margin-block: 4px;
  }
  .dock__chip::after {
    content: '';
    position: absolute;
    inset: -8px -2px;
  }
}

/* --- Composer ------------------------------------------------------------ */

.dock__form {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  padding: var(--space-4) var(--space-4) 0;
  border-top: 1px solid rgb(243 235 220 / 0.08);
  margin-top: auto;
}

.dock__form input {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  padding-inline: var(--space-4);
  border: 1px solid rgb(243 235 220 / 0.14);
  border-radius: var(--radius-pill);
  background: rgb(9 13 26 / 0.7);
  font-size: 0.9375rem;
  color: #F3EBDC;
  transition: border-color var(--dur-micro) var(--ease-enter);
}

.dock__form input::placeholder { color: rgb(138 149 180 / 0.8); }

.dock__form input:focus {
  outline: none;
  border-color: rgb(79 227 196 / 0.6);
}

/* An icon, not the word "Ask". The label was the second-widest element in the
   composer and it repeated what the input already implies. */
.dock__send {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #4FE3C4;
  color: #090D1A;
  transition: transform var(--dur-micro) var(--ease-enter), opacity var(--dur-micro) linear;
}

.dock__send:hover { transform: scale(1.06); }
.dock__send:active { transform: scale(0.95); }
.dock__send:disabled { opacity: 0.45; transform: none; }

/* --- The upgrade: one line ------------------------------------------------ */


@media (max-width: 600px) {
  /* At 335px the pills can't share a row, so four of them become four stacked
     buttons again — the exact shape this redesign removed. Three is the most
     a phone can show without the list becoming the panel. */
  .dock__chip:nth-child(n + 4) { display: none; }

  .dock__panel {
    position: fixed;
    inset: auto var(--gutter) calc(var(--gutter) + 60px) var(--gutter);
    width: auto;
    height: auto;
    max-height: min(28rem, calc(100dvh - 10rem));
  }
}

@media (prefers-reduced-motion: reduce) {
  .dock__ping { animation: none; }
}

/* --- Notice ---------------------------------------------------------------
   A standing correction, not a decoration. Used where the page would otherwise
   let a reader assume something untrue: that the showcase is client work, or
   that a service with no shipped work behind it has a track record. Deliberately
   styled to be *read* rather than skimmed past — coral rule, real contrast, and
   never collapsed into small print. */

.notice {
  max-width: var(--measure);
  margin-top: var(--space-6);
  padding: var(--space-4) var(--space-5);
  border: 1px solid rgb(var(--ink-3));
  border-left: 3px solid rgb(var(--signal));
  border-radius: var(--radius);
  background: rgb(var(--signal) / 0.06);
  /* Not --size-xs. Small print is how a disclaimer stops working. */
  font-size: var(--size-sm);
  line-height: 1.65;
  color: rgb(var(--text-2));
}

.notice strong { color: rgb(var(--text-1)); font-weight: 600; }

/* Per-card, under the button — the framing has to travel with the card if the
   card is ever seen without the page heading above it. */
.project__note {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid rgb(var(--ink-3));
  font-size: var(--size-xs);
  line-height: 1.5;
  color: rgb(var(--text-3));
}

/* --- Typed line -----------------------------------------------------------
   One rotating statement above the headline. Sized as a mono label rather than
   body copy so it reads as a caption to the h1, not a competing sentence — and
   given a fixed min-height so the line breaking mid-rotation never nudges the
   headline down. */

.typed-line {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  min-height: 1.75rem;
  font-family: var(--font-mono);
  font-size: var(--size-sm);
  line-height: 1.4;
  letter-spacing: 0.02em;
  color: rgb(var(--text-2));
}

.typed-line__caret {
  display: inline-block;
  width: 0.55em;
  height: 1.05em;
  background: rgb(var(--nova));
  animation: caret-blink 1.05s steps(1) infinite;
}

@keyframes caret-blink {
  0%, 55% { opacity: 1; }
  56%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .typed-line__caret { animation: none; opacity: 0; }
}

/* Marks an answer that came from the general lane rather than from Unova's own
   pages. Quiet, but never hidden — the whole safety argument for letting Nova
   answer off-topic questions is that a visitor can tell the difference. */
.dock__disclaimer {
  display: block;
  margin-top: 0.55rem;
  padding-top: 0.5rem;
  border-top: 1px solid rgb(243 235 220 / 0.1);
  font-size: 0.6875rem;
  line-height: 1.45;
  color: rgb(138 149 180);
}
