/* ============================================================
   GMDS

   Purple, white, black. The purple is LIGHT, not paint: it lives in the
   stars, the nebula and the atmosphere shell, all of which are rendered.
   The interface itself is white type on near-black and touches violet in
   very few places: the menu, the buttons that survive on the inner pages, and
   the accent rules.

   That distinction is the whole reason this does not read as an AI-purple
   landing page. A violet gradient laid over a dark background is the tell.
   Violet emitted by something in the scene is just what the scene looks like.
   ============================================================ */

@font-face {
  font-family: 'Encode Sans';
  src: url('../fonts/encode-latin.woff2') format('woff2');
  font-weight: 400 800; font-stretch: 75% 125%;
  font-style: normal; font-display: swap;
}

/* The rupee sign, which the Latin subset above does not contain.

   Without this every price on the services page falls back to whatever the
   device has, so the digits render in Encode Sans and the symbol next to them
   in something else: a visibly different shape, weight and baseline, halfway
   through a number. Two files is better than one wrong glyph.

   Same family name, so it is picked up by the character rather than by a
   class, and unicode-range keeps the download to pages that price something.
   Encode Sans is OFL, like the Noto faces the hero uses. */
@font-face {
  font-family: 'Encode Sans';
  src: url('../fonts/encode-rupee.woff2') format('woff2');
  font-weight: 400 800; font-stretch: 75% 125%;
  font-style: normal; font-display: swap;
  unicode-range: U+20B9;
}

:root {
  /* The palette, named by role.

     --void is the ground and --paper is the ink. These were literals scattered
     through six stylesheets until a light theme briefly existed; the theme is
     gone and the names stayed, because a card that says var(--surface) is
     easier to reason about than one that says oklch(11% 0.03 300), and the
     next person to want a second theme will find the work already done.

     Every value is the exact literal it replaced. A pass that rounded several
     of them together is how the footer button lost the solid highlight along
     its top edge, so where two tokens differ by one percent of lightness they
     are still two tokens. */
  --void:      #05030c;
  --paper:     oklch(97% 0.008 300);
  --paper-dim: oklch(80% 0.020 300);
  --paper-mut: oklch(62% 0.028 300);

  --surface:   oklch(11% 0.03 300);         /* cards, panels */
  --surface-2: oklch(9%  0.03 300);         /* the step below a card */
  --surface-3: oklch(10% 0.03 300 / 0.55);  /* a lit area of the same room */
  --scrim:     oklch(8%  0.04 300 / 0.94);  /* the mobile menu over the page */
  --input:     oklch(10% 0.025 300);        /* a field waiting to be typed in */
  --input-lit: oklch(12% 0.035 300);        /* the same field, focused */

  /* Step numbers not yet reached. Close to the ground on purpose: they are
     there to be counted, not read, and the one you are ON takes the accent. */
  --ghost:      oklch(26% 0.05 300);
  --ghost-svc:  oklch(21% 0.05 300);
  --ghost-done: oklch(45% 0.10 300);
  --ghost-line: oklch(40% 0.10 300);

  --violet:       oklch(64% 0.210 300);
  --violet-edge:  oklch(84% 0.13 300);      /* the lit top edge of a button */
  --violet-hi:    oklch(74% 0.19 300);      /* the footer CTA, hovered */
  --violet-lit:   oklch(70% 0.21 300);      /* an accent link, hovered */
  --violet-lit-b: oklch(69% 0.21 300);      /* page and form buttons, hovered */
  --violet-soft:  oklch(80% 0.14 300);      /* a link in the accent, hovered */
  --violet-press: oklch(60% 0.21 300);      /* held down */
  --violet-deep:  oklch(38% 0.14 300 / 0.55); /* a divider in the accent */

  /* The accent at the alphas the site actually uses, each one the exact value
     it replaced. color-mix() would say all of this in one line; it is not used
     because the floor here is whatever supports oklch(), and Safari shipped
     oklch in 15.4 and color-mix in 16.2. */
  --violet-a14: oklch(64% 0.21 300 / 0.14);
  --violet-a28: oklch(64% 0.21 300 / 0.28);
  --violet-a30: oklch(64% 0.21 300 / 0.30);
  --violet-a35: oklch(64% 0.21 300 / 0.35);
  --violet-a40: oklch(64% 0.21 300 / 0.40);
  --violet-a50: oklch(64% 0.21 300 / 0.50);
  --violet-a55: oklch(64% 0.21 300 / 0.55);
  --violet-a75: oklch(64% 0.21 300 / 0.75);

  /* The reference red, sampled and matched: #fb4a3f is oklch(66.2% 0.215 28).

     It measures 4.40:1 on the dark sky, which clears the 3:1 that large text
     needs. Where it crosses the glow it drops to 2.18:1, so the middle of the
     word softens. That is the cost of matching the reference exactly rather
     than picking the lightness that survives both, and it is the client's
     call to make. */
  --scarlet:     #fb4a3f;
  --scarlet-lit: oklch(76% 0.16 28);        /* the red used as a link */
  --scarlet-a70: oklch(66% 0.20 25 / 0.7);  /* a field that failed validation */

  --edge:        oklch(100% 0 0 / 0.14);
  --edge-soft:   oklch(100% 0 0 / 0.07);
  --edge-strong: oklch(100% 0 0 / 0.24);    /* a field under the pointer */

  --shadow:    oklch(4% 0.03 300 / 0.5);
  --shadow-55: oklch(4% 0.03 300 / 0.55);
  --shadow-60: oklch(4% 0.03 300 / 0.6);
  --shadow-lg: oklch(4% 0.03 300 / 0.68);

  /* The bar has its own ground, so a mark never competes with what is behind
     it. A gradient, not a filled strip. */
  --nav-veil: linear-gradient(180deg,
    oklch(8% 0.04 300 / 0.78) 0%,
    oklch(8% 0.04 300 / 0.42) 55%,
    oklch(8% 0.04 300 / 0) 100%);

  /* The washes behind the content pages, and the pair behind the services
     panel, which are their own values rather than the same three reused. */
  --wash-1:  oklch(38% 0.15 300 / 0.55);
  --wash-2:  oklch(34% 0.17 330 / 0.42);
  --wash-3:  oklch(30% 0.14 290 / 0.48);
  --wash-1b: oklch(34% 0.14 300 / 0.5);
  --wash-2b: oklch(30% 0.15 330 / 0.38);

  /* And the three behind the footer. */
  --foot-1: oklch(12% 0.04 300);
  --foot-2: oklch(26% 0.11 300 / 0.55);
  --foot-3: oklch(34% 0.14 320 / 0.4);

  --ease:      cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out:  cubic-bezier(0.25, 1, 0.5, 1);
}

*, *::before, *::after { box-sizing: border-box; }

html { background: var(--void); }

body {
  margin: 0;
  font-family: 'Encode Sans', ui-sans-serif, system-ui, sans-serif;
  color: var(--paper);
  background: var(--void);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.skip-link {
  position: fixed; top: 0; left: 0; z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--void); color: var(--paper);
  transform: translateY(-120%);
}
.skip-link:focus { transform: none; }

:where(a, button):focus-visible { outline: 2px solid var(--violet); outline-offset: 4px; }

/* The scene sits behind everything and never scrolls. It used to be the camera
   that moved; now nothing does, and it is a window onto a sky that drifts. */
.scene {
  position: fixed; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  display: block;
  pointer-events: none;
}

/* --- rail -------------------------------------------------------------
   One label, at the foot of the left edge, and that is all this is now.

   It began as a spine: a mark at the top, the product name set vertically
   down the middle, the origin at the bottom, all of it holding the left
   margin while the nav was held back. The nav is on from the first frame and
   carries the mark, so the top of this column would be the same word twice,
   and the middle of it has been empty since the rotated label came out.

   Kept as a fixed column rather than absolutely positioned at the bottom,
   because it is still the thing anchoring the left margin, and because the
   safe-area inset belongs on a fixed element.
   ---------------------------------------------------------------------- */

.rail {
  position: absolute; z-index: 30;
  left: clamp(1.2rem, 3.5vw, 2.75rem);
  bottom: max(1.2rem, env(safe-area-inset-bottom));
  pointer-events: none;
}
.rail > * { pointer-events: auto; }

/* No text-indent. That was there to pull back the trailing space letter-
   spacing adds after the last glyph, which mattered when the text was centred
   in a fixed-width strip. Flush left, the trailing space falls off the right
   and nothing needs correcting. */
.rail__origin {
  margin: 0;
  font-size: clamp(0.58rem, 0.7vw, 0.66rem);
  font-weight: 700; letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper-mut);
}

@media (max-width: 640px) {
  /* The cue sits centred on the bottom edge, and on a 390px screen it takes
     most of that edge. Upright, the origin runs into it. */
  .rail__origin { display: none; }
}

/* --- the panel ---------------------------------------------------------
   One viewport, centred, and now the only one.

   The snap went with the sequence. It existed to land a flick on a composed
   camera position and stop it parking between two of them; with a single
   screen there is nowhere to park, and a document that snaps with one snap
   point just fights anyone scrolling to the footer.
   ---------------------------------------------------------------------- */

.panels { position: relative; z-index: 10; }

.panel {
  position: relative;                  /* the two labels below anchor to this */
  min-height: 100vh; min-height: 100svh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 0 clamp(4rem, 9vw, 8rem);
  text-align: center;
}

/* AFTER the base rule, not before it.

   This override used to live up in the rail's media block, which is above
   .panel in this file. Both selectors score 0-1-0, so the base rule won on
   document order and a phone kept the desktop's 4rem a side: 128px of the
   390px screen spent on margin, with the display word squeezed into what was
   left. Everything about the mobile hero that looked wrong came from those
   two rules being in the wrong order.

   Symmetric, because nothing sits in the middle of the left margin for the
   type to clear any more: the rail is a label at the top and one at the
   foot, and the panels are centred between them. */

/* The word is the composition at this size, so tracking comes in hard and
   leading goes under one. nowrap because a display word that wraps is not a
   display word. */
.title {
  margin: 0;
  /* 15.5vw rather than 17, because the width axis below adds about a
     quarter to every glyph, and at 17vw a six-letter word runs past the
     padding. GLORY and SPEED are five each, so there is headroom to spare
     right now; the clamp stays sized for the longest word this set has
     carried rather than the shortest one currently in it. */
  font-size: clamp(3rem, 15.5vw, 13.5rem);
  font-weight: 800; font-stretch: 125%;
  line-height: 0.82; letter-spacing: -0.055em;
  text-transform: uppercase;
  color: var(--scarlet);
  white-space: nowrap;
}

.char { display: inline-block; will-change: transform; }

.lede {
  margin: clamp(1.4rem, 3.4vh, 2.6rem) 0 0;
  display: grid; gap: 0.25rem;
  font-size: clamp(0.95rem, 1.35vw, 1.25rem);
  font-weight: 600; line-height: 1.5;

  /* Gradient fill, clipped to the glyphs.

     Client's values, unchanged: white at the cap line falling to lavender at
     the baseline. Worth knowing what it costs, because it is not a contrast
     win: against the glow the stops measure 1.57, 1.33 and 1.13 to one, so
     the lower half of each letter sits almost exactly at the background
     luminance where the copy crosses the bright area. Against the dark sky
     it is 15, 12.7 and 8.5 to one and reads perfectly.

      is the fallback and has to come first, for anything that does not
     support background-clip. The halo moves to a drop-shadow filter, because
     text-shadow paints from the glyph alpha and behaves unpredictably once
     the fill is transparent; a filter applies to the composited result and
     does not care how the fill was made. */
  color: #E9D5FF;
  background: linear-gradient(180deg, #FFFFFF 0%, #F3E8FF 45%, #D8B4FE 100%);
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  filter:
    drop-shadow(0 1px 2px oklch(5% 0.02 300 / 0.95))
    drop-shadow(0 2px 9px oklch(5% 0.02 300 / 0.85))
    drop-shadow(0 5px 26px oklch(5% 0.02 300 / 0.6));
  max-width: 44ch;
}
.lede span { display: block; }

@media (max-width: 640px) {
  .panel { padding-left: 1.1rem; padding-right: 1.1rem; }

  /* 20vw, not 15.5.

     15.5vw is a fraction of the viewport, and on a phone that fraction is
     small in absolute terms: GLORY came out 205px across a 360px screen, so
     the display word used about three fifths of the width and the other two
     fifths were empty margin either side. At this size a hero word either
     fills the screen or it looks like body copy that got out of hand.

     20vw is as far as it goes. The ceiling is a six-letter word at 360px,
     which lands at about 318px against 324px of usable width: it fits, with
     nothing to spare. Anything longer than six characters needs this number
     brought down, because the word is nowrap and would run off the edge
     rather than break. */
  .title { font-size: 20vw; }

  /* Off the clamp floor. 0.95rem is 15.2px, which is under the 16px a phone
     wants for a line that is being read rather than glanced at, and this is
     the only sentence on the screen. */
  .lede { font-size: 1.05rem; }
}

.u-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* --- the navigation that arrives with the machine ----------------------

   Redesigned to belong to this page rather than to navigation in general.

   The first version was the arrangement every site has: mark left, links
   right, filled pill, sentence case, weight 700. Nothing in it came from the
   rest of the interface, which is built almost entirely out of three things:
   tracked capitals with a matching text-indent, one-pixel hairlines, and a
   variable width axis. So the nav is built out of those three things too.

   Two states, switched by data-mode on the root. Through the hero the rail
   and the cue are the whole of the furniture; once the machine is in view
   they hand over. They cross-fade, and the mark is never in two places.
   ---------------------------------------------------------------------- */

.nav {
  position: fixed; z-index: 40;
  top: 0; left: 0; right: 0;
  display: flex; align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
  padding: clamp(0.85rem, 1.9vh, 1.25rem) clamp(1.2rem, 3.5vw, 2.75rem);

  opacity: 0;
  translate: 0 -0.75rem;
  pointer-events: none;
  transition: opacity 520ms var(--ease-out), translate 520ms var(--ease-out);
}

/* On from the start.

   The hidden state above is kept because the reveal sequence can be switched
   back on, and it is what the nav arrives FROM when it is. With the sequence
   off, .nav--lit in the markup says the bar is simply present, and the
   entrance below fades it in once rather than waiting on a scroll that never
   comes. */
.nav--lit {
  opacity: 1;
  translate: none;
  pointer-events: auto;
}

[data-mode="reveal"] .nav { opacity: 1; translate: none; pointer-events: auto; }

/* Ground. The bar sits over a lit scene, and legibility should not depend on
   which part of the sky happens to be behind it. A gradient rather than a
   filled bar: a solid strip would cut the frame in half and undo the point of
   the machine standing in open space. */
.nav::before {
  content: '';
  position: absolute; inset: 0;
  background: var(--nav-veil);
  pointer-events: none;
  z-index: -1;
}

/* The rule is inset to the padding, so it reads as a line under the content
   rather than the edge of a box. Same hairline the cue uses. */
.nav::after {
  content: '';
  position: absolute;
  left: clamp(1.2rem, 3.5vw, 2.75rem);
  right: clamp(1.2rem, 3.5vw, 2.75rem);
  bottom: 0; height: 1px;
  background: var(--edge);
}

/* The mark carries the width axis, which is the one thing about this
   typeface the page actually uses: the headlines are set at 125% and it is
   what makes them look like these headlines rather than any bold sans. The
   mark is followed by an auto margin, so widening it pushes nothing. */
/* The house. Sized against the links rather than the bar, so it sits on the
   same optical line as the words next to it, and given a hit area larger than
   the drawing: a 20px glyph is a 20px target, and a target on a phone wants
   to be nearer 44. */
.nav__home {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.4rem; height: 2.4rem;
  /* The auto margin is the whole layout of the bar: it holds the icon against
     the left edge and pushes the links and the button over to the right, which
     is where the wordmark used to put them. Without it everything bunches up
     behind the icon and the CTA ends up in the middle of the bar. */
  margin-right: auto;
  margin-left: -0.5rem;                  /* pull the ink back to the margin */
  color: var(--paper);
  border-radius: 50%;
  transition: color 200ms var(--ease-out), background 200ms var(--ease-out);
}
.nav__home:hover { color: var(--paper); background: var(--edge-soft); }
.nav__home svg { display: block; }

.nav__links {
  display: flex; align-items: center;
  gap: clamp(1.1rem, 2.6vw, 2.2rem);
}

/* Tracked capitals, at the size and tracking the rail and the cue use, so
   the nav reads as the same system rather than a different one bolted on.
   text-indent matches the letter-spacing: tracking adds a trailing space
   after the last glyph, and without the indent every label sits fractionally
   left of where it looks like it should. */
.nav__links a {
  position: relative;
  display: inline-block;
  padding: 0.35rem 0;
  color: var(--paper-mut); text-decoration: none;
  font-size: clamp(0.6rem, 0.72vw, 0.68rem);
  font-weight: 700;
  letter-spacing: 0.22em; text-indent: 0.22em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 260ms var(--ease-out);
}

/* Hairline drawn from the left. Transform only, so it costs a composite and
   never a layout: the width axis would have been the obvious hover for this
   typeface, but changing it reflows every sibling to the right of the one
   under the cursor. */
.nav__links a::after {
  content: '';
  position: absolute;
  left: 0; right: 0.22em; bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 340ms var(--ease-out);
}

.nav__links a:hover,
.nav__links a:focus-visible { color: var(--paper); }

.nav__links a:hover::after,
.nav__links a:focus-visible::after { transform: none; }

.nav__toggle { display: none; }

@media (max-width: 780px) {
  /* Four labels and a button will not sit on a phone, and deleting the links
     is not a design decision, it is a missing menu. They move into a panel
     that opens under the bar. */
  .nav { gap: 0.9rem; }

  .nav__toggle {
    display: inline-flex; align-items: center;
    margin-left: auto;
    padding: 0.5rem 0.15rem;
    border: 0; background: none;
    color: var(--paper);
    font: inherit;
    font-size: 0.62rem; font-weight: 700;
    letter-spacing: 0.22em; text-indent: 0.22em;
    text-transform: uppercase;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .nav__toggle-shut { display: none; }
  .nav.is-open .nav__toggle-open { display: none; }
  .nav.is-open .nav__toggle-shut { display: inline; }

  .nav__mark { margin-right: 0; }

  .nav__links {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: flex-start;
    gap: 0;
    padding: 0 clamp(1.2rem, 3.5vw, 2.75rem);
    background: var(--scrim);
    backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--edge);

    /* The panel is out of flow already, so its closed state does not need to
       collapse to zero height. It needs to be invisible and unreachable, which
       visibility gives us for free: no keyboard focus, no pointer events, no
       screen reader, and no height to measure in JS.

       This replaces a grid-template-rows: 0fr trick that only ever collapsed
       the first link. The other three landed in implicit rows, kept their
       natural height, and left the menu permanently half open over the top of
       whatever page you were on. */
    display: flex;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.4rem);
    transition: opacity 240ms var(--ease-out),
                transform 320ms var(--ease-out),
                visibility 0s linear 320ms;
  }
  .nav.is-open .nav__links {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 240ms var(--ease-out),
                transform 320ms var(--ease-out),
                visibility 0s;
  }

  .nav__links a {
    display: block;
    padding: 1rem 0;
    font-size: 0.72rem;
    border-bottom: 1px solid var(--edge-soft);
  }
  .nav__links a:last-child { border-bottom: 0; }
  .nav__links a::after { display: none; }

}

@media (prefers-reduced-motion: reduce) {
  .nav, .nav__links, .nav__links a::after { transition: none; }
}

/* --- the reveal panel --------------------------------------------------- */

.panel--reveal {
  justify-content: flex-end;
  padding-bottom: clamp(2.5rem, 8vh, 5.5rem);
  position: relative;
}

/* The copy lands on the stand and the keyboard, which are the lightest things
   in the frame, so it needs ground of its own. A gradient rather than a box:
   a panel with an edge would read as a component sitting on the photograph
   instead of part of it. */
.panel--reveal::after {
  content: '';
  position: absolute; inset: auto 0 0 0;
  height: 46vh;
  /* Reaches strength where the words are, not at the very foot of the page.
     It used to be barely half on at 45%, which is exactly where the copy
     sits, so the copy was reading against a lit aluminium stand rather than
     against the scrim that was supposed to be carrying it. */
  background: linear-gradient(180deg,
    oklch(11% 0.06 300 / 0) 0%,
    oklch(10% 0.07 300 / 0.46) 22%,
    oklch(9%  0.07 300 / 0.80) 44%,
    oklch(7%  0.07 300 / 0.93) 100%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 640ms var(--ease-out);
}
[data-mode="reveal"] .panel--reveal::after { opacity: 1; }

.reveal {
  position: relative; z-index: 1;

  /* The same halo the hero's subtitle carries. A drop-shadow filter rather
     than text-shadow, for the reason recorded up there: it applies to the
     composited result and does not care how the fill was made. Three stops,
     tight to loose, so small type keeps an edge over a busy ground without
     the block looking like it is sitting in a box. */
  filter:
    drop-shadow(0 1px 2px oklch(6% 0.03 300 / 0.92))
    drop-shadow(0 2px 12px oklch(6% 0.03 300 / 0.78))
    drop-shadow(0 6px 30px oklch(6% 0.03 300 / 0.55));
}

.reveal {
  max-width: 34rem;
  text-align: center;
  opacity: 0;
  translate: 0 1.25rem;
  transition: opacity 640ms var(--ease-out), translate 640ms var(--ease-out);
}
[data-mode="reveal"] .reveal { opacity: 1; translate: none; }

.reveal__kicker {
  margin: 0 0 0.7rem;
  font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.24em; text-indent: 0.24em;
  text-transform: uppercase;

  /* The site's one accent, rather than another violet.

     A violet kicker over a violet backdrop is the same colour twice, and it
     read as a caption nobody had chosen. Scarlet is the only other colour
     this page owns, it is what the three hero headlines are set in, and using
     it here ties the pull-back back to what it is a pull-back from. Small,
     tracked and uppercase, so it stays a label rather than competing with the
     line under it. */
  /* A tint of the accent rather than the accent itself.

     Measured against the lit stand this copy lands on, #fb4a3f is 2.0:1. It
     is chosen to carry a display word across a night sky, and at 0.66rem on
     a lit aluminium surface it is simply too dark to read. Same hue, lifted
     to 76% lightness, which measures 5.8:1 on the scrimmed ground and still
     reads as the same red as the headlines. */
  color: var(--scarlet-lit);
}

.reveal__title {
  margin: 0;
  font-size: clamp(1.9rem, 4.6vw, 3.4rem);
  font-weight: 800; font-stretch: 108%;
  line-height: 1.02; letter-spacing: -0.03em;
  color: var(--paper);
}

.reveal__lede {
  margin: 0.9rem 0 0;
  font-size: clamp(0.9rem, 1.15vw, 1.05rem);
  line-height: 1.6;
  color: oklch(88% 0.02 300);
}

@media (prefers-reduced-motion: reduce) {
  .nav, .reveal { transition: none; }
}

/* --- cue ---------------------------------------------------------------
   Lead word, track, name, along the bottom. The name is the part that earns
   its place: it says what the next screen is, which both a count of screens
   and the word "scroll" on their own do not.

   The name changes width as it changes word, and a centred flex row would
   slide the track sideways every time it did. So the row is a grid with the
   track in the middle and equal columns either side: the line stays put and
   the words grow outwards from it.
   ---------------------------------------------------------------------- */

.cue {
  position: absolute; z-index: 30;
  left: 50%; translate: -50% 0;
  bottom: max(1.5rem, env(safe-area-inset-bottom));
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: 1rem;
  pointer-events: none;
  transition: opacity 380ms var(--ease-out);
}
/* The figure carries the emphasis, because the figure is the point. It was
   tracked capitals while it held the word "scroll"; a number set in 0.34em
   capitals reads as a part number rather than a count, so the tracking comes
   off and the weight goes up. */
.cue__lead {
  margin: 0; justify-self: end;
  font-size: 0.82rem; font-weight: 800;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  white-space: nowrap;
}
.cue__track {
  width: clamp(5rem, 14vw, 11rem); height: 1px;
  background: var(--edge); overflow: hidden;
}
.cue__fill {
  display: block; height: 100%;
  background: var(--violet);
  transform: scaleX(0); transform-origin: 0 50%;
}
.cue__name {
  margin: 0; justify-self: start;
  font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--paper-mut);
  white-space: nowrap;
}

/* On a phone the row is wider than the phone: the figure, a track and a
   six-word phrase do not fit across 390px, and shrinking the type to make
   them fit puts the only proof on the page at a size nobody reads.

   So it stacks, in the order the markup already has it, and the phrase is
   allowed to wrap. Nothing is dropped: the count is the reason this line
   exists, and the phrase is what makes the count mean anything. */
@media (max-width: 640px) {
  .cue {
    grid-template-columns: auto;
    justify-items: center;
    gap: 0.45rem;
    width: min(90vw, 22rem);
    text-align: center;
  }
  .cue__lead, .cue__name {
    justify-self: center;
    white-space: normal;
  }
  .cue__track { width: 4.5rem; }
}

/* Off on any screen that does not carry a line of its own.

   animation: none has to come first. The entrance sets `html.ready .cue` with
   fill-mode both, which holds its final opacity of 1, and an animated value
   beats a declared one whatever the specificity. Cancelling the animation is
   what lets the 0 through. */
.cue.is-off {
  animation: none;
  opacity: 0;
  pointer-events: none;
}

/* The cue used to need hiding here, because it was fixed and floated over the
   footer. It belongs to the hero now and leaves with it, so there is nothing
   to hide. Only the canvas still needs standing down. */

/* --- entrance ----------------------------------------------------------
   Per-character rise on the first title only. The later panels arrive with
   the camera, so animating them as well would be two things saying the same
   thing at the same time.
   ---------------------------------------------------------------------- */

@keyframes char-rise { from { transform: translateY(0.9em); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes rise      { from { transform: translateY(1.2rem); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes fade      { from { opacity: 0; } to { opacity: 1; } }

html.ready .panel[data-panel="0"] .char {
  animation: char-rise 1s var(--ease) both;
  animation-delay: calc(120ms + var(--i) * 45ms);
}
html.ready .panel[data-panel="0"] .lede { animation: rise 900ms var(--ease-out) 620ms both; }
html.ready .panel[data-panel="0"] .cta  { animation: rise 900ms var(--ease-out) 780ms both; }
html.ready .rail     { animation: fade 900ms var(--ease-out) both; }
html.ready .rail__cta{ animation: rise 800ms var(--ease-out) 700ms both; }
html.ready .cue      { animation: fade 900ms var(--ease-out) 900ms both; }

/* --- no WebGL, or reduced motion ---------------------------------------
   The scene is the decoration, not the content. Without it the page is a
   dark document with the same words in the same order.
   ---------------------------------------------------------------------- */

html[data-hero="flat"] body {
  background:
    radial-gradient(120% 80% at 50% 0%, oklch(24% 0.09 300) 0%, transparent 62%),
    var(--void);
}
html[data-hero="flat"] .panel { min-height: auto; padding-block: clamp(4rem, 12vh, 8rem); }
html[data-hero="flat"] .cue { display: none; }

.fallback { padding: 6rem clamp(1.25rem, 6vw, 5rem); max-width: 48rem; }
.fallback h1 { font-size: clamp(2rem, 5vw, 3.5rem); line-height: 1.05; letter-spacing: -0.03em; }
.fallback p { color: var(--paper-dim); line-height: 1.6; }

@media (prefers-reduced-motion: reduce) {
  html.ready .char, html.ready .lede, html.ready .cta,
  html.ready .nav, html.ready .cue { animation: none; }
  * { animation-duration: 1ms !important; transition-duration: 1ms !important; }
}

/* Only ever created when ?debug=1 is present; see js/hero.js. */
.diag {
  position: fixed; z-index: 90; right: 0.6rem; bottom: 0.6rem; margin: 0;
  padding: 0.7rem 0.9rem; max-width: min(26rem, 70vw);
  font: 500 11px/1.6 ui-monospace, Consolas, monospace;
  color: oklch(90% 0.05 300);
  background: oklch(10% 0.03 300 / 0.92);
  border: 1px solid oklch(100% 0 0 / 0.16); border-radius: 6px;
  white-space: pre-wrap; pointer-events: none;
}

/* Created by the inline reporter in index.html when something throws. */
.booterr {
  position: fixed; z-index: 200; left: 50%; top: 50%;
  translate: -50% -50%;
  margin: 0; padding: 1.1rem 1.3rem; max-width: min(44rem, 88vw);
  font: 500 12px/1.65 ui-monospace, Consolas, monospace;
  color: oklch(85% 0.15 25);
  background: oklch(10% 0.03 300 / 0.96);
  border: 1px solid oklch(60% 0.2 25 / 0.6); border-radius: 6px;
  white-space: pre-wrap; word-break: break-word;
}

/* --- hero furniture leaves when the machine arrives ---------------------

   Last in the file, and matched on html rather than a bare attribute, both
   deliberately.

   The entrance keyframes are `html.ready .rail`, which scores 0-2-1. A rule
   written `[data-mode="reveal"] .rail` scores 0-2-0 and loses, so the rail
   stayed lit through the reveal with its mark on top of the nav's, spelling
   GGMDS. Adding html makes it 0-2-1 as well, and being last settles the tie.

   `animation: none` has to come first: fill-mode both holds the final
   keyframe, and an animated value beats a declared one whatever the
   specificity. Cancelling the animation is what lets the opacity through.
   ---------------------------------------------------------------------- */

html[data-mode="reveal"] .rail,
html[data-mode="reveal"] .rail__cta,
html[data-mode="reveal"] .cue {
  animation: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 380ms var(--ease-out);
}

/* The hero's own type leaves with the hero.

   Panel 3's headline scrolls up through the shot as the machine comes in, so
   the word SPEED travels over the picture of itself. The screen is carrying
   that text now; the page does not need to as well. Only the hero panels
   fade: the reveal panel's own copy is what the fourth screen is for. */
html[data-mode="reveal"] .panel:not(.panel--reveal) .title,
html[data-mode="reveal"] .panel:not(.panel--reveal) .lede {
  animation: none;
  opacity: 0;
  transition: opacity 420ms var(--ease-out);
}

/* --- the footer, on the hero page --------------------------------------

   Scoped to html[data-hero], which only index.html sets, so the other pages
   keep the footer exactly as footer.css leaves it.

   Two things have to be said here and nowhere else. The scene is a fixed
   canvas at z-index 0, and the footer is a positioned element with no z-index
   of its own: it wins on document order, but only by accident, and one
   stacking context added anywhere above it would put the starfield back on
   top of the contact details. So it is stated.

   The snap alignment that used to be here went with the snapping itself.
   ---------------------------------------------------------------------- */

html[data-hero] .foot {
  position: relative;
  z-index: 20;
  margin-top: 0;                    /* the hero screen is the gap */
}

/* The machine is still being rendered behind an opaque footer, at the cost of
   a full bloom pass a frame, and js/hero.js stops that. This is the other
   half of it: the attribute is set from the same observer. */
html[data-foot="in"] .scene { visibility: hidden; }


/* --- the projects menu -------------------------------------------------

   Our Projects is a link and a menu at once: the label opens the page, the
   caret beside it opens the two projects. A menu that swallows its own parent
   makes the page it names unreachable, which is a common and annoying way to
   build this.

   Opens on click, not on hover alone. Hover-only menus cannot be reached from
   a keyboard, and on a touch screen the first tap is eaten opening the thing
   you thought you were tapping. Hover is added on top for pointers, because
   on a pointer it is genuinely quicker.
   ---------------------------------------------------------------------- */

.nav__has {
  position: relative;
  display: inline-flex; align-items: center; gap: 0.3rem;
}

/* The trigger wears the same clothes as the links beside it, because it is
   one of them as far as anybody reading the bar is concerned. A button reset
   first: font, colour and padding do not inherit into a <button>. */
.nav__trigger {
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.35rem 0;
  border: 0; background: none;
  color: var(--paper-mut);
  font: inherit;
  font-size: clamp(0.6rem, 0.72vw, 0.68rem);
  font-weight: 700;
  letter-spacing: 0.22em; text-indent: 0.22em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 260ms var(--ease-out);
}
.nav__trigger:hover,
.nav__trigger[aria-expanded="true"] { color: var(--paper); }

/* The caret turns over when the menu is open, and text-indent is cancelled on
   it: the tracking above adds a trailing space that would push the arrow away
   from the word it belongs to. */
.nav__trigger svg {
  display: block;
  text-indent: 0;
  transition: rotate 240ms var(--ease-out);
}
.nav__trigger[aria-expanded="true"] svg { rotate: 180deg; }

.nav__menu {
  position: absolute; z-index: 50;
  top: calc(100% + 0.7rem); left: -0.9rem;
  margin: 0; padding: 0.4rem;
  list-style: none;
  min-width: 11rem;
  background: var(--scrim);
  backdrop-filter: blur(14px);
  border: 1px solid var(--edge);

  /* visibility, not display, so it can be animated and so it is properly
     unreachable when shut: no focus, no pointer, no screen reader. */
  opacity: 0;
  visibility: hidden;
  translate: 0 -0.35rem;
  transition: opacity 180ms var(--ease-out), translate 220ms var(--ease-out),
              visibility 0s linear 220ms;
}
.nav__has.is-open .nav__menu,
.nav__has:hover .nav__menu,
.nav__has:focus-within .nav__menu {
  opacity: 1;
  visibility: visible;
  translate: none;
  transition: opacity 180ms var(--ease-out), translate 220ms var(--ease-out),
              visibility 0s;
}

.nav__menu a {
  display: block;
  padding: 0.6rem 0.75rem;
  color: var(--paper-dim); text-decoration: none;
  font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.14em; text-indent: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 180ms var(--ease-out), background 180ms var(--ease-out);
}
.nav__menu a:hover,
.nav__menu a:focus-visible { color: var(--paper); background: var(--edge-soft); }

@media (max-width: 780px) {
  /* In the panel the menu is not a panel of its own: it is the two projects
     indented under the heading they belong to, which is how a nested list
     reads on a phone. Always open, because the panel is already a disclosure
     and a disclosure inside a disclosure is two taps to reach a link. */
  .nav__has { display: block; }

  /* A row in the panel, like the links. The caret is dropped because the menu
     below it is always open here: an arrow that points at something already
     visible is an arrow asking to be pressed for nothing. */
  .nav__trigger {
    display: block; width: 100%;
    padding: 1rem 0;
    text-align: left;
    font-size: 0.72rem;
    color: var(--paper);
    cursor: default;
  }
  .nav__trigger svg { display: none; }

  .nav__menu {
    position: static;
    opacity: 1; visibility: visible; translate: none;
    min-width: 0; padding: 0 0 0.4rem 0.9rem;
    background: none; backdrop-filter: none; border: 0;
    border-bottom: 1px solid oklch(100% 0 0 / 0.07);
  }
  .nav__menu a {
    padding: 0.7rem 0;
    font-size: 0.66rem;
    color: var(--paper-mut);
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav__trigger, .nav__trigger svg, .nav__menu { transition: none; }
}

/* --- the word ----------------------------------------------------------

   One word in eighteen languages, running right to left, for ever.

   Two identical passes in one track; the track slides exactly -50% of itself,
   which lands pass two precisely where pass one began. Restart, and nothing
   visibly moves. Any other distance leaves a seam once a cycle.

   THE BOX MUST NOT SIZE ITSELF TO THE TRACK. The panel is a column flex
   container, so this is a flex item, and a flex item takes its content's width
   on the cross axis unless told otherwise: the first version of this became as
   wide as the whole 25,000px track and made the document thirteen thousand
   pixels across. overflow: hidden clipped what was painted, so it looked
   correct while the page had a horizontal scroll nobody could see. align-self
   and min-width are what actually hold it to the screen; the overflow only
   hides what hangs out of it.
   ---------------------------------------------------------------------- */

.mq {
  align-self: stretch;                 /* the panel's width, not the track's */
  min-width: 0;                        /* and never wider, whatever is inside */
  width: auto;
  overflow: hidden;

  /* Out through the panel's padding, edge to edge. Negative margins rather
     than 100vw: with overflow-x hidden on the body, a 100vw child is wider
     than the space actually available whenever a scrollbar is showing, and
     the page gains a horizontal scroll. Matching the padding cannot do that. */
  margin-inline: calc(-1 * clamp(4rem, 9vw, 8rem));

  /* The edges dissolve rather than cut. A hard edge says the words are inside
     a box; a fade says they carry on past the screen. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}

.mq__track {
  display: flex;
  width: max-content;
  /* 40s for one full pass of eighteen words. Slower and the screen is a
     still; faster and the Indic scripts, which carry marks above and below
     the line, blur before they can be read. */
  animation: mq 40s linear infinite;
  will-change: transform;
}

.mq__run {
  display: flex; align-items: center;
  flex: 0 0 auto;
}

@keyframes mq {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* The size the headline was. This is the display type of the page now, so it
   inherits what the display type had. */
.mq__w {
  display: block;
  font-family: 'GMDS Indic', 'Encode Sans', ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(3rem, 15.5vw, 13.5rem);
  font-weight: 800; font-stretch: 125%;
  line-height: 1.04;
  color: var(--scarlet);
  white-space: nowrap;

  /* Room for the marks, measured rather than guessed.

     Indic scripts hang matras above the line and subscript consonants below
     it, and Urdu hangs further below than anything else here. With the mask
     on .mq clipping whatever leaves this box, 0.1em was cutting the tops off
     Devanagari, Bengali, Gujarati, Kannada and Telugu and the bottoms off
     Kannada, Telugu and Urdu.

     The number is measured in a browser, not derived from the font files.
     Deriving it from per-glyph bounding boxes gave 0.23em and left the Tamil
     three pixels from the edge, because a glyph's own box does not account
     for GPOS mark positioning moving a vowel sign below it, nor for the faux
     bold Chrome synthesises when this asks for 800 from a 700 face. Both add
     ink the font file does not admit to.

     Note this cannot be fixed by making the type smaller: the ink and the box
     both scale with font-size, so the clipping is identical at every size. */
  padding-block: 0.32em;
}

/* Latin is the site's own face, drawn to be tracked in tight. The Noto faces
   are not, and letter-spacing applied to an Indic cluster pulls the marks away
   from the consonant they belong to. */
.mq__w:lang(en) { letter-spacing: -0.055em; }

.mq__dot {
  flex: 0 0 auto;
  width: 0.4rem; height: 0.4rem;
  margin: 0 clamp(1.2rem, 3.2vw, 2.8rem);
  border-radius: 50%;
  background: var(--violet);
  opacity: 0.6;
}

@media (max-width: 640px) {
  .mq { margin-inline: -1.1rem; }      /* the mobile panel padding, undone */
}

@media (prefers-reduced-motion: reduce) {
  /* Stopped, not slowed. Eighteen words parked mid-scroll would show two half
     words, so it rests at the start, which is the English one. */
  .mq__track { animation: none; }
}

/* --- the tagline -------------------------------------------------------- */

.tag {
  margin: clamp(1.4rem, 4vh, 2.6rem) 0 0;
  display: grid;
  gap: 0.5rem;
  text-align: center;
}
.tag span {
  font-size: clamp(0.62rem, 0.9vw, 0.78rem);
  font-weight: 700;
  letter-spacing: 0.34em; text-indent: 0.34em;
  text-transform: uppercase;
  color: var(--paper-mut);
}
.tag b {
  font-size: clamp(1.05rem, 2.1vw, 1.6rem);
  font-weight: 800; font-stretch: 112%;
  letter-spacing: 0.02em;
  color: var(--paper);
}

/* --- join us -----------------------------------------------------------

   The outline draws itself, the same way the GMDS in the footer does, from a
   dash offset. Repeating that here is deliberate: two ends of the same page
   using one idea reads as a house style, where two different clever effects
   would read as two different people.

   The ring is an SVG rather than a border because a border cannot be drawn
   progressively, and the alternative, animating a conic gradient, is the
   effect every site has.
   ---------------------------------------------------------------------- */

.join {
  position: relative;
  display: inline-flex; align-items: center; gap: 0.8rem;
  margin-top: clamp(1.6rem, 4.5vh, 2.8rem);
  padding: 1rem 1.9rem;
  color: var(--paper); text-decoration: none;
  font-size: clamp(0.66rem, 0.86vw, 0.76rem);
  font-weight: 800;
  letter-spacing: 0.26em; text-indent: 0.26em;
  text-transform: uppercase;
  white-space: nowrap;
  isolation: isolate;
  transition: color 240ms var(--ease-out), translate 380ms var(--ease-out);
}

/* The resting outline: always there, always faint. */
.join::before {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  border: 1px solid var(--edge);
  border-radius: 999px;
  transition: background 300ms var(--ease-out), border-color 300ms var(--ease-out);
}
.join:hover::before,
.join:focus-visible::before {
  background: oklch(64% 0.21 300 / 0.10);
  border-color: transparent;
}

.join__ring {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
.join__ring rect {
  width: calc(100% - 2px); height: calc(100% - 2px);
  fill: none;
  stroke: var(--scarlet);
  stroke-width: 1.4;
  /* Set from JS, which is the only way to know the true perimeter of a rounded
     rect whose size depends on the text inside it. The fallback here is long
     enough to hide the stroke on any button this page will ever have. */
  stroke-dasharray: var(--ring, 900);
  stroke-dashoffset: var(--ring, 900);
  transition: stroke-dashoffset 620ms var(--ease-out);
}
.join:hover .join__ring rect,
.join:focus-visible .join__ring rect { stroke-dashoffset: 0; }

/* The arrow. Drawn with borders rather than a glyph, so it is the one shape
   on the page that costs nothing and matches the CTA in the footer. */
.join__go {
  width: 0.42em; height: 0.42em;
  border-top: 1.6px solid currentColor;
  border-right: 1.6px solid currentColor;
  transform: rotate(45deg);
  text-indent: 0;
  transition: translate 320ms var(--ease);
}
.join:hover .join__go { translate: 0.22em 0; }

.join:active { translate: 0 1px; }

@media (prefers-reduced-motion: reduce) {
  .join, .join__ring rect, .join__go { transition: none; }
  /* Without the draw, the outline still has to appear on hover. */
  .join:hover .join__ring rect { stroke-dashoffset: 0; }
}
