/* ============================================================================
   Trengh Technologies — interior pages
   ----------------------------------------------------------------------------
   Loaded AFTER homepage.css, which is where the header, the mega menu and the
   footer live. This file adds only what the homepage has no use for: the page
   masthead, its entrance, and the two shell overrides an interior page needs.

   WHAT THIS FILE MUST NOT DO
   --------------------------
   No literal colour values. tokens.css is the only file allowed one, and an
   accent may not do contrast work: --color-accent is graphic (2.85:1 on
   smoke), --color-accent-text is the one that is legible (6.18:1). Every
   accent below is a rule, a dot or a border. Never a word.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   0. WHICH ACCENT, ON AN INTERIOR PAGE

   These pages sit on --color-bg, which is near white in the light theme, and
   --color-accent is Red Orange. Measured against that ground it is 2.84:1,
   which clears nothing: not 4.5:1 for text and not even 3:1 for a non-text
   mark. There is no ground on these pages where the graphic accent is safe.

   So the rule here is narrower than the homepage's, where most accent work
   happens on the dark .band and Red Orange has 6.69:1 to play with:

     every line, tick, rule, underline and border  -> --color-accent-text
       Firebrick 6.18:1 light, Coral 9.20:1 dark. Safe on both grounds.

     a background with text on it                  -> --color-accent-text
       paired ONLY with --color-on-accent, which is remapped per theme.
       White on Firebrick 6.79:1, black on Coral 9.20:1. Pairing an accent
       background with a page ink token instead is the mistake that has landed
       five times on this build.

     --color-accent                                 -> the masthead dot, and
       nothing else. It is the logo device rather than an indicator, the word
       beside it says the same thing, and it is the one place the brand colour
       should be the brand colour.

   The tick and the quote rule were both --color-accent in the first draft and
   both measured 2.84:1.
   ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
   1. MOTION TOKENS, LOCAL AND ON PURPOSE

   These do NOT read --dur-* from tokens.css. That file collapses every
   duration to 1ms under prefers-reduced-motion, which is the right answer for
   a transition and the wrong one here: it would leave the reduced-motion
   entrance running for a single frame, which is the same as no entrance at
   all. Suresh runs macOS Reduce Motion, so that path is the one he sees.

   So the gate is explicit instead. Full motion moves things. Reduced motion
   keeps the timing and the stagger, and changes what is animated to opacity
   only: nothing travels, nothing scales, nothing rotates. A fade carries no
   vestibular risk, and a page that visibly resolves is not the same page as
   one that was already finished before you looked at it.
   ------------------------------------------------------------------------ */
.page {
  --pg-in:      900ms;    /* the title words                */
  --pg-in-sm:   560ms;    /* the small parts around them    */
  --pg-in-slow: 1200ms;   /* the beam behind everything     */
  --pg-step:     70ms;    /* word to word                   */
  --pg-lift:     14px;    /* how far the small parts travel */
}

/* ---------------------------------------------------------------------------
   2. SHELL OVERRIDES
   ------------------------------------------------------------------------ */

/* The homepage header is transparent because it starts over a dark film and
   homepage.js swaps it as you scroll. There is no film here and no
   homepage.js, so the header is simply solid from the first frame. Same
   declarations the homepage uses once it has scrolled, so the two states of
   the site's one header still look like one header. */
.page .hdr {
  background: color-mix(in srgb, var(--color-bg) 86%, transparent);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 1px 0 var(--color-line);
}

/* --hdr-h is declared once, in homepage.css beside the header it describes.
   Consumed here so the masthead is not born underneath the fixed bar. */
.page main { padding-top: var(--hdr-h); }

/* Where you are, in the bar. aria-current="page" carries the meaning; this is
   the visible half of it. The mark is a rule, so the accent is doing graphic
   work and the label itself stays full-contrast ink.

   IT IS ::before, AND THAT IS NOT A STYLE PREFERENCE.
   homepage.css already owns ::after on these links: it is the dropdown chevron
   on every .nav__item--mega, and it rotates 180 degrees when the panel opens.
   An element gets ONE ::after. Putting the current-page marker there too meant
   that on any page inside a mega section, the marker replaced that section's
   chevron: "What we do" lost its arrow on all nine pages under it, while the
   other three kept theirs. It read as a markup bug in the header rather than a
   collision in a stylesheet loaded after it.

   Before adding a pseudo-element to anything in the shared header, check what
   homepage.css already puts there. */
.page .nav__link.is-current { position: relative; color: var(--color-ink); }
.page .nav__link.is-current::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px;
  height: 2px; border-radius: var(--radius-full);
  background: var(--color-accent-text);
}
@media (max-width: 1119px) {
  /* In the drawer the links are full-width rows, so an underline spanning the
     row reads as a divider. A leading bar reads as a marker. */
  .page .nav__link.is-current::before {
    left: calc(var(--gutter) * -0.5); right: auto; top: 50%; bottom: auto;
    width: 3px; height: 1.1em; transform: translateY(-50%);
  }
}

/* THE FOOTER IS IN THE FLOW HERE, NOT FIXED.
   The homepage footer is fixed to the viewport floor and uncovered as main
   scrolls off it, with the parallax driven by homepage.js. Neither half of
   that survives on an interior page: there is no homepage.js to move the
   layers, and a short page has no scroll distance to uncover anything with.

   These are the declarations homepage.css already uses below 900px wide or
   700px tall, applied here at every size. RELATIVE, not static: the scene,
   the wordmark and the legal bar are absolutely positioned against the
   footer, so dropping it to static would anchor the whole landscape to the
   viewport instead. */
.page .ftr-slot { height: auto; }
.page .ftr {
  position: relative;
  height: auto;
  display: block;
  padding-top: var(--space-xl);
  /* Room for the absolute layers, which contribute no height of their own.
     The tallest is the wordmark's box. */
  padding-bottom: max(34vw, 14rem);
}
.page .ftr__base { position: absolute; inset: auto 0 0 0; }

/* ---------------------------------------------------------------------------
   3. THE MASTHEAD

   One shape for all 28 pages, so arriving at any of them feels like arriving
   at the same site. Reading order down the page: where am I, what kind of
   thing is this, what is it called, what is it for.
   ------------------------------------------------------------------------ */
.pg {
  position: relative;
  overflow: hidden;               /* clips the beam's bleed */
  padding-block: clamp(3rem, 1.6rem + 5.6vw, 7rem) var(--space-xl);
}
.pg__wrap { position: relative; z-index: 1; }

/* THE BEAM. tokens.css §4c: the soft counterpart to the facet planes, and the
   one refraction device meant to sit under type, because facets fight text
   and beams do not. Anchored off the right edge so the colour arrives from
   outside the reading column.

   THE TWO OPACITIES ARE MEASURED, NOT CHOSEN.
   An accent may not do contrast work, and a decoration behind text is doing
   contrast work whether it meant to or not. The binding case is the lowest
   contrast ink that can ever sit over this beam, which is --color-ink-muted
   in the lede, against the brightest stop of --gradient-beam at full
   strength. Composited by hand at each stop:

     light, --color-ink-muted #48484D on smoke
       ceiling for AA 4.5:1 = 0.342     0.26 gives 5.26:1
     dark,  --color-ink-muted #A1A1A6 on black
       ceiling for AA 4.5:1 = 0.291     0.24 gives 5.21:1

   The first draft ran 0.42 in dark and measured 3.03:1, which is the accent
   contrast trap for the fifth time. Dark takes the lower number because the
   same beam reads brighter against black. Raising either value past its
   ceiling above fails AA on every one of these pages at once.

   The mask is headroom on top of that, not part of the guarantee: the figures
   above assume no mask at all, so they hold at any viewport width where the
   text runs full bleed. */
.pg__beam {
  position: absolute;
  right: -18%; top: 8%;
  width: 92%; height: 58%;
  background: var(--gradient-beam);
  filter: blur(var(--beam-blur));
  opacity: 0.26;
  transform: rotate(-8deg);
  pointer-events: none;
  /* Fades out across the left half, where the title and lede are set. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 26%, #000 74%);
          mask-image: linear-gradient(90deg, transparent 0%, transparent 26%, #000 74%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pg__beam { opacity: 0.24; }
}
[data-theme="dark"] .pg__beam { opacity: 0.24; }

/* --- breadcrumbs --------------------------------------------------------- */
.pg__crumbs ol {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2xs);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.pg__crumbs a { color: var(--color-ink-faint); }
.pg__crumbs a:hover { color: var(--color-accent-text); }
.pg__crumbs [aria-current="page"] { color: var(--color-ink); font-weight: var(--weight-medium); }
.pg__sep { color: var(--color-ink-disabled); }

/* --- kicker -------------------------------------------------------------- */
/* The dot is the mark. One Red Orange dot, per the v2.1 logo revision, and
   the only place the accent appears at full strength up here. */
.pg__kicker {
  display: flex; align-items: center; gap: var(--space-2xs);
  margin-top: var(--space-lg);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.pg__kicker::before {
  content: ""; flex: none;
  width: 0.5em; height: 0.5em; border-radius: var(--radius-full);
  background: var(--color-accent);
}

/* --- the title ----------------------------------------------------------- */
.pg__title {
  margin-top: var(--space-sm);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  font-variation-settings: "opsz" var(--opsz-display);
  color: var(--color-ink);
  text-wrap: balance;
  max-width: 18ch;
}
/* Set to length rather than to one fixed size. "ERP" and "Consolidate after
   an acquisition" at the same size makes one of them wrong. */
.pg__title--xl { font-size: var(--display-2); }
.pg__title--lg { font-size: var(--display-3); max-width: 22ch; }
.pg__title--md { font-size: var(--text-3xl); max-width: 26ch; }

/* Two elements per word: the outer one is the mask, the inner one is what
   rises out of it. One element cannot both clip and be transformed.
   The padding/margin pair is not decoration. overflow:hidden on a line of
   type crops the descenders, so the mask is grown 0.16em and pulled back by
   the same amount, which leaves the g and the y intact and the baseline
   where it was. */
.pg__title .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.pg__title .w > span {
  display: inline-block;
  animation: pg-rise var(--pg-in) var(--ease-refract) both;
  animation-delay: calc(240ms + var(--i) * var(--pg-step));
}
@keyframes pg-rise {
  from { transform: translateY(104%); opacity: 0; }
  to   { transform: none;             opacity: 1; }
}

/* --- rule and lede ------------------------------------------------------- */
.pg__rule {
  height: 1px; margin-top: var(--space-md);
  max-width: 26rem;
  background: var(--gradient-rule);
  transform-origin: left center;
  animation: pg-draw 700ms var(--ease-refract) 500ms both;
}
@keyframes pg-draw {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: none;      opacity: 1; }
}

.pg__lede {
  margin-top: var(--space-md);
  max-width: 46ch;
  font-size: var(--text-xl);
  line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* The small parts share one entrance and differ only in when it starts. */
.pg__crumbs,
.pg__kicker,
.pg__lede {
  animation: pg-lift var(--pg-in-sm) var(--ease-refract) both;
}
.pg__crumbs { animation-delay:  80ms; }
.pg__kicker { animation-delay: 160ms; }
.pg__lede   { animation-delay: 620ms; }
@keyframes pg-lift {
  from { transform: translateY(var(--pg-lift)); opacity: 0; }
  to   { transform: none;                       opacity: 1; }
}

.pg__beam { animation: pg-bloom var(--pg-in-slow) var(--ease-out) both; }
@keyframes pg-bloom {
  from { opacity: 0; transform: rotate(-8deg) scale(0.92); }
}

/* ---------------------------------------------------------------------------
   4. REDUCED MOTION

   Everything above still runs. What changes is WHAT is animated: opacity, and
   nothing else. No travel, no scale, no rotation. The stagger is kept, and
   slightly slowed, because a staggered fade is still a page resolving into
   focus, which is the whole motion principle in tokens.css §7.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .page {
    --pg-in: 620ms; --pg-in-sm: 480ms; --pg-in-slow: 900ms;
    --pg-step: 55ms; --pg-lift: 0px;
  }

  /* The mask has nothing left to hide, and keeping it would go on cropping
     descenders for no reason. */
  .pg__title .w { overflow: visible; padding-bottom: 0; margin-bottom: 0; }
  .pg__title .w > span { animation-name: pg-fade; }
  .pg__rule  { animation-name: pg-fade; }
  .pg__beam  { animation-name: pg-fade; }

  @keyframes pg-fade { from { opacity: 0; } to { opacity: 1; } }
}

/* ---------------------------------------------------------------------------
   5. THE BODY BELOW

   Placeholder, and it says so on the page. The dashed accent border is the
   same treatment the mega menu uses for its unpublished Field notes samples.
   A page that looks finished and is not is the kind of thing that survives to
   launch.
   ------------------------------------------------------------------------ */
.pg__content { padding-bottom: var(--section-y); }

.pg__pending {
  max-width: var(--container-narrow);
  padding: var(--space-md);
  border: 1px dashed var(--color-accent-text);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  color: var(--color-ink-muted);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
}
.pg__pending-label {
  margin-bottom: var(--space-2xs);
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}

.pg__back { margin-top: var(--space-lg); font-size: var(--text-sm); }

/* ---------------------------------------------------------------------------
   6. THE ASYNC SWAP

   pages.js replaces <main> between interior pages and leaves the header and
   footer untouched. It marks the document while a fetch is in flight. The
   dimming is small on purpose: this is feedback that a click registered, not
   a loading screen, and on a cached page it will barely be seen at all.
   ------------------------------------------------------------------------ */
.page main { transition: opacity var(--dur-fast) var(--ease-out); }
.page.is-swapping main { opacity: 0.55; }

@media (max-width: 640px) {
  .pg__title { max-width: none; }
  .pg__lede { font-size: var(--text-lg); }
}

/* main takes focus after an async swap so a screen reader lands in the new
   page. It is a scripted target rather than a control, so it gets no ring;
   the heading it lands on is the announcement. */
.page main:focus { outline: none; }

/* ---------------------------------------------------------------------------
   7. RELATED PAGES

   Navigation derived from the manifest, not written content, which is why it
   can ship before the copy does. It is also what data-anim="stagger" animates,
   so the motion has something real to work on rather than a demo block that
   would have to be deleted later.

   NOT HIDDEN HERE. There is deliberately no opacity: 0 in this file. Every
   starting state for a data-anim element is set by page-motion.js, and only
   once GSAP has actually loaded. Hiding in CSS and revealing in script builds
   a page that is blank whenever the script does not arrive, which is exactly
   the slow connection that could least afford it.
   ------------------------------------------------------------------------ */
.rel { margin-top: var(--space-2xl); }

.rel__label {
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-ink-faint);
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--color-line);
}

.rel__list {
  list-style: none; padding: 0;
  margin-top: var(--space-md);
  display: grid; gap: var(--space-sm);
  /* minmax(0, ...), never a bare 1fr: bare is minmax(auto, 1fr) and that auto
     floor is min-content, so one long title widens its whole column. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
}

.rel__a {
  display: block; height: 100%;
  padding: var(--space-sm);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  color: var(--color-ink);
  transition: border-color var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-refract);
}
.rel__a:hover {
  text-decoration: none;
  border-color: var(--color-accent-text);
  transform: translateY(-2px);
}
.rel__a:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.rel__h {
  display: block;
  font-size: var(--text-base); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}
.rel__p {
  display: block; margin-top: var(--space-3xs);
  font-size: var(--text-sm); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

@media (prefers-reduced-motion: reduce) {
  /* The lift is autonomous decoration on a hover, so it goes. The border
     colour is the state change that matters and it stays. */
  .rel__a:hover { transform: none; }
}

/* ---------------------------------------------------------------------------
   8. WRITTEN PAGE COMPONENTS

   Used by the HTML fragments in content/pages/. One vocabulary for all 28
   pages, so a written page is markup and never a new stylesheet.

   Same rule as section 7: NOTHING HERE IS HIDDEN. Starting states for anything
   carrying data-anim are set by page-motion.js once GSAP has loaded, so a page
   with no JavaScript is a plain, complete, readable page.
   ------------------------------------------------------------------------ */

.blk { margin-top: var(--space-2xl); }
.blk:first-child { margin-top: 0; }

.blk__kick {
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.blk__h {
  margin-top: var(--space-2xs);
  font-size: var(--text-2xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); line-height: var(--leading-snug);
  color: var(--color-ink);
  max-width: 22ch; text-wrap: balance;
}
.blk__lede {
  margin-top: var(--space-sm); max-width: 52ch;
  font-size: var(--text-lg); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* The opening statement. Set larger than body, smaller than the masthead lede,
   so the page has three clear sizes of voice rather than two.

   THE PERSPECTIVE IS LOAD BEARING. data-anim="split" rotates each line on the
   X axis, and rotationX without a perspective ancestor is a flat vertical
   squash rather than a turn. 800px is far enough back that the top line does
   not distort more than the bottom one. */
.blk--open { margin-top: 0; perspective: 800px; }
.blk__open {
  max-width: 46ch;
  font-size: var(--text-xl); line-height: var(--leading-normal);
  color: var(--color-ink);
}

/* --- capability cards ---------------------------------------------------- */
.caps, .checks, .tools, .steps { list-style: none; padding: 0; }

.caps {
  margin-top: var(--space-lg);
  display: grid; gap: var(--space-sm);
  /* minmax(0, ...) never a bare 1fr: bare is minmax(auto, 1fr), whose auto
     floor is min-content, so one long word widens the whole column. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.cap {
  padding: var(--space-md);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}
.cap__n {
  display: block;
  font-size: var(--text-sm); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  color: var(--color-accent-text);
}
.cap__h {
  margin-top: var(--space-2xs);
  font-size: var(--text-lg); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); color: var(--color-ink);
}
.cap__p {
  margin-top: var(--space-2xs);
  font-size: var(--text-sm); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* --- the pull quote ------------------------------------------------------ */
.quote {
  margin: var(--space-2xl) 0 0;
  padding-left: var(--space-md);
  border-left: 2px solid var(--color-accent-text);
  max-width: 30ch;
}
.quote p {
  font-size: var(--text-3xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display); line-height: var(--leading-snug);
  color: var(--color-ink);
}

/* --- numbered steps ------------------------------------------------------ */
.steps {
  margin-top: var(--space-lg);
  display: grid; gap: var(--space-md);
  counter-reset: step;
}
.step {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-sm);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-line);
}
.step:first-child { padding-top: 0; border-top: 0; }
.step__n {
  font-size: var(--text-sm); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  color: var(--color-accent-text);
  padding-top: 0.35em;                       /* optically aligns to the heading */
}
.step__h {
  font-size: var(--text-xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); color: var(--color-ink);
}
.step__p {
  margin-top: var(--space-2xs); max-width: 62ch;
  font-size: var(--text-base); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* --- checklist ----------------------------------------------------------- */
.checks {
  margin-top: var(--space-lg);
  display: grid; gap: var(--space-xs);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}
.check {
  position: relative; padding-left: var(--space-md);
  font-size: var(--text-base); line-height: var(--leading-normal);
  color: var(--color-ink);
}
/* A tick drawn from two borders. No icon font, no second request. */
.check::before {
  content: ""; position: absolute; left: 2px; top: 0.42em;
  width: 6px; height: 10px;
  border-right: 2px solid var(--color-accent-text);
  border-bottom: 2px solid var(--color-accent-text);
  transform: rotate(45deg);
}

/* --- the app estate: eight families, four columns ------------------------ */
/* Four columns is the brief and four columns is the point. The value of this
   block is that you can see the whole estate at once and find its edges, which
   an auto-fit track count would take away at arbitrary widths: on a wide page
   auto-fit would happily give six columns and turn eight families into a
   ragged two-and-a-bit rows. So the counts are pinned, and step 4 -> 2 -> 1.

   minmax(0, 1fr), never a bare 1fr, for the reason given up at .caps. */
.apps {
  margin-top: var(--space-lg);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-lg) var(--space-md);
}
@media (max-width: 62rem) {
  .apps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 30rem) {
  .apps { grid-template-columns: minmax(0, 1fr); }
}

.apps__fam {
  border-top: 2px solid var(--color-line-strong);
  padding-top: var(--space-xs);
}
.apps__cat {
  font-size: var(--text-2xs); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.apps__list {
  list-style: none; padding: 0;
  margin-top: var(--space-2xs);
}
.apps__list li {
  position: relative;
  padding: var(--space-3xs) 0 var(--space-3xs) var(--space-sm);
  font-size: var(--text-sm); line-height: var(--leading-snug);
  color: var(--color-ink);
}
/* The brand dot, kept neutral. The category headings above are already doing
   the accent's work, and forty-eight orange dots underneath them would read as
   decoration competing with its own label. */
.apps__list li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--color-ink-faint);
}

.apps__note {
  margin-top: var(--space-lg); max-width: 56ch;
  font-size: var(--text-base); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* --- tool tags ----------------------------------------------------------- */
.tools {
  margin-top: var(--space-lg);
  display: flex; flex-wrap: wrap; gap: var(--space-2xs);
}
.tool {
  padding: var(--space-3xs) var(--space-xs);
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-full);
  font-size: var(--text-sm); color: var(--color-ink);
}

/* --- two-up split -------------------------------------------------------- */
.split {
  margin-top: var(--space-lg);
  display: grid; gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}
.split__h {
  font-size: var(--text-lg); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); color: var(--color-ink);
}
.split__p {
  margin-top: var(--space-2xs);
  font-size: var(--text-base); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* --- closing call to action ---------------------------------------------- */
.cta {
  margin-top: var(--space-2xl);
  padding: var(--space-lg);
  border-radius: var(--radius-xl);
  background: var(--color-surface-sunk);
  border: 1px solid var(--color-line);
}
.cta__h {
  font-size: var(--text-2xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); color: var(--color-ink);
  max-width: 20ch;
}
.cta__p {
  margin-top: var(--space-sm); max-width: 52ch;
  font-size: var(--text-base); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}
/* The accent as a BACKGROUND, so the foreground must be --color-on-accent,
   which is remapped per theme. Pairing it with a page ink token instead is the
   mistake that has landed five times on this build. */
.cta__a {
  display: inline-block; margin-top: var(--space-md);
  padding: 12px 22px; border-radius: var(--radius-full);
  background: var(--color-accent-text);
  color: var(--color-on-accent);
  font-size: var(--text-sm); font-weight: var(--weight-medium);
}
.cta__a:hover { background: var(--color-accent-hover); text-decoration: none; }
.cta__a:focus-visible { outline: none; box-shadow: var(--focus-ring); }

@media (max-width: 560px) {
  .step { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xs); }
  .step__n { padding-top: 0; }
  .quote p { font-size: var(--text-2xl); }
}

/* ---------------------------------------------------------------------------
   9. THE THREE HELD SECTIONS: hscroll, deck, stack

   All three are "hold the section still while something moves through it".
   That is what ScrollTrigger pin is for, and this project does not use it.
   ADR-0006 amendment 2: pinning is position: sticky, because sticky keeps the
   document flow Lenis needs and is immune to the iOS address bar resizing
   mid-scroll, which is the thing pin gets wrong.

   The shape is the same every time and it is worth recognising:

     .x        tall, in the flow. Its HEIGHT is the scroll distance the effect
               gets. Nothing else sets that number.
     .x__stage position: sticky, top 0, height 100svh. This is what appears
               pinned. It is a child, so it is still in flow and the wrapper's
               height is what the page reserves for it.

   svh not vh: on iOS, vh is the tallest the viewport ever gets, so a 100vh
   sticky stage has its bottom edge under the address bar until the bar
   retracts. svh is the smallest, which is the only one that always fits.

   NOTHING IN HERE MAY CARRY transform, filter OR will-change ON THE WRAPPER.
   Those make it a containing block for fixed descendants and, more relevant
   here, break the sticky child out of its expected containing block.
   ------------------------------------------------------------------------ */

/* --- hscroll: the line travels sideways while the section is held --------- */
.hs {
  position: relative;
  /* The scroll distance. Longer means the characters cross more slowly. */
  height: 320svh;
  margin-top: var(--space-2xl);
}
.hs__stage {
  position: sticky; top: 0;
  height: 100svh;
  display: flex; align-items: center;
  overflow: hidden;
  /* Full bleed out of .wrap, because a line that travels the screen should
     travel the whole screen and not stop at the container edge. */
  margin-inline: calc(50% - 50vw);
}
.hs__text {
  display: flex; width: max-content;
  white-space: nowrap;
  gap: 4vw;
  padding-left: 100vw;                    /* starts off the right edge */
  margin: 0;
  font-size: clamp(2.5rem, 9vw, 9rem);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  line-height: 1.1;
  color: var(--color-ink);
}
/* SplitText writes per-character spans. A transform on an inline element does
   NOTHING, so they have to be inline-block. SplitText sets that itself, but
   the classes are named explicitly in page-motion.js and asserted here so a
   version change cannot quietly break it. */
.hs__text .hs-char { display: inline-block; will-change: transform; }
.hs__text .hs-word { display: inline-block; }

/* --- deck: the seamless looping card carousel ---------------------------- */
.deck {
  position: relative;
  /* Scroll distance scales with the card count, or a six card deck would run
     twice as fast as a three card one. page-motion.js sets --deck-cards from
     the markup; the fallback keeps the section sane if it never runs. */
  --deck-cards: 5;
  height: calc(70svh + var(--deck-cards) * 46svh);
  margin-top: var(--space-2xl);
}
.deck__stage {
  position: sticky; top: 0;
  height: 100svh;
  display: grid; place-items: center;
  overflow: hidden;
  margin-inline: calc(50% - 50vw);
}
.deck__rail {
  position: relative;
  width: 100%; height: 22rem;
  list-style: none; padding: 0; margin: 0;
  touch-action: pan-y;                    /* let the page still scroll on touch */
}
.deck__card {
  position: absolute; top: 0; left: 50%;
  /* Sized for READING, not for the portrait images the reference used. A card
     of body copy at 14rem wide with a 9/16 aspect is a column of two-word
     lines. */
  width: min(21rem, 78vw);
  margin-left: calc(min(21rem, 78vw) / -2);
  min-height: 20rem;
  padding: var(--space-md);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  box-shadow: var(--shadow-lg);
}
.deck__actions {
  position: absolute; inset: auto 0 var(--space-lg) 0;
  display: flex; justify-content: center; gap: var(--space-2xs);
}
.deck__btn {
  padding: 8px 18px;
  /* Same 3:1 rule as the form fields: this is a control, so its outline is
     what identifies it. */
  border: 1px solid var(--color-ink-disabled);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-ink);
  font: inherit; font-size: var(--text-sm);
  cursor: pointer;
}
.deck__btn:hover { border-color: var(--color-accent-text); }
.deck__btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.deck__proxy { visibility: hidden; position: absolute; }

/* --- stack: panels layered on top of each other -------------------------- */
/* THE WHOLE EFFECT IS THESE THREE DECLARATIONS. No JavaScript holds the
   layers; GSAP only adds the dim and shrink that makes them read as a stack
   rather than a hard swap, and the section is correct with it switched off. */
/* Full bleed out of .wrap: a layered panel that stops at the container edge
   reads as a card, not a layer. The inner div carries .wrap so the TEXT is
   still measured to the same column as the rest of the page. */
.blk--bleed .stack { margin-inline: calc(50% - 50vw); }
.stack { margin-top: var(--space-lg); list-style: none; padding: 0; }
.stack__panel {
  position: sticky; top: 0;
  min-height: 100svh;
  display: flex; align-items: center;
  /* Opaque, or the panel underneath shows through instead of being covered. */
  background: var(--color-bg);
  border-top: 1px solid var(--color-line);
}
.stack__inner {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-md);
  width: 100%;
  padding-block: var(--space-xl);
}
.stack__n {
  font-size: var(--display-3); font-weight: var(--weight-bold);
  line-height: 1; letter-spacing: var(--tracking-display);
  color: var(--color-accent-text);
}
.stack__h {
  font-size: var(--text-3xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); line-height: var(--leading-snug);
  color: var(--color-ink);
}
.stack__p {
  margin-top: var(--space-sm); max-width: 54ch;
  font-size: var(--text-lg); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

@media (max-width: 720px) {
  .hs { height: 260svh; }
  .deck { height: calc(60svh + var(--deck-cards) * 38svh); }
  .stack__inner { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xs); }
  .stack__n { font-size: var(--text-3xl); }
}

/* A stage taller than the viewport has a bottom nobody can scroll to, so short
   screens get the sections back in normal flow. Same reasoning as the footer's
   (max-height: 700px) branch in homepage.css. */
@media (max-height: 560px) {
  .hs, .deck { height: auto; }
  .hs__stage, .deck__stage { position: static; height: auto; padding-block: var(--space-xl); }
  .stack__panel { position: static; min-height: 0; }
}

/* ---------------------------------------------------------------------------
   10. CONTACT

   The form and the three offices. Same rule as everywhere else in this file:
   nothing is hidden waiting for JavaScript, so the page is complete and usable
   with scripting off. The form carries its own method and action; contact.js
   only stops it navigating away.
   ------------------------------------------------------------------------ */

.cform { margin-top: var(--space-lg); max-width: 46rem; }

.cform__grid {
  display: grid; gap: var(--space-sm);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.cform__field { margin: 0; display: grid; gap: var(--space-3xs); }
.cform__field--wide { margin-top: var(--space-sm); }

.cform__label {
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-ink-faint);
}

.cform__input {
  width: 100%;
  padding: 12px 14px;
  font: inherit; font-size: var(--text-base);
  color: var(--color-ink);
  background: var(--color-surface);
  /* --color-ink-disabled, NOT --color-line-strong.
     WCAG 1.4.11 wants 3:1 for anything that identifies a UI component, and a
     field's boundary is the only thing saying where the field is.
     --color-line-strong measures 1.65:1 light and 1.50:1 dark against
     --color-surface, less than half the bar. --color-ink-disabled gives 3.62
     and 3.36. tokens.css labels it "non-text only", which is exactly this.
     A card border may stay on --color-line: its text carries the meaning and
     the outline is decoration. A control's outline is not decoration. */
  border: 1px solid var(--color-ink-disabled);
  border-radius: var(--radius-md);
  /* A field that resizes the layout as you type is worse than one that does
     not, and a textarea defaults to horizontal resize in some engines. */
  transition: border-color var(--dur-fast) var(--ease-out);
}
.cform__textarea { resize: vertical; min-height: 9rem; line-height: var(--leading-normal); }
.cform__input:hover { border-color: var(--color-ink-faint); }
.cform__input:focus-visible {
  outline: none;
  border-color: var(--color-accent-text);
  box-shadow: var(--focus-ring);
}
/* :user-invalid, never :invalid. :invalid matches an empty required field from
   the moment the page loads, so every field is red before anybody has typed a
   character. :user-invalid waits until the field has actually been engaged
   with and left. */
.cform__input:user-invalid { border-color: var(--color-invalid); }

/* Off-screen rather than display:none. A bot that reads the CSS can skip a
   hidden field; one positioned away is likelier to be filled, which is the
   entire point of a honeypot. */
.cform__pot {
  position: absolute; left: -9999px;
  width: 1px; height: 1px; overflow: hidden;
}

.cform__foot {
  margin-top: var(--space-md);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm);
}
.cform__send {
  padding: 13px 26px; border: 0; border-radius: var(--radius-full);
  background: var(--color-accent-text);
  color: var(--color-on-accent);        /* the paired token, never a page ink */
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.cform__send:hover:not(:disabled) { background: var(--color-accent-hover); }
.cform__send:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.cform__send:disabled { opacity: 0.55; cursor: default; }

.cform__status { font-size: var(--text-sm); color: var(--color-ink-muted); }
.cform__status[data-kind="good"] { color: var(--color-valid); }
.cform__status[data-kind="bad"]  { color: var(--color-invalid); }

.cform__legal {
  margin-top: var(--space-md); max-width: 52ch;
  font-size: var(--text-sm); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* --- the three offices --------------------------------------------------- */
.offices {
  margin-top: var(--space-lg);
  display: grid; gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
}

.office {
  display: flex; flex-direction: column;
  padding: var(--space-md);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}
.office__country {
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.office__city {
  margin-top: var(--space-3xs);
  font-size: var(--text-xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); color: var(--color-ink);
}
.office__addr {
  margin-top: var(--space-sm);
  font-style: normal;                    /* <address> italicises by default */
  font-size: var(--text-sm); line-height: var(--leading-relaxed);
  color: var(--color-ink-muted);
}
.office__meta {
  margin-top: var(--space-sm); flex: 1;
  font-size: var(--text-sm); line-height: var(--leading-relaxed);
}

/* --- the map slot -------------------------------------------------------- */
/* Reserves its space before anything loads, so pressing Show map does not
   shove the rest of the page down. The ratio is the frame's, not the panel's. */
.office__map {
  margin-top: var(--space-md);
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface-sunk);
  border: 1px dashed var(--color-line-strong);
  display: grid; place-items: center;
}
.office__map.is-live { border-style: solid; }

.omap { padding: var(--space-md); text-align: center; }
.omap__note {
  font-size: var(--text-xs); line-height: var(--leading-normal);
  /* --color-ink-muted, NOT --color-ink-faint.
     tokens.css measures faint at 4.61:1 and calls it "AA body, just", but that
     figure is against --color-bg. This note sits on --color-surface-sunk,
     which is darker, and there it drops to 4.18:1: under the bar, at 12px,
     as real text that is not aria-hidden. Muted gives 7.05.
     THE GENERAL POINT: a contrast figure in tokens.css is quoted against
     --color-bg. Move the text onto a surface and it has to be measured again. */
  color: var(--color-ink-muted);
  max-width: 26ch; margin-inline: auto;
}
.omap__row {
  margin-top: var(--space-sm);
  display: flex; flex-wrap: wrap; gap: var(--space-2xs); justify-content: center;
}
.omap__btn {
  padding: 9px 18px;
  border: 1px solid var(--color-ink-disabled); border-radius: var(--radius-full);
  background: var(--color-surface); color: var(--color-ink);
  font: inherit; font-size: var(--text-sm); cursor: pointer;
}
.omap__btn:hover { border-color: var(--color-accent-text); }
.omap__btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.omap__link { font-size: var(--text-sm); align-self: center; padding: 9px 4px; }

.omap__frame { width: 100%; height: 100%; border: 0; display: block; }
.omap__after { margin-top: var(--space-2xs); font-size: var(--text-sm); }

@media (max-width: 560px) {
  .cform__foot { flex-direction: column; align-items: flex-start; }
  .cform__send { width: 100%; }
}

/* ---------------------------------------------------------------------------
   10. THE SPIRAL

   A canvas of Odoo module icons pulled into a spiral by page-motion.js.

   THE CANVAS NEEDS AN EXPLICIT HEIGHT AND THAT IS NOT A STYLE CHOICE. A canvas
   with no height attribute defaults to 150px, and page-motion.js measures the
   ELEMENT to size its backing store. Leave the height to the content and the
   measurement comes back 150 no matter what the layout looks like, so the
   spiral draws into a letterbox and is clipped.
   ------------------------------------------------------------------------ */
.spiral {
  margin: var(--space-lg) 0 0;
  /* Full bleed out of .wrap. A spiral that stops at the container edge reads
     as a picture of a spiral; one that runs off the sides reads as a field the
     page is sitting on. */
  margin-inline: calc(50% - 50vw);
  position: relative;
}
.spiral__c {
  display: block;
  width: 100%;
  height: clamp(28rem, 72svh, 48rem);
  /* The particles converge on the centre, so the outer reaches are sparse and
     the edges want to fall away rather than stop at a hard line. */
  -webkit-mask-image: radial-gradient(ellipse at center, #000 64%, transparent 97%);
          mask-image: radial-gradient(ellipse at center, #000 64%, transparent 97%);
}
.spiral__cap {
  max-width: 46ch;
  margin: var(--space-md) auto 0;
  padding-inline: var(--gutter);
  text-align: center;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

@media (max-width: 720px) {
  .spiral__c { height: clamp(22rem, 58svh, 32rem); }
}

/* The animation's play/pause control. Required by WCAG 2.2.2 for motion that
   starts on its own and runs past five seconds, and it is also the way in for
   anyone whose Reduce Motion setting leaves the spiral deliberately still. */
.spiral__toggle {
  position: absolute; right: var(--gutter); bottom: var(--space-sm);
  padding: 6px 14px;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-bg) 76%, transparent);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  color: var(--color-ink);
  font: inherit; font-size: var(--text-xs);
  cursor: pointer;
  /* Over the canvas, which paints nothing that needs to sit above it. */
  z-index: 1;
}
.spiral__toggle:hover { border-color: var(--color-accent-text); }
.spiral__toggle:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* ---------------------------------------------------------------------------
   11. THE REEL, and the consent-gated video beside it

   Same held-section shape as hscroll, deck and stack: a tall wrapper in the
   flow whose HEIGHT is the scroll distance, with a sticky child that is what
   appears pinned. position: sticky, never ScrollTrigger pin. ADR-0006 am.2.
   ------------------------------------------------------------------------ */
.reel {
  position: relative;
  --reel-beats: 6;                        /* page-motion.js sets the real count */
  /* One screen to arrive, then a screen of scroll per line. */
  height: calc(100svh + var(--reel-beats) * 84svh);
  margin-inline: calc(50% - 50vw);        /* full bleed: a cut should fill the frame */
}
.reel__stage {
  position: sticky; top: 0;
  height: 100svh;
  display: grid; place-items: center;
  overflow: hidden;
  padding-inline: var(--gutter);
}
.reel__beat {
  /* Every beat in the SAME grid cell, so they cross over each other rather
     than pushing the stage taller as they stack up. */
  grid-area: 1 / 1;
  max-width: 20ch;
  margin: 0;
  text-align: center;
  text-wrap: balance;
  font-size: clamp(2rem, 5.4vw, 4.5rem);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-snug);
  color: var(--color-ink);
}
/* SplitText writes a span per word. A transform on an inline element does
   nothing, so they have to be inline-block. */
.reel__beat .reel-w { display: inline-block; will-change: transform, opacity; }

/* --- the video ----------------------------------------------------------- */
/* NOTHING FROM YOUTUBE LOADS UNTIL SOMEBODY ASKS FOR IT. The poster frame is
   self-hosted, so a visitor who never presses play makes no third-party
   request at all: no youtube.com, no i.ytimg.com, no cookie. Same decision as
   the maps on /contact, and for the same reason. docs/03-security-policy.md
   section 3 allows no CDN and requires an ADR for any third-party load. */
.vid { margin-top: var(--space-2xl); }
.vid__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-surface-sunk);
}
.vid__poster { display: block; width: 100%; height: 100%; object-fit: cover; }
.vid__play {
  position: absolute; inset: 0;
  display: grid; place-items: center; gap: var(--space-sm);
  width: 100%; border: 0; padding: 0;
  background: linear-gradient(to top, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.15));
  color: #fff;                            /* on a photographic ground, not a token surface */
  font: inherit; cursor: pointer;
}
.vid__play:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.vid__play-mark {
  width: 4.5rem; height: 4.5rem; border-radius: var(--radius-full);
  display: grid; place-items: center;
  background: var(--color-accent-text);
  transition: transform var(--dur-base) var(--ease-refract);
}
.vid__play-mark svg { width: 1.6rem; height: 1.6rem; fill: var(--color-on-accent); }
.vid__play:hover .vid__play-mark { transform: scale(1.06); }
.vid__play-text { font-size: var(--text-sm); font-weight: var(--weight-medium); }
.vid__frame iframe { width: 100%; height: 100%; border: 0; display: block; }
.vid__cap {
  margin-top: var(--space-sm);
  font-size: var(--text-sm); color: var(--color-ink-muted);
}

@media (prefers-reduced-motion: reduce) {
  .vid__play:hover .vid__play-mark { transform: none; }
}
@media (max-width: 720px) {
  .reel { height: calc(80svh + var(--reel-beats) * 76svh); }
  .vid__play-mark { width: 3.4rem; height: 3.4rem; }
}
