/* ==========================================================================
   studio.everon.one

   One dark page. Three sources, all of them inspected rather than eyeballed:
   the QuotesAI theme's skeleton and its three CSS effects, nosignups.net's
   premium-dark recipe read out of its shipped stylesheet, and the copy that
   was parked when the Studio page came off everon.one.

   WHAT MAKES DARK LOOK EXPENSIVE, AND WHAT WE TOOK

     - The accent is LIGHT, never paint. It appears as a ring plus a soft halo
       (box-shadow), not as a filled surface. One violet, used sparingly.
     - Near-blacks are LAYERED - #0a0a0b, #141417, #1b1b20 - never one flat
       black. Depth comes from the stack, not from shadows.
     - A metallic sheen, linear-gradient(120deg, light, dark 51%, light), on
       the elements that want to read as a machined edge.

   NO 62.5% ROOT TRICK. everon.one inherits that from the Hostina template and
   every rem there means something other than what it says. This is a
   greenfield file: 1rem is 16px, as the browser intended.

   NOTHING IS PRE-HIDDEN. The storefront's one standing flaw is
   [data-animate] { opacity: 0 } in CSS - if its JavaScript ever fails to run,
   the page is blank. Here every section is fully visible in CSS, and motion.js
   sets the "before" state itself only once it knows it can run. No JS, no
   animation, complete page.
   ========================================================================== */

/* ---------- fonts, self-hosted ----------
   One variable file per family per subset. Google serves Inter and Space
   Grotesk as variable fonts, so a single download covers every weight; asking
   for four weights fetched the same bytes four times.

   latin-ext is not optional: Bosnian needs c-acute, c-caron, z-caron, s-caron
   and d-stroke, and without it every Bosnian heading silently falls back to a
   system face. */
@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/space-grotesk-latin.woff2") format("woff2");
  font-weight: 300 700;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/space-grotesk-latin-ext.woff2") format("woff2");
  font-weight: 300 700;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "InterVar";
  src: url("../fonts/inter-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "InterVar";
  src: url("../fonts/inter-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/*
 * Nunito, for the bar and nothing else.
 *
 * everon.one sets its whole page in Nunito. Matching its declared sizes here
 * never made the two bars look alike, because Inter is a different face: at an
 * identical 15px its glyph run stands 19px against Nunito's 20, and at 21.6px
 * it is 26 against 30. No size can close that, and the last attempt to try -
 * shaving 3% off everon's numbers - only made the sizes differ too.
 *
 * These are the storefront's own two files, byte for byte, so the outlines,
 * the hinting and the variable axis are the same and the rasteriser has no
 * room to disagree. Latin and latin-ext only; no italics, because the bar has
 * none. The unicode-ranges are copied verbatim from its fonts.css.
 */
@font-face {
  font-family: "Nunito";
  src: url("../fonts/nunito-latin.woff2") format("woff2");
  font-weight: 200 1000;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Nunito";
  src: url("../fonts/nunito-latin-ext.woff2") format("woff2");
  font-weight: 200 1000;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------- tokens ----------
   Dark blue, not dark grey. Every neutral carries a little of the accent hue -
   the ground is a night sky rather than soot, and the hairlines are cold light
   rather than white at low opacity. A mid-grey next to this blue would read as
   unconsidered; these read as chosen, which is the whole difference.

   The accents themselves are not invented: rgb(56,130,255) and rgb(160,210,255)
   are the two anchors of the domain search field ring on everon.one. */
:root {
  --ground: #090b10;
  --surface: #12151c;
  --surface-2: #181c25;
  --line: rgba(140, 170, 255, 0.1);
  --line-strong: rgba(140, 170, 255, 0.2);

  --ink: #f4f4f5;
  --muted: #9ba3b4;
  --faint: #7d879d;   /* was #667089: 3.98:1 on the ground, under AA */

  --accent: #3882ff;
  --accent-fill: #2a6fe0;   /* white on this is 4.6:1; on --accent, 3.62 */
  --accent-2: #a0d2ff;
  --accent-3: #569bff;
  --glow: rgba(56, 130, 255, 0.28);

  --radius: 12px;
  --radius-lg: 18px;

  --display: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
  --body: "InterVar", "Segoe UI", system-ui, -apple-system, sans-serif;
  /* The bar only. everon.one is set in Nunito throughout; this site is not,
     and only the bar is being cloned. */
  --nav-font: "Nunito", "Segoe UI", system-ui, sans-serif;
  --nav-text: #a7a7a7;        /* everon --color-text-dark */
  --nav-text-hover: #fff;

  --sheen: linear-gradient(120deg, #2a2a2e 0%, #0a0a0b 51%, #2a2a2e 100%);
  /* The distance every section keeps from its neighbours. Two adjacent
     sections each contribute this, so the gap a visitor sees between any two
     is twice the value: 200px on a desktop, 112px on a phone. One number
     changes the rhythm of the whole page. */
  --section-pad: clamp(3.5rem, 7vw, 6.25rem);
  --gutter: 2.5rem;
  /* 1280px is everon.one's content width: its Bootstrap container is
     1320px wide with 20px of padding, so its content spans 1280 and its logo
     starts 152px in at a 1584px viewport. Matching the number here rather
     than only on the bar keeps the nav aligned with the sections under it -
     widening the bar alone would leave the logo overhanging the content by
     50px on each side. Fluid below the cap, which Bootstrap is not; it steps
     at breakpoints instead. The desktop figure is the one being matched. */
  --shell: min(1280px, 100% - var(--gutter));
  --nav-h: 80px;  /* 8rem on everon.one, where the root is 10px */
}

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

html { scroll-behavior: smooth; }

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

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;

  /* The blue plate a phone paints over a tapped link or button. It is a
     browser default rather than a design, and everon.one does not have it
     only because Bootstrap's reboot switches it off on <body>. This site
     imports no Bootstrap, so it has to say so itself - without this the
     language button in the drawer flashed blue on every tap. The property
     inherits, so one declaration covers every control on the page. */
  -webkit-tap-highlight-color: transparent;

  /* clip, NOT hidden.
     overflow-x:hidden on body stops the overflow propagating to the viewport
     and makes BODY the scroll container instead. The page still scrolls and
     looks normal, but window.scrollY never moves, position:sticky sticks to
     the wrong scrollport, and anything bound to scroll progress - the process
     rail, the work theater - silently never advances.
     overflow-x:clip does the same clipping without creating a scroll
     container, which is what was wanted in the first place. */
  overflow-x: clip;
}

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
  margin: 0;
}

p { margin: 0; }
a { color: inherit; text-decoration: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }

:focus-visible {
  outline: 2px solid var(--accent-2);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Two details nobody names and everybody feels. */
::selection {
  background: var(--accent-fill);
  color: #fff;
}

* {
  scrollbar-width: thin;
  scrollbar-color: rgba(140, 170, 255, 0.22) transparent;
}

*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: rgba(140, 170, 255, 0.22);
  border-radius: 999px;
  border: 3px solid var(--ground);
}
*::-webkit-scrollbar-thumb:hover { background: rgba(140, 170, 255, 0.34); }

.sprite { display: none; }

/* ==========================================================================
   SCROLL REVEALS - THE PART THAT WAS MISSING

   The page always had reveals. They were invisible, and this is why: nothing
   was hidden BEFORE its turn came. Motion's animate() writes the from-value at
   the moment it runs, so an element that fades 0 -> 1 when it enters view is
   fully painted right up until that instant. inView fires at 15% visibility,
   which on a phone is a couple of hundred milliseconds after the block is
   already on screen and being read. The animation ran every time; there was
   simply never a frame where the element was not there.

   So: hide them up front, and let the reveal be the thing that puts them on
   the page.

   THE GUARD IS THE WHOLE DESIGN. This is scoped to a class that studio.js
   writes onto <html>, and it writes it only when Motion actually loaded AND
   the visitor has not asked for reduced motion. Without that, a stylesheet
   that arrives while the script does not - a blocked CDN, a parse error, a
   browser that refuses the module - would leave a page of invisible text with
   no way back. The unguarded version of this rule is the single most common
   way a site ships a blank page, and it fails silently for exactly the people
   least able to work around it.

   Opacity and nothing else. The lift belongs to the animation, which owns
   `transform` on these elements; putting a starting offset here as well would
   mean two properties describing the same movement, and while the element is
   at opacity 0 there is nothing for an offset to be seen doing anyway.
   ========================================================================== */
html.motion-ready .js-reveal,
html.motion-ready .js-bento-cell,
html.motion-ready .js-arrive {
  opacity: 0;
}

/* ==========================================================================
   THE LOADING SCREEN

   The storefront's, brought over unchanged in everything that shows: the same
   two shapes, the same 150x104 stage, the same 62px towers, the same white
   and #ccc, the same 3.6s orbit. It is the one piece of the two sites that a
   visitor moving between them sees twice in a row, so it is the one piece
   that has to be identical rather than merely consistent.

   The two shapes are the favicon's own paths - a solid tower and a lighter
   one - inlined in the HTML rather than linked, so the loading screen does
   not itself wait on a request, and so the two can be moved independently.

   The motion is a circle seen edge-on: horizontal position follows the sine
   of the angle, depth follows the cosine and is expressed as scale - largest
   at the near point, smallest at the far one. The depth ORDER changes with it,
   and that part is stepped rather than eased. There is no halfway between
   passing in front and passing behind, and easing it is exactly what makes
   this kind of animation read as two flat shapes sliding past each other
   rather than one object going around another.

   ONE THING IS DELIBERATELY NOT COPIED: the ground. The storefront paints
   #000 because its own body is white and the seam is unavoidable there. This
   site's page is --ground, so using --ground here means the screen does not
   flash a different black as it fades. Same design decision, different site.
   ========================================================================== */
#preloader {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: var(--ground);
  z-index: 9999;
}

#preloader .preloader-mark {
  position: relative;
  display: grid;
  width: 150px;
  height: 104px;
  place-items: center;
}

#preloader .preloader-tower {
  grid-area: 1 / 1;
  width: 62px;
  height: auto;
  overflow: visible;
  transform-origin: 50% 50%;
}

#preloader .preloader-tower--solid {
  fill: #ffffff;
  z-index: 1;
}

#preloader .preloader-tower--ghost {
  fill: #cccccc;
  animation:
    everonOrbitPath 3.6s cubic-bezier(0.45, 0, 0.55, 1) infinite,
    everonOrbitDepth 3.6s step-end infinite;
}

/* The orbit has to be centred on the SOLID tower, and the two shapes are not
   concentric: inside its own 256-unit viewBox the solid spans 0-176 and the
   ghost spans 96-256, so at the same size and position the ghost already sits
   about 21px to the right of the solid centre. Orbiting from where it lands
   naturally just wobbles it on the right-hand side and never takes it around.
   Every step below is that 21px correction plus the sine of the angle. */
@keyframes everonOrbitPath {
  /* near point - over the solid, largest, in front */
  0%   { transform: translateX(-21px) scale(1.14); opacity: 0.9; }
  /* swung out right, mid depth */
  25%  { transform: translateX(5px) scale(1); opacity: 0.8; }
  /* far point - over the solid, smallest, behind */
  50%  { transform: translateX(-21px) scale(0.76); opacity: 0.5; }
  /* swung out left */
  75%  { transform: translateX(-47px) scale(1); opacity: 0.8; }
  100% { transform: translateX(-21px) scale(1.14); opacity: 0.9; }
}

@keyframes everonOrbitDepth {
  0%   { z-index: 2; }
  25%  { z-index: 0; }
  75%  { z-index: 2; }
  100% { z-index: 2; }
}

/* A loading screen is the one place a visitor cannot navigate away from, so
   an animation someone asked not to see matters more here than anywhere
   else. The mark holds at the near point, the frame that reads best still. */
@media (prefers-reduced-motion: reduce) {
  #preloader .preloader-tower--ghost {
    animation: none;
    transform: translateX(-4px) scale(0.98);
    opacity: 0.75;
    z-index: 2;
  }
}

.ico {
  width: 1em;
  height: 1em;
  display: block;
  fill: currentColor;
  flex: none;
}

.shell {
  width: var(--shell);
  margin-inline: auto;
}

.eyebrow {
  font-family: var(--display);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.9rem;
}

.section {
  padding: var(--section-pad) 0;
  scroll-margin-top: var(--nav-h);
}

.section__head { max-width: 46rem; margin-bottom: clamp(2.5rem, 5vw, 3.5rem); }

.section__title { font-size: clamp(1.9rem, 4.2vw, 2.9rem); }

.section__lede {
  color: var(--muted);
  margin-top: 1rem;
  max-width: 60ch;
}

/* Skip link - the page is one long column, and a keyboard user should not have
   to tab the whole nav to reach it. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--ground);
  padding: 0.75rem 1.25rem;
  border-radius: 0 0 var(--radius) 0;
  font-weight: 600;
}
.skip:focus { left: 0; }

/* ==========================================================================
   Nav - the storefront's bar, in this site's colours

   Matched to everon.one by measurement rather than by eye: 8rem tall, sticky,
   a background that fades in over the first 220px of scroll, a 49px mark,
   1.5rem links, a 0.8rem/3rem button at 17px on a 16px radius.

   ONE FIGURE IS NO LONGER THE STOREFRONT'S: the width the drawer arrives at.
   It was 991.98px, Bootstrap's `lg`, so both bars changed shape together. It
   is 1279.98px here now, and the reason is arithmetic rather than taste - see
   the note above that media query.

   Built with this stylesheet rather than by importing Bootstrap, which is what
   the storefront uses for its offcanvas and its dropdown. Two components are
   not worth 80KB of framework on a page five megabytes were just taken out of,
   and the behaviour is a few dozen lines either way.
   ========================================================================== */
.nav {
  /*
   * everon.one's root, tracked across every width this bar is drawn at.
   *
   * It was declared inside the drawer's media query, where the drawer needed
   * it - so above 992px it did not exist and anything reaching for it fell
   * back to a hardcoded pixel value. That is exactly how the Hosting pill
   * ended up with the wrong padding: written as 7px 16px, which is the
   * storefront's 0.7rem/1.6rem at its 10px root and nothing like it at the
   * 14.4px root it uses above 1600px.
   */
  --ev-rem: 10px;        /* everon html { font-size: 62.5% } */

  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  min-height: var(--nav-h);
  padding: 10px 0;
  display: flex;
  align-items: center;
  /*
   * The storefront's surface, not a near-miss of it: pure black, fading in
   * over the first 220px of scroll. This was rgba(10,10,11) - four percent
   * lighter, invisible on its own and obvious the moment the two sites are
   * switched between in adjacent tabs, which is the test being applied here.
   *
   * The shadow is full whenever the bar is not at the top, as the storefront
   * does it, rather than fading with the background.
   */
  background: rgba(0, 0, 0, var(--nav-bg, 1));
  box-shadow:
    0 4px 15px rgba(255, 255, 255, 0.05),
    0 2px 6px rgba(0, 0, 0, 0.7);
  transition: background-color 280ms ease, box-shadow 280ms ease;
  /* body sets `antialiased`, which renders lighter than the storefront's
     `auto`. macOS only; a no-op on Windows, and free either way. */
  -webkit-font-smoothing: auto;
}
.nav.is-at-top { box-shadow: none; }

.nav__inner {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  width: var(--shell);
  margin-inline: auto;
}

.brand {
  display: inline-flex;
  align-items: center;
  margin-right: auto;
}

/* Sized by height so the artwork keeps its 1076x240 ratio at any nav height,
   and given an explicit width:auto so the intrinsic width attribute does not
   win. 49px is the storefront's figure. */
.brand__logo {
  height: 49px;
  width: auto;
  display: block;
}

/* ------------------------------------------------------------------- links */
.nav__menu {
  display: flex;
  align-items: center;
  /* The storefront runs its list straight into the aside - the <ul>'s right
     edge IS the language item's left edge, and the only air between them is
     the list's own pe-3. This had 34px on top of that. */
  gap: 0;
}

.nav__menuhead { display: none; }

.nav__links {
  display: flex;
  align-items: center;
  gap: 20px;
  padding-right: 10px;   /* everon pe-3 */
  font-family: var(--nav-font);
  font-size: 15px;      /* everon 1.5rem at its 10px root */
  color: var(--nav-text);
}
.nav__links a {
  padding: 5px;
  /* This site's body line-height is 1.65, which made the links 35px against
     the storefront's 33. The bar is the one place the two have to agree. */
  line-height: 1.5;
  transition: 0.3s linear;   /* everon transitions all properties */
}
.nav__links a:hover { color: var(--nav-text-hover); }

/* ------------------------------------------------------------ the ring pill
   The storefront marks its link to this site with a rotating gradient ring;
   this is that treatment pointing the other way.

   The ring is one pseudo-element, not a border and not a wrapper: inset 0 with
   a 1px pad, the fill masked away so only the padding band survives. That is
   what makes a gradient behave like a border without border-image.

   The angle needs @property to exist at all - a plain custom property is a
   string as far as animation is concerned, so it would sit at 0deg showing a
   lopsided gradient, which reads as a rendering fault rather than a design.
   The @supports below drops to a plain border where that is the case. */
@property --pill-ring-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.nav__links a.nav__pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* The storefront's 0.7rem/1.6rem, in its root rather than in ours: 7px/16px
     below 1600 and 10.08px/23.04px above it. Qualified with .nav__links so it
     outranks the plain link padding, which is what was winning before. */
  padding: calc(0.7 * var(--ev-rem)) calc(1.6 * var(--ev-rem));
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.035);
  color: #e9e9e9;
}

.nav__pill::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(
    from var(--pill-ring-angle),
    rgba(56, 130, 255, 0.75) 0deg,
    rgba(56, 130, 255, 0) 200deg,
    rgba(160, 210, 255, 0.95) 290deg,
    rgba(56, 130, 255, 0.75) 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  animation: pill-ring-spin 8s linear infinite;
}

@keyframes pill-ring-spin {
  to { --pill-ring-angle: 360deg; }
}

/* Brighter on hover, and the same speed: a ring that accelerates under the
   cursor reads as a glitch. */
.nav__pill:hover { color: #fff; background: rgba(255, 255, 255, 0.07); }
.nav__pill:hover::before {
  background: conic-gradient(
    from var(--pill-ring-angle),
    rgba(86, 155, 255, 1) 0deg,
    rgba(56, 130, 255, 0.25) 200deg,
    rgba(200, 228, 255, 1) 290deg,
    rgba(86, 155, 255, 1) 360deg
  );
}

@supports not (background: conic-gradient(from var(--pill-ring-angle), red, blue)) {
  .nav__pill::before {
    background: none;
    border: 1px solid rgba(120, 175, 255, 0.55);
    padding: 0;
    mask: none;
    -webkit-mask: none;
  }
}

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

.nav__aside {
  display: flex;
  align-items: center;
  gap: 10px;      /* everon gap-3, at its 10px root */
}

/* ---------------------------------------------------------------- language */
.lang {
  position: relative;
  display: flex;         /* no line box, so the panel offset is exact */
  padding: 5px;          /* everon p-2 - the wrapper carries it, not the button */
}

.lang__toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;      /* everon gap-3, at its 10px root */
  padding: 0;
  justify-content: space-between;
  background: none;
  border: 0;
  cursor: pointer;
  font: 400 15px/1.5 var(--nav-font);
  color: var(--nav-text);
  transition: color 0.3s linear;
  /* everon gets these from Bootstrap reboot; a <button> defaults to auto and
     would refuse to shrink below its content where its <a> would. */
  min-width: 0;
  min-height: 0;
}
.lang__toggle:hover { color: var(--nav-text-hover); }

/*
 * INLINE, not inline-flex, and that is the whole point of this rule.
 *
 * The storefront's globe is an <iconify-icon>, whose host style is
 * `display:inline-block; vertical-align:0`. It therefore sits IN the text run,
 * on the baseline, rather than being centred against it - which is why ours
 * was about a pixel high however carefully the sizes were matched. Making the
 * row inline again puts the glyph back on the baseline for nothing, and the
 * spacing then has to be a margin rather than a gap, which is also what the
 * storefront uses: me-2, plus a whitespace text node in its markup that
 * renders as a real space. Hence 8.8px and not 5.
 */
.lang__now { display: inline; }
.lang__now .ico {
  display: inline-block;
  vertical-align: -0.125em;   /* iconify's own host value */
  width: 1em;
  height: 1em;
  font-size: 1em;
  line-height: 1;
  margin-right: 5px;   /* everon me-2; the markup's whitespace adds the rest */
}
/* The caret stays a direct flex child of the toggle and so stays centred,
   which is what the storefront's caret does. */
.lang__caret {
  font-size: 1em;
  line-height: 1;
  transition: rotate 0.2s ease;
}
.lang.is-open .lang__caret { rotate: 180deg; }

/*
 * The storefront's dropdown, measured while open: as wide as its own wrapper
 * rather than a fixed 12rem, 6.25px below the button, a hairline of white at
 * 14 percent, a 10px radius, 10px of padding, and no shadow at all. It has no
 * entrance transition either - Bootstrap's .fast-animation sets a variable
 * this particular menu never reads.
 *
 * The background is rgba(20,20,20,0.96) and not the flat #141414 it was. Same
 * colour, four percent of the page showing through - which is small enough to
 * read as depth rather than as a translucent panel, and it is what
 * `.offcanvas .dropdown-menu` paints on the storefront at every width, not
 * only in the drawer. Measured there rather than guessed: the computed value
 * came back as rgba(20, 20, 20, 0.96) at 1700px and at 390px alike.
 */
.lang__menu {
  position: absolute;
  top: calc(100% + 1.25px);   /* 6.25 from the button, less the 5px padding */
  left: 0;
  right: 0;
  min-width: 100px;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(20, 20, 20, 0.96);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
  z-index: 3;
}
.lang.is-open .lang__menu {
  opacity: 1;
  pointer-events: auto;
}

.lang__menu a {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 10px;
  border-radius: 0;
  font-family: var(--nav-font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--nav-text);
  transition: color 0.3s linear, background-color 0.3s linear, transform 0.3s linear;
}
.lang__menu a:hover {
  color: var(--nav-text-hover);
  background: rgba(255, 255, 255, 0.08);
  border-radius: 8px;
  transform: translateX(3px);
}

/* The current language is whichever tree the page was built into, so the
   markup needs no state of its own. Weight alone marks it, as on the
   storefront: its own .lang-item--current blue loses on specificity to
   #main-header .dropdown-item, so what actually ships there is a bolder row
   in the same grey. */
html[lang="en"] .lang__menu a[data-lang-code="en"],
html[lang="de"] .lang__menu a[data-lang-code="de"],
html[lang="bs"] .lang__menu a[data-lang-code="bs"] {
  font-weight: 600;
}

/* --------------------------------------------------------------- the button
   The storefront's sign-up button, measured: 0.8rem by 3rem of padding, 17px,
   600, a 16px radius. White on black there; the accent here, because that is
   this site's colour. */
.nav__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 30px;
  /*
   * everon.one's button is 149.4px wide. Same padding and same radius here,
   * but "Hosting" is seven characters against "Client Area"'s eleven, so the
   * box came out 125.9 - visibly narrower beside it, and a 16px radius on a
   * narrower box also reads as rounder. The padding is not the difference and
   * increasing it would only move the number around, so the width is pinned
   * to the storefront's instead and the label centres inside it.
   *
   * A minimum, not a fixed width: German and Bosnian may set a longer word
   * here, and that must still fit rather than overflow.
   */
  min-width: 149px;
  border-radius: 16px;
  border: 1px solid var(--accent-fill);
  background: var(--accent-fill);
  color: #fff;
  font: 600 17px/1.5 var(--nav-font);
  white-space: nowrap;
  transition: background-color 0.25s ease, color 0.25s ease;
}
.nav__cta:hover {
  background: transparent;
  color: var(--accent);
}

/* ------------------------------------------------------- burger and drawer */
.nav__burger,
.nav__close {
  display: none;
  align-items: center;
  justify-content: center;
  /* 36x36 measured; 0.6rem on a 25px glyph makes the box 44. */
  padding: 0.6rem;
  background: none;
  border: 0;
  color: var(--ink);
  cursor: pointer;
  font-size: 25px;
}

.nav__scrim { display: none; }

/*
 * 1279.98px, and it used to be 991.98 - Bootstrap's `lg`, which is where the
 * storefront's own drawer appears. Both bars changed shape at the same width,
 * which was the point.
 *
 * SEVEN ITEMS DO NOT FIT AT 992, AND THE ROW HIDES IT.
 *
 * Home, About us, What we build, How we work, Work, Hosting and Contact. Flex
 * items shrink before they overflow, so a row that has run out of room does
 * not spill - it squeezes the words instead, and scrollWidth still reports the
 * same number as clientWidth while it happens. Nothing looks broken; the
 * labels just quietly get narrower than their text.
 *
 * So it was measured with flex-shrink off, in all three trees, at the widths
 * that matter (space needed against space available inside .nav__links):
 *
 *     viewport   English        German         Bosnian
 *     992        610 / 441      665 / 365      615 / 414     none of them fit
 *     1440       610 / 769      665 / 693      615 / 742     all fit
 *     1630       667 / 862      722 / 785      672 / 832     all fit
 *     1920       667 / 1110     722 / 1032     672 / 1079    all fit
 *
 * German is the binding case, as it usually is: it needs 665px against the
 * 365px a 992px viewport leaves once the mark and the button have taken
 * theirs. Closing the gaps entirely would recover 120px of the 300 needed, so
 * there is no tightening this into fitting - the row is simply too long for
 * that width, and it is too long in every language.
 *
 * The band that breaks is wider than it first looked, because the button
 * counts too: the German nav CTA read "Sprechen Sie uns an" at 219px against
 * the English 149, and every one of those pixels comes out of the row. It is
 * "Anfragen" in the bar now - the hero button below keeps the full sentence,
 * they are separate strings - which put 70px back.
 *
 * Even so, at a viewport of exactly 1200 the three languages came out at +39,
 * -7 and +19. Fitting by seven pixels is not fitting. Measured at 1280 they
 * are +103, +57 and +83, and 1280 is also where the shell stops growing, so
 * every width above it is roomier still. That is the edge, and the drawer has
 * everything below it.
 *
 * The storefront keeps 991.98. It has six shorter items and it fits.
 */
@media (max-width: 1279.98px) {
  .nav__burger { display: inline-flex; }

  /*
   * EVERON'S REM SYSTEM, REBUILT AS A VARIABLE.
   *
   * Every measurement in this drawer is one of the storefront's rem values,
   * and its root CHANGES inside the range the drawer covers: 62.5% down to
   * 991.98px, then 70% below 768px. So its p-4 is 15px on a small tablet and
   * 16.8px on a phone, and any fixed pixel value would be right at exactly one
   * width and wrong either side of it.
   *
   * --ev-rem carries that root, so everything below is written as the same
   * multiple that appears in the storefront's own markup - a p-4 there reads
   * as 1.5 here. This site's own root is 16px and stays that way; nothing
   * outside the drawer sees this.
   */
  .nav__menu {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    /* Full bleed, as the storefront's is. Bootstrap pins .offcanvas-end to a
       400px column and its header.css overrides that to 100%, because a
       frosted panel stopping short of the edge shows the page beside it. */
    width: 100%;
    max-width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0;               /* the head and the body carry their own */
    background: #000;         /* the fallback; see @supports below */
    border-left: 0;
    box-shadow: none;
    translate: 100% 0;
    transition: translate 0.3s ease-in-out;   /* Bootstrap offcanvas timing */
    overflow-y: auto;
    z-index: 60;
  }
  .nav.is-open .nav__menu { translate: 0 0; }

  /* Real glass, and only where it will render. Without the blur a translucent
     panel over the page is unreadable, which is why the colour above is solid
     and this is the override rather than the other way round. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .nav__menu {
      background: rgba(6, 8, 14, 0.62);
      -webkit-backdrop-filter: blur(26px) saturate(150%);
      backdrop-filter: blur(26px) saturate(150%);
    }
  }

  /* .offcanvas-header.p-4 */
  .nav__menuhead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: calc(1.5 * var(--ev-rem));
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  }
  /* Bootstrap's .btn-close is a 16px glyph inside 0.35em of padding - 27.2px
     square - and me-3 holds it that much clear of the header's own edge. */
  .nav__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* The glyph stays the storefront's 16px; the BOX grows to a thumb. */
    font-size: 16px;
    width: 44px;
    height: 44px;
    padding: 0;
    margin-right: calc(var(--ev-rem) - 8px);   /* keeps the glyph where it was */
    color: #fff;
  }

  /* .offcanvas-body (1rem of padding) holding .nav-item.px-3 (another 1rem):
     the text lands two units in from the panel edge, and the rows carry no
     rules between them - the storefront has none. */
  /*
   * display:contents, so Hosting can sit UNDER Talk to us.
   *
   * That is the storefront's arrangement for its own Studio button, and it is
   * awkward for the same reason there: Hosting lives inside this <nav> and the
   * CTA lives in the <div class="nav__aside"> that follows it, so putting one
   * below the other means ordering across that boundary - and `order` only
   * sorts siblings. Taking the <nav>'s own box away promotes its links to be
   * flex children of .nav__menu alongside that <div>, and then the whole lot
   * can be ordered together.
   *
   * The element stays in the tree and in the inheritance chain, so the font
   * size and colour below still reach the links; what it loses is its padding,
   * which the rows now carry themselves. It also degrades in the right
   * direction: a browser that does not support display:contents leaves the
   * menu exactly as it is today rather than scrambling it.
   */
  .nav__links {
    display: contents;
    font-size: calc(1.5 * var(--ev-rem));
  }
  .nav__links a {
    /* one unit from .offcanvas-body plus one from .nav-item.px-3 - the <nav>'s
       own padding, moved onto the rows now that the <nav> has no box */
    padding: calc(0.5 * var(--ev-rem)) calc(2 * var(--ev-rem));
    border-bottom: 0;
  }
  .nav__links a:first-child { margin-top: var(--ev-rem); }

  /*
   * Hosting, at the bottom. order:1 puts it after .nav__aside; everything
   * else here is the default 0, so nothing else needs an order of its own.
   *
   * Sized against .nav__cta rather than guessed at - the same padding, the
   * same 17px/600, the same 16px radius - plus one pixel of vertical padding,
   * because the CTA's height includes a 1px border and the ring here is a
   * pseudo-element that takes up no space at all. The horizontal margin lines
   * its edges up with the CTA's: .nav__aside contributes one unit of margin
   * and one of padding, so two units puts both buttons in the same column.
   */
  .nav__links a.nav__pill {
    order: 1;
    justify-content: center;
    margin: 0 calc(2 * var(--ev-rem)) calc(2 * var(--ev-rem));
    padding: calc(0.8 * var(--ev-rem) + 1px) calc(3 * var(--ev-rem));
    border-radius: 16px;
    font-size: 17px;
    line-height: 1.5;
    font-weight: 600;
    color: #fff;
  }

  /* The storefront's aside is d-flex flex-column gap-3 p-3 mt-3. */
  .nav__aside {
    flex-direction: column;
    align-items: stretch;
    gap: var(--ev-rem);
    margin: var(--ev-rem);      /* stands in for .offcanvas-body's padding */
    padding: var(--ev-rem);     /* the aside's own p-3 */
  }

  .lang {
    display: block;
    padding: calc(0.5 * var(--ev-rem));    /* .nav-item.dropdown.p-2 */
  }

  .lang__toggle {
    /* flex, not inline-flex: the storefront's control is a d-flex nav-link,
       so it is block-level and sits in no line box. Inline-flex left a line
       box under it and its descender space made the row 1.2px taller. */
    display: flex;
    align-items: center;
    width: 100%;
    min-height: 44px;      /* a thumb target, not a 25px line of text */
    justify-content: space-between;
    padding: 0;                            /* the wrapper carries it */
    font-size: calc(1.5 * var(--ev-rem));
  }
  /*
   * OVER the buttons, not pushing them down.
   *
   * This was position:static in the drawer, so opening it re-laid the column
   * out and shoved Talk to us and Hosting down the panel - the whole menu
   * jumped under the finger that opened it. The storefront's own picker is
   * absolutely positioned inside its offcanvas and simply covers what is
   * beneath it, and that is the behaviour worth copying: nothing else moves.
   *
   * The base rule already positions this against .lang, which is position
   * relative; all that is needed here is to stop overriding that, and to sit
   * above the two buttons underneath. z-index is needed even though both are
   * later in the DOM, because they are unpositioned and paint below anything
   * positioned with a z-index of its own.
   */
  .lang__menu {
    top: calc(100% + 0.4rem);
    opacity: 1;
    translate: 0 0;
    pointer-events: auto;
    box-shadow: none;   /* the storefront's has none either */
    z-index: 5;
    display: none;
  }
  .lang.is-open .lang__menu { display: block; }

  .nav__cta {
    width: 100%;
    padding: calc(0.8 * var(--ev-rem)) calc(3 * var(--ev-rem));
  }

  /*
   * :not([hidden]) is load-bearing, and its absence made the whole site
   * unusable on a phone.
   *
   * The scrim carries the `hidden` attribute and the script toggles it. But
   * `[hidden] { display: none }` lives in the USER AGENT stylesheet, and any
   * author rule beats the user agent regardless of specificity - so a bare
   * `.nav__scrim { display: block }` here switched it on permanently below
   * 992px. The result: a fixed rgba(4,6,10,0.62) sheet at z-index 55 over the
   * entire page, which is why every mobile screenshot looked dimmed, and
   * every tap - the burger included - landed on the scrim instead of on what
   * was under it. The scrim closes the drawer when clicked, so the menu could
   * never open: the click that should have opened it was being swallowed by
   * the thing that only exists while it is open.
   *
   * It never showed up in testing because the drawer was always opened from
   * script with .click(), which dispatches straight at the element and skips
   * hit-testing entirely.
   */
  .nav__scrim:not([hidden]) {
    display: block;
    position: fixed;
    inset: 0;
    /* Black at 0.35, not 0.62. The storefront lightened its own backdrop for
       exactly this reason: the panel in front is frosted, and dimming the page
       to near-black leaves the blur nothing to pick up. */
    background: #000;
    opacity: 0.35;
    z-index: 55;
  }

  /* The storefront's own mobile figure, so the two bars shrink together. */
  .brand__logo { height: clamp(31px, 9.75vw, 44px); }
}

/* Below 768px the storefront's root goes to 70%, and every drawer measurement
   above follows it without being restated. This is the one line that moves
   them: 1.5 units of header padding becomes 16.8px instead of 15, the links
   become 16.8px instead of 15, and so on down. */
@media (max-width: 767px) {
  .nav { --ev-rem: 11.2px; }        /* everon html { font-size: 70% } */
}

/* --------------------------------------------------------------- wide screens
   everon.one has a SECOND desktop layout, and this bar had never met it.

       @media (min-width: 1600px) {
         html { font-size: 90%; }                    <- 10px root becomes 14.4
         .main-container { width: 1700px; max-width: 90%; }
       }

   Above 1600px the storefront rescales its entire rem system by 1.44 and
   widens to 1700px. Its bar is 8rem: 80px below that breakpoint and 115.2px
   above it. Its links are 1.5rem: 15px below, 21.6px above.

   This site was matched against the storefront at a 1584px viewport - sixteen
   pixels under the breakpoint - so every number agreed on the test screen and
   none of them agreed on a real one. On a 2875px display the storefront was
   1700px wide with a 115px bar, and this was 1280px wide with an 80px bar.

   Two things do NOT scale over there, because they are px and not rem: the
   49px mark and the button's 17px label. Those already matched and are left
   alone. Everything below is the storefront's own measured figure at 2875px.
   ========================================================================== */

/* The other half of the drawer breakpoint above - these two must always be
   the same edge, or there is a band with neither a drawer nor a laid-out row. */
@media (min-width: 1280px) {
  /*
   * The storefront's list is `navbar-nav flex-grow-1 justify-content-center`:
   * it takes all the room between the mark and the aside and centres the links
   * inside it, which leaves the button hard against the right edge. This bar
   * had them bunched at the right instead - invisible at 1584px, where there
   * is no spare room to distribute, and plain at 2875px.
   */
  .nav__menu { flex: 1; }
  .nav__links { flex: 1; justify-content: center; }
}

@media (min-width: 1600px) {
  /* everon html { font-size: 90% } - the same jump the bar's type makes. */
  .nav { --ev-rem: 14.4px; }

  :root {
    --shell: min(1700px, 90%);
    --nav-h: 115.2px;   /* 8rem at the storefront's 14.4px root */
  }

  .nav { padding: 14.4px 0; }

  .nav__links { font-size: 21.6px; gap: 28.8px; }   /* everon 1.5rem at 14.4px */
  .nav__links a { padding: 7.2px; }

  .lang { padding: 7.2px; }
  .lang__toggle { font-size: 21.6px; gap: 14.4px; }
  .nav__links { padding-right: 14.4px; }
  .nav__aside { gap: 14.4px; }
  /* a margin now rather than a gap, because the row is inline so the glyph
     keeps the baseline; 1em still carries the icons themselves to 21.6px */
  .lang__now .ico { margin-right: 7.2px; }

  .lang__menu {
    top: calc(100% + 1.78px);   /* 8.98 from the button, less the 7.2px padding */
    min-width: 144px;
    padding: 14.4px;
  }
  .lang__menu a {
    padding: 10.08px 14.4px;
    gap: 17.28px;
    font-size: 21.6px;
  }

  /* Padding scales with the rem system; the 17px label and 16px radius do not.
     175.81px is the storefront's button at this width. */
  .nav__cta { padding: 11.52px 43.2px; min-width: 176px; }
}

@media (prefers-reduced-motion: reduce) {
  .nav,
  .nav__menu,
  .lang__menu,
  .lang__caret { transition: none; }
}

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  position: relative;
  /* The bar is out of flow now, so its height is the hero's to carry. */
  padding: calc(var(--nav-h) + clamp(2.5rem, 7vw, 5rem)) 0 clamp(3.5rem, 7vw, 5.5rem);
  overflow: hidden;
}

/* ---- the ground, breathing ----
   One bloom. The accent as light, never paint: enormous and very soft, so it
   never resolves into a shape, only into the ground being warmer where the
   argument is.

   It drifts AND it breathes, on two clocks that do not divide into each other -
   19s for the wander, 31s for the swell. Because they alternate, the real
   cycles are 38s and 62s, so the two only line up again after several minutes
   and the movement never settles into an obvious loop.

   That is done with the individual `translate` and `scale` properties rather
   than one `transform`, which is the only way two animations can drive a
   single element without overwriting each other. The centring stays in
   `transform`, so a browser that does not know the individual properties still
   places the bloom correctly and simply leaves it still.

   Composited, not repainted - nothing here touches layout or paint. */
.hero::before {
  content: "";
  position: absolute;
  top: -32%;
  left: 50%;
  width: min(1100px, 130%);
  aspect-ratio: 1;
  border-radius: 50%;
  pointer-events: none;
  will-change: translate, scale, opacity;

  /* centring only; the animations move it from here */
  transform: translate3d(-50%, 0, 0);

  background: radial-gradient(
    circle,
    rgba(56, 130, 255, 0.19) 0%,
    rgba(56, 130, 255, 0.06) 38%,
    transparent 68%
  );

  animation:
    bloom-drift 19s ease-in-out infinite alternate,
    bloom-breathe 31s ease-in-out infinite alternate;
}

@keyframes bloom-drift {
  0%   { translate: 0 0; }
  50%  { translate: 8% 5%; }
  100% { translate: -9% -4%; }
}

@keyframes bloom-breathe {
  0%   { scale: 0.86; opacity: 0.78; }
  100% { scale: 1.24; opacity: 1; }
}

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

.hero__inner {
  position: relative;
  text-align: center;
  max-width: 62rem;
  margin-inline: auto;
}

/* --- the shine badge ---
   The theme's ShineBorder, rebuilt in plain CSS: a radial gradient sweeping a
   1px masked border. mask-composite: exclude keeps only the border band, the
   same technique as the Studio nav ring already live on everon.one. */
.badge {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.5rem 1.1rem;
  border-radius: 999px;
  background: var(--surface);
  font-size: 0.875rem;
  color: var(--muted);
  isolation: isolate;
}

.badge::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: radial-gradient(circle at 50% 50%, transparent, transparent, var(--accent), var(--accent-2), var(--accent-3), transparent, transparent);
  background-size: 300% 300%;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: shine-pulse 14s linear infinite;
  pointer-events: none;
}

@keyframes shine-pulse {
  0% { background-position: 0% 0%; }
  50% { background-position: 100% 100%; }
  100% { background-position: 0% 0%; }
}

.badge__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
  flex: none;
}

.hero__title {
  font-size: clamp(2.6rem, 7.5vw, 5rem);
  margin: 1.75rem 0 0;
  letter-spacing: -0.035em;
}

.hero__lede {
  color: var(--muted);
  font-size: clamp(1.0625rem, 1.7vw, 1.25rem);
  max-width: 46rem;
  margin: 1.5rem auto 0;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  justify-content: center;
  margin-top: 2.25rem;
}

/* --- buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.8rem 1.6rem;
  border-radius: var(--radius);
  font-family: var(--display);
  font-weight: 500;
  font-size: 0.9375rem;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.25s ease, background-color 0.2s ease,
    border-color 0.2s ease, color 0.2s ease;
}

/* The accent as light, not paint: the button is white, the violet is the halo
   around it. Filling this with violet would make it a purple button, which is
   every AI-generated landing page ever made. */
.btn--primary {
  background: var(--ink);
  color: #101013;
  box-shadow: 0 0 0 1px rgba(56, 130, 255, 0.35), 0 10px 34px var(--glow);
}
.btn--primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 0 0 1px rgba(56, 130, 255, 0.55), 0 16px 46px rgba(56, 130, 255, 0.4);
}

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-strong);
}
.btn--ghost:hover {
  border-color: rgba(255, 255, 255, 0.32);
  background: rgba(255, 255, 255, 0.03);
}

.btn .ico { font-size: 1.1em; transition: transform 0.2s ease; }
.btn:hover .ico { transform: translateX(3px); }

@media (prefers-reduced-motion: reduce) {
  .btn:hover { transform: none; }
  .btn:hover .ico { transform: none; }
  .badge::before { animation: none; }
}

/* --- the four facts, on frosted glass ---
   Wider than the prose above it. The headline and lede are capped at 62rem so
   they keep a readable measure; a four-up strip has no such need and looks
   pinched at that width, so it is not held to the text column.

   IT TRACKS --shell, AND THAT MATTERS.

   This was a literal `min(1320px, 100% - var(--gutter))`, chosen when the shell
   was 1180 - the number existed to make the strip WIDER than everything around
   it. Then the shell became 1280, and 1700 above everon.one's 1600px
   breakpoint, and the fixed number quietly turned into the opposite of what it
   was for: on a wide screen the strip was the narrowest thing on the page,
   sitting 190px inside a bar and a set of sections that had moved out past it.

   Following the shell instead means it can never invert again, and on a 2875px
   screen it lines up edge for edge with the navbar and every section below it.
   The prose above stays at its own 62rem measure, so the strip is still the
   wider element - which was the whole point of the original number. */
.hero__facts {
  position: relative;
  width: var(--shell);
  margin: clamp(3rem, 6vw, 4.5rem) auto 0;
}

/*
 * Real frosted glass, not a grey panel pretending.
 *
 * backdrop-filter blurs whatever is BEHIND the element, and what is behind this
 * happens to be the two drifting blooms - so the glass picks up their colour
 * and shifts with them instead of sitting on top of the hero as a separate
 * object. The saturate() is what stops the blur going milky.
 *
 * The 1px gap plus a translucent parent background draws the dividers, and the
 * inset highlight along the top edge is the lit rim that reads as thickness.
 */
.facts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.13);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 14px 34px rgba(0, 0, 0, 0.45);
  -webkit-backdrop-filter: blur(9px) saturate(135%);
  backdrop-filter: blur(9px) saturate(135%);
}

.fact {
  display: flex;
  gap: clamp(0.85rem, 1vw, 1.15rem);
  /* Centred against the whole text block, not aligned to its first line. */
  align-items: center;
  /* Trimmed from a flat 1.6rem/1.45rem: the icon took width the text needs
     back, and padding is the cheapest place to find it. */
  padding: clamp(1.3rem, 1.5vw, 1.7rem) clamp(1.1rem, 1.25vw, 1.45rem);
  /* Translucent, or there would be nothing for the blur to show through. */
  background: rgba(18, 18, 22, 0.34);
  text-align: left;
}

/* Without backdrop-filter a translucent panel over a moving gradient is just
   unreadable, so those browsers get the solid card the strip used to be. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .facts { background: var(--line); }
  .fact { background: var(--surface); }
}

/*
 * The glyph inside is `.ico`, sized 1em square - so this font-size IS the icon
 * size. It was 1.375rem (22px); the floor here is exactly double that, and it
 * grows a little further on wide screens.
 *
 * `grid` rather than the default inline: an inline box carries line-height,
 * which would pad the span taller than the glyph and defeat the centring it is
 * being asked to do. `flex: none` because a 48px icon is the one thing in this
 * card that must never be squeezed to buy the text room.
 */
.fact__icon {
  font-size: clamp(2.75rem, 2.1rem + 1.1vw, 3.15rem);
  color: var(--accent);
  display: grid;
  place-items: center;
  flex: none;
}

.fact__body { font-size: 0.875rem; line-height: 1.55; }
.fact__body strong {
  display: block;
  font-family: var(--display);
  font-weight: 500;
  font-size: 0.9375rem;
  color: var(--ink);
  letter-spacing: -0.01em;
}
.fact__body span { color: var(--faint); }

/*
 * Four, two, one - never three.
 *
 * There are four cards, so `auto-fit` is the wrong tool here: the moment it
 * decides on three columns it strands the fourth alone on a row of its own.
 * Explicit steps that halve keep every row full.
 *
 * The four-across step used to run down to 900px. A doubled icon eats roughly
 * 40px of every card, and at 900px that left the text about 110px to wrap
 * inside - four and five line paragraphs. Dropping to two columns at 1080px
 * hands each card ~2.2x the width instead, which is where the extra icon
 * width comes from.
 */
@media (max-width: 1080px) {
  .facts { grid-template-columns: repeat(2, 1fr); }
}
/* Single file earlier than the old 520px, for the same reason as above: two
   columns below ~680px leaves about 160px to wrap in, while one column at the
   same width gives 256px and a shorter card. Two narrow columns are not worth
   more than one comfortable one. */
@media (max-width: 680px) {
  .facts { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Intro
   ========================================================================== */
/*
 * The heading spans the section; the photograph and the prose sit beside each
 * other under it.
 *
 * They used to share a column - heading, then photograph, with the prose in the
 * other one - so the prose began level with the HEADING and the photograph
 * began 196px lower. On a 1280px shell that was survivable. At 1700 the prose
 * ran out 325px before the photograph did and left a column of nothing beside
 * it. Lifting the heading out makes the two blocks start on the same line, and
 * centring them keeps the shorter one level rather than stranded at the top.
 */
.intro__head {
  max-width: 46rem;    /* the same measure every other section head keeps */
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.intro__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

/* The top margin existed to hold the photograph off the heading above it.
   Nothing is above it now. */
.intro__grid .shot { margin-top: 0; }

.intro__prose { color: var(--muted); font-size: 1.0625rem; }
.intro__prose p + p { margin-top: 1.1rem; }
.intro__prose a {
  color: var(--ink);
  border-bottom: 1px solid var(--accent);
  padding-bottom: 1px;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.intro__prose a:hover { color: var(--accent); border-color: var(--accent-2); }

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

/* ==========================================================================
   The six areas - bento
   ========================================================================== */
.bento {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
}

/* Two cells are wider than the rest. Not decoration: web/mobile apps and cloud
   engineering are the work that pays for the studio, and the grid says so. */
.bento__cell:nth-child(4),
.bento__cell:nth-child(5) { grid-column: span 3; }

@media (min-width: 861px) {
  .bento__cell:nth-child(4) { grid-column: span 2; }
  .bento__cell:nth-child(5) { grid-column: span 1; }
}

@media (max-width: 860px) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bento__cell:nth-child(4),
  .bento__cell:nth-child(5) { grid-column: span 2; }
}

@media (max-width: 560px) {
  .bento { grid-template-columns: 1fr; }
  .bento__cell:nth-child(4),
  .bento__cell:nth-child(5) { grid-column: span 1; }
}

.bento__cell {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 1.75rem;
  border-radius: var(--radius-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  overflow: hidden;
  transition: border-color 0.3s ease, background-color 0.3s ease, transform 0.3s ease;
}

/* A very faint sheen along the top edge - the machined-edge cue, at 1px. */
.bento__cell::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.14), transparent);
}

.bento__cell:hover {
  border-color: var(--line-strong);
  background: var(--surface-2);
  transform: translateY(-3px);
}

/* The card is wide enough now for these to share a line rather than stack. */
.bento__head {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin-bottom: 0.9rem;
}

.bento__icon {
  font-size: 1.75rem;
  color: var(--accent);
  flex: none;              /* never squeezed by a title that wraps */
  transition: transform 0.3s ease;
  transform-origin: center;
}
.bento__cell:hover .bento__icon { transform: scale(0.86); }

.bento__title {
  font-size: 1.1875rem;
  font-weight: 500;
  margin: 0;
}

.bento__lede {
  color: var(--muted);
  font-size: 0.9375rem;
  line-height: 1.6;
}

.bento__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 1.1rem;
}
.bento__chips li {
  font-size: 0.75rem;
  color: var(--faint);
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.2rem 0.65rem;
}

/* The audience line. It fades in on hover on pointer devices - but on touch,
   where there is no hover, it is simply always visible. Content that only
   exists on hover does not exist on half the devices that will see this page. */
.bento__for {
  margin-top: auto;
  padding-top: 1.1rem;
  font-size: 0.8125rem;
  color: var(--faint);
  line-height: 1.5;
}
.bento__for strong {
  color: var(--accent);
  font-weight: 500;
}

@media (hover: hover) and (pointer: fine) {
  .bento__for {
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 0.3s ease, transform 0.3s ease;
  }
  .bento__cell:hover .bento__for,
  .bento__cell:focus-within .bento__for {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bento__cell:hover { transform: none; }
  .bento__cell:hover .bento__icon { transform: none; }
  .bento__for { opacity: 1 !important; transform: none !important; }
}

/* ==========================================================================
   Process - the scroll-linked rail
   ========================================================================== */
/*
 * THE RAIL IS SEGMENTED, NOT MASKED.
 *
 * It used to be one bar running the full height behind everything, and the
 * numbers are translucent, so the line was plainly visible straight through
 * every circle it passed. Punching holes in a continuous bar would mean a
 * mask, and a mask cannot be written in CSS without knowing where each circle
 * has landed - which only JavaScript can measure, and only after layout.
 *
 * So each step draws its own piece of the line instead: from just below its
 * own number to just above the next one. That geometry is exact and entirely
 * declarative, because a step already knows the two things it needs - its own
 * height, and the gap to its neighbour.
 *
 *   segment top    = --no-size + --rail-gap        (clears its own circle)
 *   segment height = 100% + --step-gap             (down to the next circle)
 *                  - --no-size - 2 * --rail-gap    (minus both clearances)
 *
 * --rail-gap is therefore two things at once: the breathing room around each
 * circle, and exactly how much a circle grows when the line arrives at it. A
 * lit circle gains --rail-gap on every side and so closes the gap precisely,
 * touching the line without ever overlapping it.
 */
.process {
  --no-size: 32px;
  --rail-gap: 6px;
  --step-gap: clamp(1.75rem, 3.5vw, 2.75rem);

  position: relative;
  display: grid;
  /* Narrow enough that the deck fills it. The frame is capped at 68vh, so
     on an ordinary screen a wider column would only add empty gutters
     either side of a deck that had already stopped growing. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
  gap: clamp(2rem, 4vw, 3.75rem);
  /*
   * Centred, not top-aligned. These two columns cannot be made the same height
   * at every width and it is a mistake to try: the deck is capped by its own
   * aspect ratio against the column width, while the steps grow taller as that
   * column narrows and the text wraps more. They cross over somewhere around
   * 1600px, so whichever is chased ends up wrong on the other side of it.
   *
   * Centring means the shorter column is never stranded against the top with a
   * void under it, which is what actually read as misalignment.
   */
  align-items: center;
}

.process__list {
  display: grid;
  gap: var(--step-gap);
  padding-left: 3.25rem;
}

.step { position: relative; }

/* The dim segment, and the bright one that fills over it. Same geometry, so
   they are declared together and differ only in paint. */
.step::before,
.step::after {
  content: "";
  position: absolute;
  /* Centred on the circle: its left edge, plus half its width, less half the
     line's own width. */
  left: calc(-3.25rem + var(--no-size) / 2 - 1px);
  top: calc(var(--no-size) + var(--rail-gap));
  height: calc(100% + var(--step-gap) - var(--no-size) - 2 * var(--rail-gap));
  width: 2px;
  border-radius: 2px;
}

.step::before { background: var(--line); }

.step::after {
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
  box-shadow: 0 0 14px var(--glow);
  transform-origin: top center;
  /* Full by default. The empty state exists only once JS is driving, exactly
     as with the hero reveals: nothing is hidden by a stylesheet that might be
     the last thing standing. */
  transform: scaleY(var(--seg, 1));
}

/* The last step has nothing to connect to. */
.step:last-child::before,
.step:last-child::after { display: none; }

.step__no {
  position: absolute;
  left: -3.25rem;
  top: 0;
  width: var(--no-size);
  height: var(--no-size);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  font-family: var(--display);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  z-index: 1;
}

.step__head {
  display: flex;
  align-items: baseline;
  gap: 0.85rem;
  flex-wrap: wrap;
  margin-bottom: 0.4rem;
}

.step__title { font-size: 1.1875rem; font-weight: 500; }

/* Durations are the honest part of a process section - anyone can list five
   steps. Monospace and tabular so the numbers line up down the page. */
.step__dur {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.75rem;
  color: var(--accent);
  background: rgba(56, 130, 255, 0.1);
  border: 1px solid rgba(56, 130, 255, 0.22);
  border-radius: 999px;
  padding: 0.1rem 0.6rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.step__text {
  color: var(--muted);
  font-size: 0.9375rem;
  max-width: 56ch;
}

/* ==========================================================================
   The case - border beam
   ========================================================================== */
.case {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: center;
  padding: clamp(2rem, 4vw, 3.25rem);
  border-radius: var(--radius-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  overflow: hidden;
}

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

/* The theme's BorderBeam: a small gradient square travelling the perimeter on
   an offset-path. The page's only perpetual animation, spent on the one thing
   that has earned the attention. offset-path is not universal, so the card is
   already complete without it. */
.case__beam {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid transparent;
  -webkit-mask: linear-gradient(transparent, transparent), linear-gradient(#fff, #fff);
  -webkit-mask-clip: padding-box, border-box;
  -webkit-mask-composite: source-in;
  mask: linear-gradient(transparent, transparent), linear-gradient(#fff, #fff);
  mask-clip: padding-box, border-box;
  mask-composite: intersect;
  pointer-events: none;
}

@supports (offset-path: rect(0 auto auto 0 round 180px)) {
  .case__beam::after {
    content: "";
    position: absolute;
    aspect-ratio: 1;
    width: 180px;
    background: linear-gradient(to left, var(--accent), var(--accent-2), transparent);
    offset-anchor: 90% 50%;
    offset-path: rect(0 auto auto 0 round 180px);
    animation: border-beam 14s linear infinite;
  }
}

@keyframes border-beam {
  100% { offset-distance: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .case__beam::after { animation: none; opacity: 0.5; }
}

.case__title {
  font-size: clamp(1.4rem, 2.6vw, 1.85rem);
  margin-bottom: 0.9rem;
}
.case__text { color: var(--muted); max-width: 54ch; }

.case__meta {
  display: grid;
  gap: 0.75rem;
  margin: 0;
}
.case__meta > div {
  padding: 0.9rem 1.1rem;
  border-radius: var(--radius);
  background: var(--ground);
  border: 1px solid var(--line);
}
.case__meta dt {
  font-family: var(--display);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 0.25rem;
}
.case__meta dd {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--ink);
}
.case__meta dd a {
  border-bottom: 1px solid var(--accent);
  transition: color 0.2s ease;
}
.case__meta dd a:hover { color: var(--accent); }

/* ==========================================================================
   CTA
   ========================================================================== */
/* ==========================================================================
   Footer
   ========================================================================== */
.footer {
  border-top: 1px solid var(--line);
  padding: 2.5rem 0 3.5rem;
  margin-top: clamp(4rem, 8vw, 6rem);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 2.5rem;
  align-items: center;
  justify-content: space-between;
  font-size: 0.875rem;
  color: var(--faint);
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}
.footer__links a {
  /* 23px tall measured on a phone; WCAG 2.2 asks 24 and a thumb wants more. */
  display: inline-block;
  padding-block: 0.5rem;
  transition: color 0.2s ease;
}
.footer__links a:hover { color: var(--ink); }


/* ==========================================================================
   The holographic headline — liquid mercury

   Hairline letters that never sit still: the hue rotates continuously, the
   interference bands breathe, and where the pointer rests the whole film beads
   up into a compact silver pool the way mercury does.

   Rebuilt rather than embedded from unicorn.studio. That snippet pulls a WebGL
   runtime off a CDN and fetches the scene by project id, so the effect would
   last exactly as long as someone else runs that service — and it would be the
   only third-party request on a site that deliberately has none.

   WHY IT IS ONE ELEMENT AND NOT TWO STACKED COPIES OF THE TEXT

   A text stroke and a background are painted separately, so `color:
   transparent` plus `background-clip: text` shows the gradient through the
   glyph shapes while `-webkit-text-stroke` keeps the outline lit everywhere.
   The usual two-copies approach needs the second copy in a data-text
   attribute, which the extractor does not read — the German and Bosnian
   headlines would have gone untranslated in the copy people actually see.

   THREE ANIMATED VALUES, ALL REGISTERED

   A custom property can only be interpolated once the browser knows its type,
   which is what @property is for. Registering them is what lets the hue rotate
   in CSS and the bead collapse on a transition instead of snapping.
   ========================================================================== */

@property --mx {
  syntax: "<length-percentage>";
  initial-value: 50%;
  inherits: false;
}
@property --my {
  syntax: "<length-percentage>";
  initial-value: 42%;
  inherits: false;
}
/* Turns forever. Every band reads its hue as an offset from this, so the whole
   film shifts together the way an oil slick does. */
@property --holo-hue {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
/* Breathes, quicker than the hue turns, which is what makes the surface look
   agitated rather than merely tinted. */
@property --holo-phase {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}
/* 1 while the pointer moves, easing to 0 when it stops. Everything that makes
   mercury look like mercury hangs off this: radii shrink, bands compress, and
   the colours draw together into a bead. Written by studio.js. */
@property --holo-spread {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

/*
 * The guard is load-bearing. Outside it the headline is ordinary filled white
 * text, because a browser that honoured `color: transparent` but not the
 * stroke would render the most important sentence on the page as nothing.
 */
@supports ((-webkit-background-clip: text) or (background-clip: text)) and
          (-webkit-text-stroke: 1px #fff) {
  .hero__title {
    /* Radii are LENGTHS. radial-gradient(circle <percentage>) is invalid —
       percentages are only legal for ellipse — and one bad token drops the
       entire background-image declaration. It computed to `none` once already
       and the headline rendered as bare outlines with no bloom at all, while
       every @supports test passed and getComputedStyle looked perfectly
       healthy. In em, so the bead scales with the headline across its clamp. */
    --holo-core: calc(0.4em + var(--holo-spread) * 0.32em);
    --holo-r: calc(2em + var(--holo-spread) * 1.5em);
    --holo-r2: calc(1.4em + var(--holo-spread) * 1.1em);
    /* Bands sit tight together when still and pull apart while moving. */
    --holo-band: calc(7% + var(--holo-spread) * 9% + var(--holo-phase) * 5%);

    color: transparent;

    /* A hairline, so the letters read as an etched edge rather than an
       outline. The plain value is declared first so a browser without max()
       keeps a visible stroke instead of dropping the declaration and rendering
       nothing at all. */
    -webkit-text-stroke-width: 0.9px;
    -webkit-text-stroke-width: max(0.65px, 0.011em);
    -webkit-text-stroke-color: rgba(255, 255, 255, 0.92);

    -webkit-background-clip: text;
    background-clip: text;
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size: 100% 100%;

    /*
     * Top to bottom: the specular bead, then two interference films offset
     * from one another. The offset is the point — mercury is silver with
     * colour fringes, not a rainbow, so each film is threaded with near-white
     * and the two disagree slightly about where their bands fall.
     */
    background-image:
      radial-gradient(
        circle var(--holo-core) at var(--mx) var(--my),
        rgba(255, 255, 255, 0.99) 0%,
        rgba(226, 236, 255, 0.72) 48%,
        rgba(255, 255, 255, 0) 100%
      ),
      radial-gradient(
        circle var(--holo-r) at calc(var(--mx) - 1.5%) calc(var(--my) + 2%),
        hsl(calc(var(--holo-hue) + 190deg) 100% 86% / 0.95) 0%,
        rgba(245, 250, 255, 0.9) calc(var(--holo-band) * 0.9),
        hsl(calc(var(--holo-hue) + 300deg) 100% 76% / 0.92) calc(var(--holo-band) * 1.9),
        rgba(238, 244, 255, 0.72) calc(var(--holo-band) * 2.7),
        hsl(calc(var(--holo-hue) + 45deg) 100% 74% / 0.88) calc(var(--holo-band) * 3.6),
        hsl(calc(var(--holo-hue) + 150deg) 96% 70% / 0.55) calc(var(--holo-band) * 4.6),
        transparent 82%
      ),
      radial-gradient(
        circle var(--holo-r2) at calc(var(--mx) + 3%) calc(var(--my) - 4%),
        rgba(255, 255, 255, 0.5) 0%,
        hsl(calc(var(--holo-hue) + 260deg) 100% 80% / 0.7) calc(var(--holo-band) * 1.4),
        rgba(230, 240, 255, 0.45) calc(var(--holo-band) * 2.4),
        hsl(calc(var(--holo-hue) + 95deg) 100% 72% / 0.6) calc(var(--holo-band) * 3.4),
        transparent 76%
      );

    /* Never still. The hue turn is the slow one, the band breath is quick, and
       together they read as a live surface rather than a moving gradient. */
    animation:
      holo-hue 5.5s linear infinite,
      holo-breathe 2.1s ease-in-out infinite alternate;

    /* Surface tension. When studio.js drops --holo-spread to 0 the bead does
       not snap shut, it flows together — the long tail on this easing is the
       whole mercury impression. */
    transition: --holo-spread 620ms cubic-bezier(0.16, 1, 0.3, 1);
  }

  @keyframes holo-hue {
    to { --holo-hue: 360deg; }
  }

  @keyframes holo-breathe {
    from { --holo-phase: 0; }
    to   { --holo-phase: 1; }
  }

  /* No pointer to follow, so the bead wanders by itself and a phone gets the
     effect instead of a hollow headline. */
  @media (hover: none) {
    .hero__title {
      animation:
        holo-hue 5.5s linear infinite,
        holo-breathe 2.1s ease-in-out infinite alternate,
        holo-drift 11s ease-in-out infinite alternate;
    }
  }

  @keyframes holo-drift {
    from { --mx: 20%; --my: 32%; }
    to   { --mx: 80%; --my: 60%; }
  }

  /* Reduced motion keeps the iridescence and stops every bit of movement: the
     bead rests off-centre, which still reads as lit metal. */
  @media (prefers-reduced-motion: reduce) {
    .hero__title {
      animation: none;
      transition: none;
      --mx: 38%;
      --my: 44%;
      --holo-hue: 210deg;
      --holo-phase: 0.5;
      --holo-spread: 0.35;
    }
  }
}

/* ==========================================================================
   The blue fire

   The same Unicorn Studio scene the storefront hero runs, self-hosted here as
   well - runtime, scene and noise texture all served from this origin. Checked
   before it was copied: it renders with every external host blocked, and a
   network log of the live storefront shows the only host it contacts is
   everon.one. The unicorn.studio and googleapis URLs inside the runtime are
   editor and fallback paths that never fire.

   The scene is authored at 1440x900 and is scaled to COVER the hero from
   studio.js, the same way the storefront does it - a shader canvas cannot be
   told to behave like background-size: cover, so the maths has to be done for
   it and re-done on resize.
   ========================================================================== */
.hero-fire {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms ease;
}

/* Only once the scene has actually reported ready. A canvas that failed to
   initialise stays invisible and the CSS bloom underneath carries the hero. */
.hero--fire .hero-fire { opacity: 1; }

.hero-fire > div[data-us-project-src] {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1440px;
  height: 900px;
  transform: translate(-50%, -50%) scale(var(--hero-fire-scale, 1));
  transform-origin: center center;
}

/* The bloom becomes the fallback rather than the design. It is what shows
   while the scene loads, and what stays if WebGL is unavailable, scripting is
   off, or the visitor asked for reduced motion. */
.hero--fire::before {
  opacity: 0;
  transition: opacity 900ms ease;
}

/* Content sits above both layers. .hero__inner and .hero__facts are already
   positioned, so without this the fire would paint over the headline. */
.hero__inner,
.hero__facts { z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  /* Never started, so nothing to hide - but be explicit, because the scene is
     a continuously animating shader and that is precisely what was opted out
     of. */
  .hero-fire { display: none; }
  .hero--fire::before { opacity: 1; }
}

/* ---- blueprint grid ----
   A faint dot lattice behind the sections that are about how the work is made.
   Blue at 4%, 28px pitch, and masked so it fades out at the top and bottom
   edges rather than stopping on a hard line. Texture that says engineering
   studio without saying anything.

   On ::before so it sits under the content, and pointer-events:none so it can
   never intercept a click.

   THE SWELL IS A MASK, WHICH IS WHY THE COLOUR DOES NOT MOVE.

   The first attempt at animating this laid a second layer of blue gradients
   over the lattice and drifted them across. It did move, but what you saw was
   the dots turning a stronger blue rather than a wave going through them,
   because adding light adds colour - that is what adding light does.

   So nothing is added here. The dots are the same rgba(140,170,255,0.16) they
   have always been, and what travels is a hole in their own mask: a soft
   ellipse that runs diagonally across the section, taking the lattice from
   about a third of its strength up to full and back down as it passes. Only
   brightness changes, so the colour is exactly the colour it was.

   opacity sits at 0.85 rather than 0.55 because the swell now multiplies it:
   0.85 through a mask averaging around two thirds lands on the old 0.55, with
   the crest brighter and the trough fainter either side of it.

   mask-position has to be what animates. A transform would carry the mask and
   the background together and slide the dots themselves, which is a drifting
   lattice and not this. That means it repaints rather than composites, so it
   is one element and desktop only - measured at 180fps with it running and
   180fps with it off. */
.has-grid { position: relative; }

.has-grid::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(140, 170, 255, 0.16) 1px, transparent 1px);
  background-size: 28px 28px;
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent);
  opacity: 0.55;
}

@media (min-width: 861px) {
  .has-grid::before {
    opacity: 0.85;
    -webkit-mask-image:
      linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent),
      radial-gradient(58% 46% at 50% 50%, #000 0%, rgba(0, 0, 0, 0.62) 52%, rgba(0, 0, 0, 0.3) 100%);
    mask-image:
      linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent),
      radial-gradient(58% 46% at 50% 50%, #000 0%, rgba(0, 0, 0, 0.62) 52%, rgba(0, 0, 0, 0.3) 100%);
    -webkit-mask-size: 100% 100%, 165% 150%;
    mask-size: 100% 100%, 165% 150%;
    -webkit-mask-repeat: no-repeat, no-repeat;
    mask-repeat: no-repeat, no-repeat;
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
    animation: grid-swell 28s ease-in-out infinite alternate;
  }
}

@keyframes grid-swell {
  from {
    -webkit-mask-position: 0 0, 8% 18%;
    mask-position: 0 0, 8% 18%;
  }
  to {
    -webkit-mask-position: 0 0, 92% 82%;
    mask-position: 0 0, 92% 82%;
  }
}

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

/* ==========================================================================
   The Work Theater — stacked form

   Everything here is the FALLBACK, and it is designed rather than left over:
   this is what a visitor gets with no JavaScript, on a phone, or having asked
   for reduced motion. The pinned theater in the next block is an enhancement
   layered on top of exactly this markup.
   ========================================================================== */
/* The stage now opens with the section heading, so the space that used to sit
   between the two belongs inside it. */
.work__stage { margin-top: 0; }

.work__inner {
  display: grid;
  gap: clamp(2rem, 4vw, 3rem);
}

.work__list {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
}

.work__item {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(1.75rem, 4vw, 3rem);
  align-items: center;
}

/* Alternating sides stop three big cards reading as a list of the same thing. */
.work__item:nth-child(even) .work__visual { order: 2; }

/* ---------------------------------------------------------- the phone form
   Five projects stacked vertically is five screenfuls of scrolling before the
   next section, and every one of them looks like the one above it. Sideways
   instead: one project at a time, swiped.

   THE PEEK IS THE INSTRUCTION. Nothing on a touch screen says "this scrolls" -
   there is no cursor to change and no scrollbar to see. So the next card is
   deliberately left hanging off the right edge, cut by the screen and faded
   back. A slice of something you cannot read is a question, and swiping is the
   obvious way to answer it. That is why the slide is 86% and not 100%: the
   remaining 14% is the whole affordance.

   Scroll-snap does the movement, so the finger stays in charge - momentum,
   rubber-banding and interruption all behave the way the platform does them,
   which is not true of anything driven by a script. */
@media (max-width: 900px) {
  .work__item { grid-template-columns: 1fr; }
  .work__item:nth-child(even) .work__visual { order: 0; }

  .work__list {
    display: flex;
    align-items: start;   /* the cards are different heights; do not centre them */
    gap: 14px;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;   /* a swipe past the end must not navigate back */
    -webkit-overflow-scrolling: touch;

    /*
     * Full bleed. The shell keeps half a gutter on each side, so pulling the
     * scroller out by that much and paying it back as padding lets a card run
     * to the actual edge of the screen while the FIRST card still lines up
     * with the heading above it. Without this the peek would stop at the
     * content edge and read as a layout mistake rather than an invitation.
     */
    margin-inline: calc(var(--gutter) / -2);
    padding-inline: calc(var(--gutter) / 2);
    scroll-padding-inline: calc(var(--gutter) / 2);

    /* room for the rim glow, which would otherwise be clipped: a scroller on
       one axis forces the other to compute to auto, and hidden is the only way
       to stop a stray vertical scrollbar appearing inside the strip. */
    padding-block: 6px;

    scrollbar-width: none;
  }
  .work__list::-webkit-scrollbar { display: none; }

  .work__item {
    flex: 0 0 min(86%, 420px);
    scroll-snap-align: start;
    scroll-snap-stop: always;   /* one card per swipe, never two */

    /* Faded and a shade smaller until it is the one being read. The class is
       set by studio.js from scroll position; with no script every card simply
       stays at full strength and the carousel still works. */
    opacity: 0.42;
    scale: 0.97;
    transform-origin: 50% 40%;
    transition: opacity 0.45s cubic-bezier(0.16, 1, 0.3, 1),
                scale 0.45s cubic-bezier(0.16, 1, 0.3, 1);
  }
  .work__item.is-current {
    opacity: 1;
    scale: 1;
  }

  /* The meter is turned on further down the file - see the note there. */

  /* A scrollable region has to be reachable by keyboard; studio.js adds the
     tabindex, and this is the ring for it. */
  .work__list:focus-visible {
    outline: 2px solid var(--accent-2);
    outline-offset: 4px;
    border-radius: var(--radius);
  }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  /* Keep the carousel - it is the layout, not an effect - but stop it moving
     under anyone who asked for less of that, and stop dimming the neighbours,
     since a fade they cannot see resolve is just low contrast. */
  .work__item {
    opacity: 1;
    scale: 1;
    transition: none;
  }
  .work__rail-fill { transition: none; }
}

/*
 * The figure is now only a box to hold a device in - and, once the theater is
 * running, the perspective that lets that device turn. Everything that used to
 * make it look like a card moved onto .work__device, which is the thing that
 * actually rotates.
 */
.work__visual {
  position: relative;
  margin: 0;
}

.work__device {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  background: #0e1218;
  box-shadow: 0 26px 64px rgba(0, 0, 0, 0.6);
}

.work__chrome {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.6rem 0.85rem;
  background: rgba(255, 255, 255, 0.04);
  border-bottom: 1px solid var(--line);
}
.work__chrome i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
}
.work__url {
  margin-left: 0.6rem;
  font: 400 0.6875rem/1 ui-monospace, "Cascadia Mono", Consolas, monospace;
  color: var(--faint, #6b7280);
  background: rgba(255, 255, 255, 0.05);
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
}
/* Drawn from the attribute so the address is never a translatable string. */
.work__url::before { content: attr(data-url); }

/* The screenshots' own proportions, so nothing is cropped: they are captured
   at 1584x849, which is the viewport the sites were photographed in. */
.work__screen { position: relative; aspect-ratio: 1584 / 849; }

.work__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The light that crosses the glass as a project passes centre. */
.work__rim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    105deg,
    transparent 38%,
    rgba(150, 200, 255, 0.38) 50%,
    transparent 62%
  );
  translate: var(--rimx, -70%) 0;
  opacity: var(--rim, 0);
  mix-blend-mode: screen;
}

.work__visual img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}


.work__kicker {
  font-family: var(--display);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.75rem;
}

.work__title {
  font-size: clamp(1.35rem, 2.6vw, 1.9rem);
  margin-bottom: 0.85rem;
}

.work__text {
  color: var(--muted);
  max-width: 52ch;
}

.work__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 1.25rem;
}
.work__chips li {
  font-size: 0.75rem;
  color: var(--faint);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
}

.work__link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 1.35rem;
  font-family: var(--display);
  font-weight: 500;
  font-size: 0.9375rem;
  color: var(--ink);
  border-bottom: 1px solid var(--accent);
  padding-bottom: 2px;
  transition: color 0.2s ease;
}
.work__link:hover { color: var(--accent-2); }
.work__link .ico { font-size: 1.05em; transition: transform 0.2s ease; }
.work__link:hover .ico { transform: translateX(3px); }

/* Counter and rail belong to the pinned form; stacked, they say nothing. */
.work__meter { display: none; }

/*
 * ...except on the phone, where the carousel needs it.
 *
 * This has to sit AFTER the rule above rather than in the mobile block with
 * the rest of the carousel. A media query contributes nothing to specificity,
 * so `@media ... { .work__meter { display: flex } }` earlier in the file and
 * `.work__meter { display: none }` later are a straight tie, and a tie is won
 * by whichever comes last. It did, and the meter stayed hidden.
 *
 * On a strip with no scrollbar and no cursor, this is the only thing that says
 * how far through five projects you are. It sits under the cards - `order`
 * moves it past the list, which comes after it in the markup.
 */
@media (max-width: 900px) {
  .work__meter {
    display: flex;
    align-items: center;
    gap: 1rem;
    order: 1;
    margin-top: 0.25rem;
  }
  .work__rail-fill { transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1); }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .work__rail-fill { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .work__link .ico { transition: none; }
}

/* ==========================================================================
   The Work Theater — pinned form

   Enabled only by .work--theater, which studio.js adds when the conditions are
   right: a fine pointer, a tall enough viewport, and no reduced-motion
   preference. Everything above stays exactly as it was for everyone else.

   HOW THE FREEZE WORKS, AND WHY IT IS NOT SCROLL-HIJACKING

   The section becomes very tall and the stage inside it is sticky. Scrolling
   consumes the section's height while the stage stays put, so the page FEELS
   frozen while the scrollbar keeps meaning exactly what it says. Motion's
   scroll() reads progress through the section and hands each third to one
   project.

   Actually hijacking the wheel - preventDefault and animating between stops -
   is how sites end up with broken trackpads and visitors who cannot leave.
   Sticky gives the same feeling, keeps the browser in charge, and works with
   keyboard, touch and find-in-page for free.
   ========================================================================== */
.work--theater .work__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
}

/* Three screens of scroll per project, plus a little lead-in and lead-out. */
/*
 * THE SEAM BETWEEN THE PROCESS AND THE WORK.
 *
 * There was 392px of nothing between the last process step and "Built by
 * Everon", and none of it came from one place:
 *
 *   120px  the process section's own padding-bottom
 *   120px  the work section's padding-top, immediately after it
 *   152px  inside the work stage, above the heading
 *
 * The 152px is not waste and must stay. The stage is a viewport tall and
 * centres what it holds, so that space is what puts the heading in the middle
 * of the screen once the section pins. Take it away and the heading rides the
 * very top edge for the whole theater.
 *
 * The other 240px is two sections' worth of breathing room paid twice at the
 * same seam, on top of a stage that already opens with a screenful of lead.
 * That is the part to give back.
 */
/*
 * The work section is the one place where equal padding does NOT produce an
 * equal gap, so it is given unequal padding on purpose.
 *
 * Pinned, its stage is a viewport tall and centres what it holds, which puts
 * roughly 152px of its own space above the heading. Left alone that seam would
 * read as 100 + 100 + 152 = 352px against 200px everywhere else. So the
 * section spends nothing at the top and the process section above it gives
 * back about half of its own, and the seam lands on the same 200px as the
 * rest. Below, there is no such lead - the last project is still on screen
 * when the scroller ends - so the full measure is paid there.
 */
#how-we-work { padding-bottom: calc(var(--section-pad) * 0.48); }
.work--theater { padding-top: 0; }
.work--theater { padding-bottom: var(--section-pad); }

/* Stacked, the stage is an ordinary block with no lead-in to lean on, so this
   section keeps the standard measure like every other. */
@media (max-width: 999px) {
  #how-we-work { padding-bottom: var(--section-pad); }
}
/* One screen of scroll per project plus lead-in and lead-out: three wanted
   340vh, five want 540vh. paint() already divides by however many
   projects it finds. */
.work--theater .work__scroller { height: 540vh; }

/* Tighter than the stacked form: the heading is inside the stage now, and a
   viewport is only so tall.

   Gap ONLY. This element is also `.shell`, which is what gives it the content
   width and the auto side margins that centre it; a `width: 100%` here
   overrides that and the whole section bleeds to both edges of the screen. */
.work--theater .work__inner {
  gap: clamp(1.25rem, 3vh, 2rem);
}

/*
 * Every project occupies the same space, which is what makes the handover a
 * cross-fade rather than a jump. They used to do that by being absolutely
 * positioned - and that is why the section had a hole in it. Taking all three
 * out of flow left the list measuring ZERO pixels tall, so there was nothing
 * for the stage to centre; the projects hung off a collapsed line at the
 * middle of a full-height stage while the heading sat far above it.
 *
 * Assigning all three to the same grid cell stacks them just as completely,
 * but the cell still measures the tallest of them, so the list has a real
 * height and the stage has something to centre. No absolute positioning, no
 * hard-coded height to keep in sync with the artwork.
 */
.work--theater .work__list {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.work--theater .work__item {
  grid-column: 1;
  grid-row: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(2rem, 4vw, 3.5rem);
  align-content: center;
  align-items: center;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}

.work--theater .work__item.is-active { pointer-events: auto; }
.work--theater .work__item:nth-child(even) .work__visual { order: 0; }

/* The blueprint develop.
   A project arrives desaturated and blue-washed - a drawing - and resolves into
   full colour as its segment locks in. Driven from studio.js by two custom
   properties so the whole thing is one filter and one overlay, not a stack of
   animated layers. */
/*
 * THE TURN.
 *
 * A project does not fade into the next one, it turns away from it. The device
 * leaving rotates on its own vertical axis and recedes; the one arriving comes
 * round from the other side, and a light crosses its glass as it passes centre.
 *
 * The rotation is on the DEVICE, not the figure. The figure is a grid cell in
 * the content column, and rotating that would swing its corners out of the
 * column - the same bleed that had to be fixed once already. A child turning
 * inside a box that never moves keeps every pixel of it in place.
 *
 * `perspective` lives on the figure rather than on the deck, so each project
 * turns about its own centre. One shared perspective on the parent would make
 * the outer two lean toward the middle, which reads as a fairground ride
 * rather than as a page.
 */
.work--theater .work__visual { perspective: 1500px; }

/* The whole group turns, not the screenshot alone: the palette belongs to
   the picture and has to leave with it. */
.work--theater .work__group {
  translate: 0 0 var(--tz, 0px);
  rotate: y var(--ry, 0deg);
  transform-origin: center center;
}

/* Counter and rail earn their place once the theater is pinned. */
.work--theater .work__meter {
  display: flex;
  align-items: center;
  gap: 1rem;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: clamp(1.5rem, 4vh, 3rem);
  width: var(--shell);
  max-width: var(--shell);
  z-index: 3;
}

.work__rail {
  position: relative;
  flex: 1;
  height: 2px;
  border-radius: 2px;
  background: var(--line);
  overflow: hidden;
}

.work__rail-fill {
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  box-shadow: 0 0 14px var(--glow);
}

.work__count {
  font-family: var(--display);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--faint);
  white-space: nowrap;
}
.work__count b { color: var(--ink); font-weight: 500; }

/* The pinned stage is a fixed-height box; long copy must not push out of it. */
.work--theater .work__text { max-width: 46ch; }
.work--theater .work__title { font-size: clamp(1.5rem, 2.8vw, 2.2rem); }

/* ==========================================================================
   Capabilities marquee

   One row, drifting left, built the same way as the storefront's TLD ticker -
   because that ticker had both of these bugs first and its comments record the
   fixes.

   SPACING IS PADDING, NOT GAP. This is the seam.

   The track is the list rendered twice and the animation translates it by
   exactly -50%. For that to be seamless, half the track must equal exactly one
   copy's period: twenty items AND the twenty spaces that follow them. With
   `gap` on a flex track, forty children have thirty-NINE gaps, so half the
   width is twenty items plus nineteen-and-a-half spaces - half a gap short, and
   the row hops by that much every time it loops. Padding folds each space into
   its own item, so duplicating the list duplicates the spacing with it and the
   arithmetic is exact.

   THE BOX IS CAPPED. This is the hole.

   The loop is only seamless while ONE copy is at least as wide as the visible
   box; wider than that and the second copy has not arrived yet when the first
   runs out, which shows as a blank stretch that ends with the row snapping
   back. Capping the box at the content column means it can never outgrow a
   copy, whatever the monitor.

   Logos are Simple Icons paths inlined into the page's sprite and filled with
   currentColor, so this one rule colours all twenty Everon blue.
   ========================================================================== */
.marquee {
  padding: clamp(1.75rem, 3vw, 2.5rem) 0;
  border-block: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(24, 28, 37, 0.5), transparent);
  overflow: hidden;
}

.marquee__row {
  /* Never wider than one copy of the list. */
  width: var(--shell);
  margin-inline: auto;
  overflow: hidden;
  /* Fade both ends so items arrive and leave instead of popping at the edge. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%);
}

.marquee__track {
  display: flex;
  align-items: center;
  width: max-content;
  animation: marquee-drift 58s linear infinite;
}

/* Pause on hover, so a name someone is trying to read stops moving. */
.marquee__row:hover .marquee__track { animation-play-state: paused; }

.marquee__item {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  /* The spacing lives HERE, not in a track gap - see the note above. */
  padding: 0 1.6rem;
  font-family: var(--display);
  font-size: 1.125rem;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
}

.marquee__logo {
  width: 28px;
  height: 28px;
  flex: none;
  display: block;
  color: var(--accent);
  opacity: 0.92;
}

@keyframes marquee-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (max-width: 575px) {
  .marquee__item { font-size: 1rem; padding: 0 1.1rem; }
  .marquee__logo { width: 24px; height: 24px; }
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  /* Static, it should start flush at the left rather than mid-loop. */
  .marquee__row {
    -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent);
    mask-image: linear-gradient(90deg, #000 88%, transparent);
  }
}
/* ==========================================================================
   FAQ

   Native <details>, so it opens and closes with no JavaScript. The script only
   smooths the height; without it the browser's own instant toggle is a
   perfectly good answer.
   ========================================================================== */
/* The FAQ and the enquiry form share the row, half each. The questions used
   to run to 52rem with the rest of the section empty beside them; now the
   width the site gained is carrying something. */
.faq__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 4vw, 4rem);
  align-items: start;
}

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

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

/* ---------------------------------------------------------- enquiry form */
.enquiry {
  padding: clamp(1.5rem, 2.4vw, 2rem);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);

  /* Two things in the nav point here now - Talk to us and Contact - and the
     form is not a .section, so it never inherited the scroll offset the
     sections have. Measured before this line: arriving at #enquiry put the
     form's top 59px behind the fixed bar, which is "Tell us about the
     project" hidden under it. The extra 1rem is so the box has a little air
     above it rather than being welded to the bar. */
  scroll-margin-top: calc(var(--nav-h) + 1rem);
}

.enquiry__title {
  font-family: var(--display);
  font-size: 1.1875rem;
  font-weight: 500;
  margin: 0;
}

.enquiry__note {
  color: var(--muted);
  font-size: 0.9375rem;
  line-height: 1.6;
  margin: 0.5rem 0 0;
}

.enquiry__grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 0.9rem;
  margin-top: 1.4rem;
}

/* Each field is a <p> so the label and its control stay one block for a
   screen reader and for anything that strips the CSS. */
.field {
  grid-column: 1 / -1;
  display: grid;
  gap: 0.4rem;
  margin: 0;
}
.field--third { grid-column: span 2; }
.field--rest  { grid-column: span 4; }
.field--half  { grid-column: span 3; }

@media (max-width: 560px) {
  .field--third,
  .field--rest,
  .field--half { grid-column: 1 / -1; }
}

.field label {
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  color: var(--muted);
}

.field__wrap { display: block; position: relative; }

.enquiry input,
.enquiry select,
.enquiry textarea {
  width: 100%;
  padding: 0.7rem 0.85rem;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--ground);
  color: var(--ink);
  font: 400 0.9375rem/1.5 var(--body);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.enquiry textarea {
  resize: vertical;
  min-height: 7rem;
}

.enquiry ::placeholder { color: var(--faint); opacity: 1; }

.enquiry input:focus,
.enquiry select:focus,
.enquiry textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--glow);
}

/*
 * THE DROPDOWNS.
 *
 * A <select> can be styled down to the last pixel while it is CLOSED and not at
 * all once it is open: the list is drawn by the operating system, in the
 * system palette, at the system size. On a dark page that means a white list
 * with a blue highlight appearing out of nowhere - which is the one part of
 * this form that still looked like a browser default.
 *
 * So the real <select> stays in the markup, doing the work - it submits, it is
 * required, it is what a keyboard and a screen reader already know how to
 * drive, and with the script blocked it is the whole control. studio.js then
 * builds a listbox beside it and hides it from view, mirroring every change
 * back onto it. The panel is the language picker's, because this site already
 * has a dropdown and a second look would be a second answer to a solved
 * question.
 */
.enquiry select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.4rem;
  cursor: pointer;
}

/* The native caret, for the no-script case where the real select is visible. */
.field__wrap--select::after {
  content: "";
  position: absolute;
  right: 0.95rem;
  top: 50%;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}
/* Once the listbox exists it draws its own, and the wrapper is no longer the
   thing being pointed at. */
.field__wrap.has-picker::after { display: none; }

/* Still worth setting: a few platforms honour it, and this is the fallback. */
.enquiry select option,
.enquiry select optgroup {
  background: var(--surface-2);
  color: var(--ink);
}

/* --------------------------------------------------------------- the picker
   Taken out of the flow rather than display:none, because a <select> that is
   not rendered cannot be focused, and the label still points at it - clicking
   the label has to land somewhere real. */
.field__wrap.has-picker select {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  border: 0;
}

.picker { position: relative; }

.picker__button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  padding: 0.7rem 0.85rem;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--ground);
  color: var(--ink);
  font: 400 0.9375rem/1.5 var(--body);
  text-align: left;
  cursor: pointer;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/* Nothing chosen yet reads as a placeholder, the same as the other fields. */
.picker.is-empty .picker__button { color: var(--faint); }

.picker__button:hover { border-color: var(--accent); }
.field__wrap.has-picker select:focus-visible + .picker .picker__button,
.picker__button:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--glow);
}

.picker__caret {
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.2s ease;
}
.picker.is-open .picker__caret { transform: translateY(1px) rotate(-135deg); }

.picker__panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 5;
  max-height: 17rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 6px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: #141414;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.16s ease;
}
.picker.is-open .picker__panel { opacity: 1; pointer-events: auto; }

.picker__group {
  padding: 0.55rem 0.65rem 0.3rem;
  font-family: var(--display);
  font-size: 0.75rem;   /* matches the other micro-labels */
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
}

.picker__opt {
  padding: 0.5rem 0.65rem;
  border-radius: 8px;
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--nav-text);
  cursor: pointer;
  transition: color 0.2s linear, background-color 0.2s linear;
}
.picker__opt:hover,
.picker__opt.is-active {
  color: #fff;
  background: rgba(255, 255, 255, 0.08);
}
.picker__opt[aria-selected="true"] {
  color: var(--accent-2);
  font-weight: 500;
}

@media (prefers-reduced-motion: reduce) {
  .picker__panel,
  .picker__caret { transition: none; }
}

.enquiry__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem 1.1rem;
  margin-top: 1.4rem;
}

.enquiry__send { flex: none; }
.enquiry.is-sending .enquiry__send { opacity: 0.6; pointer-events: none; }

.enquiry__status {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--muted);
  min-height: 1.2em;
}
.enquiry.is-ok .enquiry__status { color: var(--accent-2); }
.enquiry.is-error .enquiry__status { color: #ff9a8b; }

/*
 * The trap, and the message bank.
 *
 * display:none in a CLASS, not the hidden attribute. [hidden] lives in the user
 * agent stylesheet and any author rule beats it - which is exactly how the
 * drawer scrim ended up covering every phone screen. A class cannot be
 * outvoted the same way.
 */
.enquiry__trap,
.enquiry__msg { display: none; }

.faq__item { border-bottom: 1px solid var(--line); }

.faq__q {
  position: relative;
  display: block;
  padding: 1.35rem 3rem 1.35rem 0;
  font-family: var(--display);
  font-size: clamp(1rem, 1.6vw, 1.125rem);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  transition: color 0.2s ease;
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q:hover { color: var(--accent-2); }

/* The plus, drawn from two rules so it can rotate into a minus. */
.faq__q::before,
.faq__q::after {
  content: "";
  position: absolute;
  right: 4px;
  top: 50%;
  width: 14px;
  height: 1.5px;
  background: var(--accent);
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.faq__q::after { transform: rotate(90deg); }
.faq__item[open] .faq__q::after { transform: rotate(0deg); opacity: 0; }

.faq__a {
  overflow: hidden;
  color: var(--muted);
}
.faq__a p {
  padding-bottom: 1.5rem;
  max-width: 62ch;
}

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

/* ==========================================================================
   Eyebrow rule, card spotlight, step ignition
   ========================================================================== */

/* A short blue rule before each eyebrow, drawn in when the section arrives.
   scaleX from 0 is a transform, so it costs a composite and not a layout. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
.eyebrow::before {
  content: "";
  width: 24px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(var(--rule, 1));
  transform-origin: left center;
}

/* Cursor spotlight on the service cards. One radial per card, positioned from
   two custom properties written by studio.js - the same technique family as
   the mercury headline, and pointer devices only. */
@media (hover: hover) and (pointer: fine) {
  .bento__cell::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.35s ease;
    background: radial-gradient(
      250px circle at var(--spot-x, 50%) var(--spot-y, 50%),
      rgba(56, 130, 255, 0.1),
      transparent 65%
    );
  }
  .bento__cell:hover::before { opacity: 1; }
}

/* The process numbers light up as the rail reaches them - and grow by exactly
   the clearance the line was keeping, so the circle meets it.
   Width and height rather than `scale`, deliberately: scaling would blur the
   1px border and swell the digit with it, where growing the box leaves both
   crisp and the number the size it always was. The negative margins keep the
   circle centred on the line while it grows. */
.step__no {
  transition: border-color 0.4s ease, color 0.4s ease, background-color 0.4s ease,
    box-shadow 0.4s ease, width 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    margin 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.step.is-lit .step__no {
  border-color: var(--accent);
  color: var(--ink);
  background: rgba(56, 130, 255, 0.16);
  box-shadow: 0 0 18px var(--glow);
  width: calc(var(--no-size) + 2 * var(--rail-gap));
  height: calc(var(--no-size) + 2 * var(--rail-gap));
  margin-left: calc(-1 * var(--rail-gap));
  margin-top: calc(-1 * var(--rail-gap));
}

@media (prefers-reduced-motion: reduce) {
  .step__no { transition: none; }
  .bento__cell::before { display: none; }
}

/* ==========================================================================
   Hero floaters

   The same marks that run in the marquee below, rising slowly through the hero
   like helium balloons - dark blue at half opacity, barely there.

   FOUR MOTIONS, FOUR ELEMENTS. A logo does four things at once: it rises, it
   sways, it turns, and it flinches away from the cursor. Rise, sway and flinch
   are all translation, and `translate` holds exactly one pair of numbers - so
   anything sharing that property has to share a clock too, and eighteen logos
   would drift in lockstep instead of independently. Each motion therefore gets
   its own element and its own property:

     .floaty         rises      translate Y    CSS
     .floaty__sway   sways      translate X    CSS
     .floaty__push   flinches   translate XY   JS
     .floaty__ico    tilts      rotate         CSS

   That split is also what keeps the cursor honest. The path is authored
   entirely in CSS and JS never touches it; the repulsion lives on its own
   layer and eases back to zero. So a logo pushed aside is not given a new
   trajectory - it returns to the one it was always on.

   The marks are sprite symbols already in the page for the marquee, and the
   whole layer is built in JS. That is deliberate: no markup means nothing new
   for the i18n extractor to find, so /de/ and /bs/ inherit this by sharing
   studio.js, with no rebuild and no new keys to translate.
   ========================================================================== */
.hero-floaties {
  --floaty-ink: #1e50a8;
  --floaty-alpha: 0.5;

  position: absolute;
  /* The same band the marquee runs in - `--shell`, centred - so the marks rise
     through the column the page is already built on rather than the full
     bleed. left/right 0 plus an explicit width is what lets `margin-inline`
     centre an absolutely positioned box. */
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: var(--shell);
  margin-inline: auto;
  z-index: 0;
  /* Deliberately NOT clipped. The hero already has `overflow: hidden`, which
     is what hides a logo entering below and leaving above; clipping here as
     well would slice one in half against an invisible column edge the moment
     the cursor shoved it sideways. */
  overflow: visible;
  /* Decoration must never intercept the hero's buttons. */
  pointer-events: none;
  /* Eighteen animating children have no bearing on anything outside - but no
     `paint`, which would reintroduce exactly the clipping avoided above. */
  contain: layout;
}

.floaty {
  position: absolute;
  /* Parked on the hero's bottom edge; the animation carries it up past the
     top. --travel is written by JS from the hero's real height, because the
     hero is not a fixed fraction of the viewport. */
  top: 100%;
  left: var(--x);
  width: var(--size);
  height: var(--size);
  margin-left: calc(var(--size) / -2);
  animation: floaty-rise var(--rise-dur) linear var(--rise-delay) infinite;
}

@keyframes floaty-rise {
  from { translate: 0 0; }
  to   { translate: 0 calc(-1 * var(--travel, 900px)); }
}

.floaty__sway,
.floaty__push,
.floaty__ico {
  display: block;
  width: 100%;
  height: 100%;
}

.floaty__sway {
  animation: floaty-sway var(--sway-dur) ease-in-out var(--sway-delay) infinite alternate;
}

@keyframes floaty-sway {
  from { translate: calc(-1 * var(--sway)) 0; }
  to   { translate: var(--sway) 0; }
}

/* JS owns this element's translate, and nothing else writes to it. */
.floaty__push {
  translate: 0 0;
}

.floaty__ico {
  color: var(--floaty-ink);
  opacity: var(--floaty-alpha);
  animation: floaty-tilt var(--turn-dur) ease-in-out infinite alternate;
}

/* A balloon rocks; it does not spin. A full rotation would also turn every
   brand mark upside down, which is worse than no motion at all. */
@keyframes floaty-tilt {
  from { rotate: calc(-1 * var(--tilt)); }
  to   { rotate: var(--tilt); }
}

@media (prefers-reduced-motion: reduce) {
  /* JS never builds the layer under this setting; this is belt and braces. */
  .hero-floaties { display: none; }
}

/* ==========================================================================
   The studio photograph, with the mark floating over it

   TWO MOTIONS, ONE ELEMENT - which is only possible because they are two
   different CSS properties rather than two halves of one `transform`. The
   mark breathes on `scale` (CSS, everywhere) and drifts against the cursor on
   `translate` (JS, pointer devices only). Neither needs a wrapper and neither
   needs the other's clock: the breathing carries on undisturbed while JS
   moves the very same element.

   On touch nothing writes the translate, so the breathing is the whole
   effect - which is exactly the brief.
   ========================================================================== */
.shot {
  position: relative;
  /* All four sides, not just the top. A <figure> carries `margin: 1em 40px`
     from the browser's own stylesheet, which quietly inset this 40px either
     side and left the photograph 1100px wide inside an 1180px column - narrower
     than every other block on the page, and just subtle enough to read as bad
     alignment rather than as a bug. */
  margin: clamp(2.5rem, 5vw, 3.75rem) 0 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line-strong);
  box-shadow: 0 18px 46px rgba(0, 0, 0, 0.5);
  background: var(--surface);
}

.shot__img {
  display: block;
  width: 100%;
  height: auto;
}

/* A scrim. The photograph has bright laptop lids and warm highlights, and a
   light mark laid straight over them disappears. Sits above the photograph
   and below the mark. */
.shot::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 54% at 50% 50%, rgba(6, 10, 20, 0.62), rgba(6, 10, 20, 0.2) 70%, transparent),
    linear-gradient(180deg, rgba(6, 10, 20, 0.3), transparent 42%);
  pointer-events: none;
}

.shot__mark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* ::after is generated as the last child and would otherwise cover this. */
  z-index: 1;
  pointer-events: none;
  /* JS owns this property on this element, and nothing else writes to it. */
  translate: 0 0;
}

.shot__logo {
  width: clamp(150px, 30%, 380px);
  height: auto;
  /* A light mark on a photograph needs its own edge; the blue is the page's
     accent picked up so it reads as lit rather than pasted on. */
  filter:
    drop-shadow(0 10px 34px rgba(0, 0, 0, 0.6))
    drop-shadow(0 0 22px rgba(56, 130, 255, 0.28));
  animation: shot-breathe 7s ease-in-out infinite alternate;
}

/* Breathing is `scale` alone, deliberately: it leaves `translate` free for the
   cursor drift to use on the parent. */
@keyframes shot-breathe {
  from { scale: 0.965; opacity: 0.82; }
  to   { scale: 1.045; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .shot__logo { animation: none; scale: 1; opacity: 1; }
}

/* ==========================================================================
   The process stage - a card per step, stacking as the rail advances

   Sticky beside the list, so the pile stays in view for the whole read rather
   than scrolling away after the first step.

   The cards are placeholders: a gradient and a CSS counter. There is no text
   in the markup at all, which is what keeps five decorative panels out of the
   translation files. Dropping an <img> inside a card replaces the look
   without touching anything else here.
   ========================================================================== */
.process__stage {
  /* Named, because the frame's height limit is derived from it. */
  --stage-top: clamp(5rem, 12vh, 7.5rem);
  position: sticky;
  top: var(--stage-top);
  align-self: start;
}

/* The photographs' own ratio, so `cover` has nothing to crop. */
/*
 * THE DECK CASCADES DOWN instead of stacking in one place.
 *
 * Five cards in a single slot left a 283px card beside a 582px list, and the
 * column read as half empty. Each card now rests half its own height below the
 * one before it, so the deck ends up three card-heights tall - one for the
 * first, half each for the four that follow - and stands level with the steps
 * beside it.
 *
 * That is where this ratio comes from: the frame is one card wide and three
 * cards tall, so 2752 x (1536 x 3).
 *
 * The cap is what keeps it honest on a short screen. The frame is sticky, and
 * a sticky box taller than the window can never fully show itself. Past that
 * point height stops following width, the cards size themselves from the
 * frame's height instead, and the whole deck simply gets smaller rather than
 * hanging off the bottom of the screen.
 */
.stage__frame {
  position: relative;
  /* One card wide; seven thirds of a card tall. */
  aspect-ratio: 2752 / 3584;
  max-height: calc(100vh - var(--stage-top) - 2rem);
}

.stage__card {
  position: absolute;
  /* Sized from the frame's HEIGHT, not stretched to its box: a third of the
     frame tall, and whatever width that makes at the photograph's ratio. When
     the cap above shortens the frame, the cards follow it down and stay
     centred instead of distorting. */
  /* Three sevenths of the frame: seven thirds tall, so one card is 3/7. */
  height: calc(100% * 3 / 7);
  width: auto;
  aspect-ratio: 2752 / 1536;
  left: 0;
  right: 0;
  margin-inline: auto;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line-strong);
  background: linear-gradient(150deg, var(--c1), var(--c2));
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.55);
  /* Individual properties, not one `transform`, so the deck offset and the
     arrival can be reasoned about separately. */
  transition:
    opacity 0.75s cubic-bezier(0.16, 1, 0.3, 1),
    translate 0.75s cubic-bezier(0.16, 1, 0.3, 1),
    scale 0.75s cubic-bezier(0.16, 1, 0.3, 1);
}

/* The photograph itself. The card's gradient shows through until it has
   decoded, which is why the gradient stays. */
.stage__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /*
   * The blur goes on the PHOTOGRAPH, not the card. Blurring the card would
   * smear its border and its shadow into the background, which reads as a
   * rendering fault rather than as distance.
   *
   * A blurred image also bleeds a soft transparent edge, because the filter
   * samples past the bounds of the picture and finds nothing there. Growing it
   * a fraction pushes that edge under the card's own clip, so the frame stays
   * hard while its contents go soft.
   */
  scale: calc(1 + var(--depth, 0) * 0.012);
  filter: blur(calc(var(--depth, 0) * 1.4px));
  transition:
    filter 0.6s cubic-bezier(0.16, 1, 0.3, 1),
    scale 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

/* A third of a card down from the one before. A card is 3/7 of the frame, so
   a third of it is 1/7 - which is why every slot lands on a seventh. */
.stage__card:nth-child(1) { top: 0; }
.stage__card:nth-child(2) { top: calc(100% / 7); }
.stage__card:nth-child(3) { top: calc(100% * 2 / 7); }
.stage__card:nth-child(4) { top: calc(100% * 3 / 7); }
.stage__card:nth-child(5) { top: calc(100% * 4 / 7); }

.stage__card:nth-child(1) { --c1: #1d4ed8; --c2: #0a1730; z-index: 1; }
.stage__card:nth-child(2) { --c1: #1e60b8; --c2: #08182c; z-index: 2; }
.stage__card:nth-child(3) { --c1: #1a6f9c; --c2: #071c30; z-index: 3; }
.stage__card:nth-child(4) { --c1: #2563a8; --c2: #0a1424; z-index: 4; }
.stage__card:nth-child(5) { --c1: #3882ff; --c2: #0b1c3a; z-index: 5; }

/*
 * Everything below only applies once JS is actually driving the rail. Without
 * it the cards simply sit there, visible and stacked - the same rule the hero
 * reveals follow, because a stylesheet that hides content is a stylesheet that
 * can leave the page blank when the script never arrives.
 */
/* Waiting: straight down and out of sight, far enough to read as arriving
   from below the frame rather than fading up on the spot. */
.process.is-live .stage__card {
  opacity: 0;
  translate: 0 150px;
  scale: 0.94;
}

/*
 * Arrived. Fully opaque now - depth is carried by SIZE and FOCUS instead of
 * transparency, which is how distance actually reads: a thing further away is
 * smaller and less sharp, not see-through. `--depth` is written by studio.js
 * and counts how many cards sit in front of this one, so the front card is 0
 * and the one at the back of a full deck is 4.
 */
.process.is-live .stage__card.is-on {
  opacity: 1;
  translate: 0 0;
  scale: calc(1 - var(--depth, 0) * 0.05);
}

@media (max-width: 900px) {
  /*
   * The stage does not appear on a phone.
   *
   * It was never good there. The cascade has to collapse into a single slot
   * because three card-heights of sticky panel would swallow the screen, and
   * what is left is one small picture pinned above the steps, repeating what
   * the words beside it already say. The steps are the content; the pictures
   * were only ever there to fill a column that a phone does not have.
   *
   * `display: none` also means the five photographs are never fetched -
   * lazy images inside a box that is not rendered are not requested - so a
   * phone saves the whole set.
   *
   * Back to a plain block: with the stage gone there is no second column to
   * lay out and nothing to reorder, so neither the grid nor the flex column
   * it used to become has anything left to do.
   */
  .process { display: block; }
  .process__stage { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* rail() never runs here, so nothing adds is-live and the defaults above
     already show the finished state: full rail, cards stacked. Only the lit
     numbers need saying, since those come from a class. */
  .step__no {
    border-color: var(--accent);
    color: var(--ink);
    background: rgba(56, 130, 255, 0.16);
    width: calc(var(--no-size) + 2 * var(--rail-gap));
    height: calc(var(--no-size) + 2 * var(--rail-gap));
    margin-left: calc(-1 * var(--rail-gap));
    margin-top: calc(-1 * var(--rail-gap));
    transition: none;
  }
  .stage__card { transition: none; }
}

/* The gradient placeholders are gone: every project has a screenshot now. */

/* ==========================================================================
   The work panel - a build log

   The four stages of the process section, told again for one project. It is
   the same claim the rail above makes, with the specifics filled in, and it
   fills as the project turns to face you.
   ========================================================================== */
.work__lede {
  color: var(--ink);
  font-size: 1.0625rem;
  margin-top: 0.35rem;
}

.work__log {
  margin-top: 1.15rem;
  border-left: 1px solid var(--line);
}

.work__logrow {
  position: relative;
  display: grid;
  grid-template-columns: 5.25rem minmax(0, 1fr);
  gap: 0.75rem;
  padding: 0.5rem 0 0.5rem 1rem;
  /* Defaults to 1, so with no JS - or in the stacked form, where there is no
     turn to follow - every row simply reads. The dim state exists only while
     something is driving it. */
  opacity: calc(0.32 + var(--in, 1) * 0.68);
}

.work__dot {
  position: absolute;
  left: -4px;
  top: 0.95rem;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  scale: calc(1 + var(--in, 0) * 0.35);
}
/* Under the theater the dots start unlit and light in order. */
.work--theater .work__dot { background: var(--line-strong); }
.work--theater .work__logrow.is-lit .work__dot {
  background: var(--accent);
  box-shadow: 0 0 12px var(--glow);
}

.work__logk {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.75rem;   /* 11px was the smallest text on the page */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  padding-top: 0.15rem;
}
.work__logv {
  font-size: 0.8125rem;
  color: var(--muted);
  line-height: 1.5;
}

@media (max-width: 620px) {
  .work__logrow { grid-template-columns: 1fr; gap: 0.15rem; }
}

/* ==========================================================================
   The project palette

   Four colours per project, sampled from the screenshot rather than chosen:
   the commonest colours in the capture, plus the most saturated one covering a
   real area, which is where a brand accent hides when it is only a logo and a
   button.

   The name and hex live in a data attribute and are drawn by CSS. Written as
   `title` they would be read by the i18n extractor and twenty swatch labels
   would go through the translation pipeline; written as text they would do the
   same. This way the tooltip costs nothing.
   ========================================================================== */
.work__palette {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.8rem;
  margin-top: 0.95rem;
}

.work__palettek {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.75rem;   /* was 0.6875rem, which rendered at 11px */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint, #6b7280);
}

.work__swatches {
  display: flex;
  gap: 0.45rem;
}

.work__swatch {
  position: relative;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--c);
  /* A ring, so a near-white swatch is still a disc on a dark page. */
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22);
  cursor: help;
}

.work__swatch::after {
  content: attr(data-c);
  position: absolute;
  bottom: calc(100% + 9px);
  /* Centred on its swatch. That is safe now the palette sits centred under
     a wide picture; it opened rightward before, when the palette began at the
     panel's left margin and a centred tooltip would have been clipped. */
  left: 50%;
  white-space: nowrap;
  font: 500 0.6875rem/1 ui-monospace, "Cascadia Mono", Consolas, monospace;
  color: var(--ink);
  background: rgba(8, 12, 20, 0.96);
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  padding: 0.45rem 0.65rem;
  opacity: 0;
  translate: -50% 4px;
  pointer-events: none;
  transition: opacity 0.18s ease, translate 0.18s ease;
  z-index: 4;
}

.work__swatch:hover::after {
  opacity: 1;
  translate: -50% 0;
}

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

/* Set while the drawer is open. On <html>, never on body: body carries
   `overflow-x: clip`, and writing overflow onto it makes body the scroll
   container - which once stopped window.scrollY moving and silently killed
   every scroll-driven effect on this page. */
html.nav-open { overflow: hidden; }
