/* ==========================================================================

   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);

  -webkit-backdrop-filter: blur(14px) saturate(120%);

  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));

  -webkit-backdrop-filter: blur(18px);

  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;
}

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

.logo-mark {
  display: block;
  width: 2rem;
  height: 2rem;
  flex: 0 0 auto;
  overflow: visible;
  object-fit: contain;
}

.logo-mark .lp,
.logo-mark .lnode,
.logo-mark .globe {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.logo-mark .lp { stroke-width: 2.2; }
.logo-mark .lnode { fill: currentColor; stroke: none; }
.logo-mark .globe { opacity: 0.82; }

.nav__brand b,
.footer__brand b,
.loader__mark b { color: rgb(var(--nova)); }

.footer__brand {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.loader__logo {
  width: clamp(4rem, 10vw, 5.5rem);
  height: clamp(4rem, 10vw, 5.5rem);
  margin-inline: auto;
  margin-bottom: var(--space-4);
}



/* The nav logo is shared by every page. Keep its box fixed so a missing or
  late-loading image cannot move the brand text; the 32px size also gives the
  cream mark enough contrast to remain recognizable on small screens. If the
  logo looks blank, inspect this rule first, then check the image src in HTML. */
.nav__brand img {

  display: block;

  width: 2rem;

  height: 2rem;

  flex-shrink: 0;

  border-radius: 0.5rem;

}



.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);

  -webkit-backdrop-filter: blur(18px);

  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; }



/* --- Tabs (showcase subpages) --------------------------------------------- */



.tabs {

  display: flex;

  flex-wrap: wrap;

  gap: var(--space-1);

  border-bottom: 1px solid rgb(var(--ink-3));

}



/* 44px tall for touch; the active one is a span, not a link, so it can't be

   clicked to reload the page it already is. */

.tab {

  display: inline-flex;

  align-items: center;

  min-height: 44px;

  padding-inline: var(--space-4);

  font-size: var(--size-sm);

  font-weight: 500;

  color: rgb(var(--text-2));

  border-bottom: 2px solid transparent;

  margin-bottom: -1px;

  transition: color var(--dur-micro) var(--ease-enter),

    border-color var(--dur-micro) var(--ease-enter);

}



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



.tab[aria-current='page'] {

  color: rgb(var(--nova));

  border-bottom-color: rgb(var(--nova));

}



/* --- Theme toggle --------------------------------------------------------- */



/* 44px on every axis. It is a 18px glyph, which is exactly the case the touch

   target rule exists for — the visual mark is small, the tappable area is not. */

.nav__theme {

  display: flex;

  align-items: center;

  justify-content: center;

  width: 44px;

  height: 44px;

  border-radius: var(--radius-pill);

  color: rgb(var(--text-2));

  transition: color var(--dur-micro) var(--ease-enter),

    background-color var(--dur-micro) var(--ease-enter);

}

@media (max-width: 899px) {
  .nav__theme { margin-left: auto; }
}

.nav__theme:hover { color: rgb(var(--vinyl)); background: rgb(var(--vinyl) / 0.06); }



/* One glyph at a time, swapped by attribute rather than by rewriting innerHTML

   on click — so the icon is already correct in the served HTML, and a click

   cannot cause a reflow. Defaults to the moon, matching the default dark page;

   theme.js sets data-state as soon as it runs. */

.nav__theme-sun { display: none; }

.nav__theme[data-state='light'] .nav__theme-sun { display: block; }

.nav__theme[data-state='light'] .nav__theme-moon { display: none; }



/* --- 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;
  max-width: 100%;
  white-space: normal;
  text-align: center;
  transition: transform var(--dur-micro) var(--ease-enter),
    border-color var(--dur-micro) var(--ease-enter),
    color var(--dur-micro) var(--ease-enter);
}

[data-magnetic] {
  display: inline-flex;
  max-width: 100%;
}



.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 {

  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: 0;

    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);

  -webkit-backdrop-filter: blur(8px);

  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: 0;

  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__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__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);

}





/* --- 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. */



html.no-intro .curtain,

html.no-intro .loader {

  display: none !important;

}



#scene[data-loading='true'] {
  display: block !important;
  z-index: var(--z-canvas-loading);
}

.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 {
  --nova: var(--dusk-accent);
  --vinyl: var(--dusk-fg);
  --text-1: var(--dusk-fg);
  --text-2: var(--dusk-text-2);
  --text-3: var(--dusk-text-3);
  --ink-3: var(--dusk-ink-3);
  color-scheme: dark;
  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;
  will-change: transform;
}

.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));
}

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }

  /* ARRIVAL — starts at first paint on new page and sweeps out cleanly */
  .sweep--enter div {
    animation: sweep-out 320ms cubic-bezier(0, 0, 0.2, 1) both;
  }
  .sweep--enter div:nth-child(2) { animation-delay: 30ms; }
  .sweep--enter div:nth-child(3) { animation-delay: 60ms; }

  /* DEPARTURE — sweeps in quickly to cover before page transit */
  .sweep--leave div {
    animation: sweep-in 200ms cubic-bezier(0.4, 0, 0.2, 1) both;
  }
  .sweep--leave div:nth-child(2) { animation-delay: 25ms; }
  .sweep--leave div:nth-child(3) { animation-delay: 50ms; }
}



@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);

  -webkit-backdrop-filter: blur(18px);

  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 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);

  -webkit-backdrop-filter: blur(14px);

  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);

}



.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);

}


/* Mobile overflow guard for the service deep-dive bundle cards: flex/grid
   children default to min-width:auto and refuse to shrink below their content,
   which pushed the .tier cards ~24px past a 390px viewport. This lets them
   shrink without changing the layout on any larger screen. */
@media (max-width: 640px) {
  .column-left,
  .column-left .row,
  .glass,
  .glass .row,
  .glass.tier,
  .glass.tier .row { min-width: 0; max-width: 100%; }
  .glass.tier .row { flex-wrap: wrap; }

  /* On narrow phone screens, action button rows in heroes and intros stack
     cleanly as full-width elements with centered text instead of awkwardly wrapping
     into staggered, unequal-width buttons. */
  .hero .row,
  .answer .row,
  .column-left .row {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    gap: var(--space-3);
  }

  .hero .row > *,
  .hero .row [data-magnetic],
  .hero .row .btn,
  .answer .row > *,
  .answer .row [data-magnetic],
  .answer .row .btn,
  .column-left .row > *,
  .column-left .row [data-magnetic],
  .column-left .row .btn {
    width: 100%;
    display: flex;
    justify-content: center;
    text-align: center;
  }
}

/* Nova launcher status: ambient glow at rest, a tighter shimmer while thinking. */
[data-dock-open] { position: relative; }
[data-dock-open]::after {
  content: ''; position: absolute; inset: -5px; border-radius: inherit; z-index: -1;
  background: radial-gradient(closest-side, rgb(var(--nova) / 0.55), transparent 70%);
  opacity: 0.3; animation: novaGlow 3.4s ease-in-out infinite;
}
[data-dock-open][data-state='thinking']::after { opacity: 0.75; animation-duration: 1.1s; }
@keyframes novaGlow { 0%,100% { transform: scale(0.9); opacity: 0.22; } 50% { transform: scale(1.15); opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) { [data-dock-open]::after { animation: none; } }
