﻿/* ============================================================
   Shiverbug Studios stylesheet
   Palette: ink (logo black), warm paper, sea blue / turtle
   green / sandy-shores orange
   Type:    Bricolage Grotesque (display) + Instrument Sans (body)
   ============================================================

   COLOUR RULES
   Four accents is three more than most sites need, so each one has
   exactly one job and nothing else may borrow it. Before reaching for
   an accent, find the row it belongs to:

   sea   INFORMATIONAL + INTERACTIVE. Every link, every focus ring on a
         light background, the FAQ marker, the "read more" buttons. Also
         the section accent wherever the section is explaining something:
         what we do, how it works, the FAQ, the press kit, the legals.
         If it is teal it is either a link or a section that teaches.

   sand  THE SELL. Primary buttons, the featured package, the photo
         stickers, the footer address - and the section accent on every
         section whose job is to win the work: the hero, the proof band,
         the packages, the publishers pitch, the footer. Most of those
         are the ink bands, which is the second reason: sand is the only
         accent with enough weight to read on --ink.
         Sand also owns the whole team area - the roster, the talent
         pool and the individual profiles. In a co-dev studio the people
         ARE the pitch, so the team reads as part of the sell rather
         than as a separate thing. On light backgrounds it appears as
         --sand-ink, never --sand: see the note by .kicker--sand.

   leaf  RECOGNITION, AND THINGS GOING WELL. The studio band and the
         award - plus the state layer that means the same thing in
         miniature: a sent form, a live dot, an active member, a "do"
         rule in the brand guidance. Leaf is now mostly a state colour;
         if you find yourself reaching for it as decoration, check that
         what you are colouring really is a claim that something is
         good, and use sea or sand if it is not.

   crab  CAUTION, AND NOTHING ELSE. A failed form, a "don't" rule. The
         one exception is .badge--crab, where it is a literal crab.

   Sections take ONE accent and carry it through kicker, underline,
   sticker, sub-label and bullet. A kicker in one colour beside an
   underline in another is a bug, not a flourish.

   One exception, and it is the reason sea is listed first: a sea section
   cannot tint its own labels, because sea-ink means "this is a link"
   everywhere else and a heading that goes nowhere must not wear it. So
   on a sea section, headings stay --ink and small labels go
   --text-soft. Sand and leaf sections have no such clash and tint
   normally, with --sand-ink and --leaf-ink.

   Four-up rows (.svc, .step, .game__features) are the one place colour
   carries no meaning, because there it is an index. They always run
   sea, sand, leaf, crab in that order so the rhythm repeats across
   pages rather than being re-picked each time.
   ============================================================ */

/* ============================================================
   DEEP WATER
   One theme, and it is dark. There is no light variant and no
   [data-theme] switch: the site is dark the way it is teal, as a
   decision rather than a mode, so the values below are simply the
   values. Nothing here waits on JavaScript.

   The palette is deep sea, not neutral grey. That is the whole trick.
   A dark page built by darkening a light one lands on grey, and grey
   reads as a light design with the lights off; every surface here
   carries the same cold blue cast the game does, so the dark is a
   colour the site chose rather than an absence of the one it had.

   THE BANDS ARE NOT SEPARATED BY LIGHTNESS. On paper the layout was
   warm ground interrupted by near-black water, and the contrast did
   all the work. Four near-blacks cannot do that - stacked up they read
   as one flat slab, which is what made the first attempt at this feel
   like an inversion. So the bands are told apart by ATMOSPHERE: each
   one carries teal and sand light bleeding in from its corners, strong
   enough to see, and the wave divider gives the boundary a hard edge.
   Lightness only has to carry the small steps, between a section and
   the card sitting on it.

   Depth comes from elevation, not from outline. On paper every card
   was a white plate behind a 2px near-black rule with the same rule
   offset below it as a shadow - a sticker. Inverted, that rule becomes
   a pale grey box round everything, which is the single most
   inverted-looking thing a dark page can do. Cards here are lifted
   instead: a lighter fill, a one-pixel rim catching light along the
   top, and a soft shadow underneath.

   The sticker survives in one place, .sticker itself, because it is
   sand on a photograph rather than a surface on a surface, and a hard
   dark outline is still exactly right there.
   ============================================================ */

:root {
  /* ---- brand constants ----
     The press kit shows these, so they are the real values and they do
     not move. --brand-ink is now LIGHTER than the page behind it, which
     is worth knowing before reaching for it as a shadow: use --hard. */
  --brand-ink: #14171c;
  --brand-paper: #f7f1e4;
  --sea: #0fa3b1;
  --sea-deep: #0b7f8b;
  --leaf: #55b368;
  --leaf-deep: #3e9451;
  --sand: #eda14d;
  --sand-deep: #d3822a;
  --crab: #e0584a;
  --crab-deep: #c14033;
  --sea-chip: #0c828d;
  --leaf-chip: #3c854a;
  --crab-chip: #db3b2b;

  /* ---- surfaces, deepest first ----
     --ink is the band, --paper-2 the alternate section, --paper the page
     and --plate a card raised off it. The wave dividers name --paper and
     --paper-2 inline in the markup, so those two keep their names.

     These went DOWN in lightness and UP in saturation at the same time,
     which is the only combination that gets you a darker page without
     getting you a greyer one. The page was hue 216 at 53% saturation; it
     is now hue 207 at 77%, and about a quarter darker. Less indigo, more
     sea, and unmistakably a colour rather than a dimmed neutral.

     Both halves of that matter and the reason is worth writing down,
     because a version of this file went the other way and was much worse.
     Darkening alone lands on near-black, and four near-blacks in a row are
     one flat slab. Neutralising as well - dropping the cast to a true grey
     - looks like the safe move and is the trap: the accents here are all
     cyan, so a blue-grey surface at least shares a family with them, and a
     neutral one leaves them floating on nothing. Do not neutralise these
     to make the accents pop. It does the opposite.

     Card separation improved on the way down: --plate against --paper is
     1.28 where it was 1.17, because --plate gained the saturation too. */
  --ink: #020a11;
  --paper-2: #03101a;
  --paper: #04131f;
  --plate: #0d2b40;
  --ink-2: #041220;

  /* ---- type ----
     Heading and body are unchanged and are a cool off-white; they were
     never the problem.

     THE SOFT TIER WAS THE PROBLEM. --text-soft and --text-faint used to
     sit at hue 207 and 18% saturation, which is air-force blue - a
     desaturated steel that is the single most tired colour you can put on
     a dark page, and it was carrying every lede, every caption and every
     bit of small print on the site. They are the same lightness now at
     hue 200 and roughly 35% saturation, so they belong to the sea instead
     of to the grey. Nothing else about them moved: this is a saturation
     fix, not a brightness one, and turning them UP would flatten the gap
     to body copy that is the only reason the tier exists.

     Measured against --plate, the lightest surface and so the worst case:
     heading 12.9, body 9.9, soft 6.5, faint 5.2. */
  --heading: #eaf2f8;
  --text-ink: #c9d6e2;
  --text-soft: #85b3c9;
  --text-faint: #6fa0b8;
  /* type on a band. The bands are darker still, so these ride up.

     --text-paper is the brand cream and it STAYS warm. It is the only warm
     type on the site and it is doing real work: a cold page whose bands
     carry warm type has somewhere for the eye to go, and one that is cool
     all the way down is a mood board. Its soft partner used to be a
     neutral #b9bcc4 sitting directly under it, which read as two unrelated
     colours stacked rather than one voice at two strengths - so it is a
     dimmed cream now instead of a grey. */
  --text-paper: #f2ecdd;
  --text-paper-soft: #cbc3b3;

  /* ---- accents ----
     This is where the colour on this site lives, and it is the tier that
     was pulling its punches. These used to be aliased straight to the
     brand constants above - the muted print values - and a muted teal on
     a dark page is a teal you have to look for.

     They are lifted, and the direction was not a guess. The press kit two
     hundred lines down ships the brand's own blues as #05CDF2 and #08E2F2,
     vivid cyan, and the site's --sea is a print-safe reduction of them. So
     going brighter here goes TOWARD the brand rather than away from it,
     and --sea-ink is now essentially that cyan. The constants themselves
     do not move: they are what the kit hands out, these are what the site
     paints.

     Measured on --plate, the worst case: sea 7.7, leaf 8.1, sand 8.1,
     crab 5.7, all well clear of 4.5 - the deeper page bought the headroom
     that paid for this.

     --focus is separate from --sea-chip and has to be. The chips are sized
     so #fff clears 4.5 on them, so they cannot brighten with the page; a
     3px focus ring wants to be the loudest thing on screen. Two jobs
     pulling opposite ways, so two values. */
  --sea-ink: #24cdf2;
  --leaf-ink: #5fd975;
  --sand-ink: #ffb055;
  --crab-ink: #ff7a66;
  --focus: #3ad8f5;

  /* ---- elevation ----
     --line is a rim, not an outline: one pixel of light along the edge of
     a raised surface. The light is cyan rather than white now. White at
     9% over a blue surface composites to a dusty grey hairline, and that
     hairline is on every card on every page - one slightly wrong colour
     repeated eighty times, which is a bigger contribution to how a page
     feels than any single element it outlines.

     --line-strong is the exception, for controls drawn as an icon in a
     circle with no text to identify them: there the boundary IS the
     control, so 1.4.11 applies, and it measures 6.0 on --plate against
     the 3 it needs. It was #55748c, which is air-force blue by any name -
     the exact colour the whole of this pass exists to get rid of. It is
     frankly cyan now, which reads as a violation of the colour rules at
     the top of the file, and is not: these two controls are the one place
     on the site where the border is the interactive element, so teal is
     the honest colour for them rather than a borrowed one.

     --hard is the one genuinely black value, for the sticker's outline and
     drop, and went blacker to stay clear of --ink. */
  --line: rgba(140, 230, 252, .12);
  --line-hover: rgba(140, 230, 252, .30);
  --line-strong: #2fb4d6;
  --hard: #01050a;
  --rim: inset 0 1px 0 rgba(255, 255, 255, .06);
  --lift: 0 10px 28px -14px rgba(0, 0, 0, .9);
  --lift-hover: 0 18px 40px -16px rgba(0, 0, 0, .95);
  --shadow-pop: inset 0 1px 0 rgba(255, 255, 255, .05), 0 22px 48px -20px rgba(0, 0, 0, .95);

  /* ---- the inverted fill ----
     .btn--ink and the press pills reverse out of the page entirely. */
  --invert-bg: #e8eef5;
  --invert-fg: #0a121e;
  --invert-bg-hover: #ffffff;

  /* ---- the four index accents at tick weight ----
     Pointed at the on-dark tier rather than at the constants, so a tick
     and the link beside it are the same colour. -deep is never right here:
     it exists to survive a light background and is mud on this one. */
  --tick-sea: var(--sea-ink);
  --tick-sand: var(--sand-ink);
  --tick-leaf: var(--leaf-ink);
  --tick-crab: var(--crab-ink);

  color-scheme: dark;

  --font-display: "Bricolage Grotesque", "Arial Black", sans-serif;
  --font-body: "Instrument Sans", system-ui, sans-serif;

  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;
  --ease-out: cubic-bezier(.22, 1, .36, 1);
}



/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* scrollbar-gutter keeps the scrollbar's 15px reserved whether or not the page
   is long enough to need one. Without it the content box is 15px narrower on a
   scrolling page than on a short one, so --shell resolves against a different
   width and every centred container - the nav bar included - jumps 7.5px
   sideways on the way from a long page to a short one. */
/* The collapsed nav is 60px: 8px of padding either side of a 44px row, which is
   the AAA target-size floor rather than anything to do with the type. 88 clears
   it by 28px, the same breathing room the old 96 gave the 72px pill. */
html { scroll-behavior: smooth; scroll-padding-top: 88px; scrollbar-gutter: stable; }

/* seamless cross-page transitions (progressive enhancement) */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .22s; }
/* The bar is fixed furniture, not content. It must not travel with the root
   snapshot - unnamed, it gets captured as part of it, and the cross-fade rescales
   that whole snapshot whenever two pages differ in size, which is what made the
   nav appear to grow and shrink between pages. Naming it lifts it out.

   What it must ALSO not do is cross-fade. The bar is not pixel-identical between
   pages: the home page alone wears .nav--hero, and before the first scroll that
   bar is 94px tall with a 40px wordmark against 60px and 24px everywhere else.
   Two earlier attempts at that mismatch both failed, in opposite directions, and
   both are worth naming so neither gets tried again:

   One shared name with the group pinned STRETCHED the bar. A pinned group takes
   the new size immediately, so the old 60px snapshot was painted into a 94px box
   and scaled by half again on the way in.

   A name per state GHOSTED it. With no counterpart to pair with, the old bar ran
   its exit fade while the new bar ran its entry fade, so for a fraction of a
   second there were visibly two nav bars stacked on the screen.

   Both come from trying to animate between two bars that are not the same shape.
   So: don't. One name, group pinned, old snapshot dropped outright and the new
   one shown with no animation at all. The bar is simply already correct when the
   new page arrives, while the page body behind it still cross-fades. There is
   nothing left to stretch and nothing left to double. */
.nav { view-transition-name: site-nav; }
::view-transition-group(site-nav) { animation: none; }
::view-transition-old(site-nav) { animation: none; display: none; }
::view-transition-new(site-nav) { animation: none; }
body {
  font-family: var(--font-body);
  color: var(--text-ink);
  background: var(--paper);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img, video, svg { display: block; max-width: 100%; }
/* width/height attributes on <img> reserve space and stop layout shift, but they
   also set a presentational height that would beat any CSS aspect-ratio.
   height:auto hands sizing back to the stylesheet. */
img { height: auto; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -.01em;
  text-wrap: balance;
}

/* One shell width for the nav, the hero and every section, so their left edges
   agree at every size. The 92vw is for phones. Above 900px it tightens to 86vw,
   because between roughly 1150 and 1350 CSS px the 1160px cap was landing almost
   flush with the viewport - at 1280 that is a 60px gutter, which reads as
   claustrophobic on a screen you sit back from. Past ~1350px the cap wins again
   and nothing changes. */
:root { --shell: min(1160px, 92vw); }
@media (min-width: 900px) { :root { --shell: min(1160px, 86vw); } }

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

.skip-link {
  position: fixed; top: -60px; left: 16px; z-index: 200;
  background: var(--invert-bg); color: var(--invert-fg); padding: .6rem 1rem;
  border-radius: var(--radius-sm); transition: top .2s;
}
.skip-link:focus { top: 12px; }

/* 2.4.7 Focus Visible + 1.4.11 Non-text Contrast: one unmistakable ring
   everywhere, rather than relying on the browser default (which disappears
   against the ink sections). Both colours clear 3:1 on their own backdrop.
   Component-level rules with higher specificity still win where they exist. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}
.nav :focus-visible, .hero :focus-visible, .services :focus-visible,
.proof :focus-visible, .footer :focus-visible, .lightbox :focus-visible,
.community :focus-visible {
  outline-color: var(--sand);
}

::selection { background: var(--sand); color: var(--ink); }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: .55rem;
  font-family: var(--font-display); font-weight: 700; font-size: 1rem;
  padding: .85rem 1.5rem; border-radius: var(--radius);
  border: 2px solid transparent;
  transition: transform .18s var(--ease-out), box-shadow .18s, background .18s, color .18s;
  cursor: pointer; white-space: nowrap;
}
.btn svg { width: 1em; height: 1em; }
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn--sand { background: var(--sand); color: var(--ink); box-shadow: 0 6px 20px -6px rgba(237, 161, 77, .55); }
.btn--sand:hover { background: var(--sand-deep); }
.btn--ink { background: var(--invert-bg); color: var(--invert-fg); }
.btn--ink:hover { background: var(--invert-bg-hover); }
/* A light button, for use on a dark band - so it is pinned to the brand
   constants rather than the theme. On dark, --paper IS the band. */
.btn--paper { background: var(--brand-paper); color: var(--brand-ink); }
.btn--paper:hover { background: #fff; }
.btn--ghost { border-color: rgba(255,255,255,.35); color: #fff; }
.btn--ghost:hover { border-color: #fff; background: rgba(255,255,255,.08); }
.btn--ghost-ink { border-color: rgba(255, 255, 255, .3); color: var(--heading); }
.btn--ghost-ink:hover { border-color: var(--line); background: rgba(255, 255, 255, .05); }

/* ---------- decorations ---------- */
.kicker {
  font-family: var(--font-display); font-weight: 700;
  font-size: .85rem; letter-spacing: .14em; text-transform: uppercase;
  display: inline-flex; align-items: center; gap: .5rem;
  margin-bottom: 1rem;
}
.kicker::before { content: ""; width: 26px; height: 3px; border-radius: 2px; background: currentColor; }
.kicker--sea { color: var(--sea-ink); }
.kicker--leaf { color: var(--leaf-ink); }
/* Sand is the one accent that changes value with its backdrop, so it is the one
   that has to be told where it is. The safe tier is the default: --sand-deep is
   2.60:1 on --paper, and this used to be set to it unconditionally on the claim
   that every kicker--sand sat on ink. The publishers pitch on games.html does
   not - it is a paper-to-white gradient - so that kicker was failing 1.4.3 in
   the field. Defaulting to --sand-ink and brightening only inside the ink bands
   means the mistake cannot recur: a sand kicker dropped on a light section now
   lands on the passing colour by itself. */
.kicker--sand { color: var(--sand-ink); }
.hero .kicker--sand,
.proof .kicker--sand,
.community .kicker--sand,
.footer .kicker--sand { color: var(--sand); }

/* marker-style underline accents */
/* The band used to be .3em tall sitting .06em up, which put its top edge 31% of
   the way through the x-height - through the bottom third of every lowercase
   letter. Reviewers read it as a strikethrough, which is fair: a highlight
   covers the whole x-height and an underline covers none of it, and this sat in
   between. Now .15em, topping out level with the baseline, so it reads as one
   pass of a marker under the word. Still thick enough to keep the character. */
[class^="underline-"] { position: relative; white-space: nowrap; z-index: 0; display: inline-block; }
[class^="underline-"]::after {
  content: ""; position: absolute; left: -2%; right: -2%; bottom: .04em; height: .15em;
  border-radius: 3px; z-index: -1; transform: rotate(-1deg);
}
.underline-sea::after { background: color-mix(in srgb, var(--sea) 60%, transparent); }
.underline-sand::after { background: color-mix(in srgb, var(--sand) 80%, transparent); }
.underline-leaf::after { background: color-mix(in srgb, var(--leaf) 60%, transparent); }

.squiggle { position: relative; display: inline-block; }
.squiggle::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -.18em; height: .22em;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 12 2, 22 8 T 42 8 T 62 8 T 82 8 T 102 8 T 122 8' fill='none' stroke='%23eda14d' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E") repeat-x;
  background-size: auto 100%;
}

.dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  background: var(--leaf); margin-right: .15rem;
}
.dot--pulse { animation: pulse 2s infinite; }
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(85, 179, 104, .5); }
  50% { box-shadow: 0 0 0 7px rgba(85, 179, 104, 0); }
}

.sticker {
  position: absolute;
  font-family: var(--font-display); font-weight: 700; font-size: .85rem;
  padding: .45rem 1rem; border-radius: var(--radius-sm);
  border: 2px solid var(--hard);
  box-shadow: 0 3px 0 var(--hard);
  transform: rotate(-4deg);
}
/* One sticker colour, on purpose. A sticker is always shouting about something
   we want you to notice, which makes it sand by definition; the teal variant
   that used to sit here was never placed on a page and only ever offered a
   second answer to a question that has one. */
.sticker--sand { background: var(--sand); color: var(--ink); }

/* ---------- nav ---------- */
/* The bare edge. There is no bar over the hero at all: the wordmark sits large
   on the artwork and the links are plain type beside it, and the whole thing
   collapses into a slim ink strip as soon as the page moves.

   Only the home page opens on the ink hero. Every other page opens on paper,
   where white type over nothing would be invisible, so they start in the
   collapsed state and stay there - the bare treatment is a hero treatment, and
   a page without a hero is already at rest. index.html is the only page whose
   header carries .nav--hero, and the collapsed look below is the default
   precisely so that forgetting the modifier fails safe (legible, just not
   bare) rather than fails invisible. */
.nav {
  position: fixed; inset-inline: 0; top: 0; z-index: 100;
  pointer-events: none;
  /* Solid, not frosted. This was --ink at 93% over a 12px backdrop blur, which
     is the stock header recipe and, at 93% opaque, a compositing layer buying
     about seven percent of blur nobody can see. The bar is opaque ink now: the
     content scrolls under it and stops, rather than smearing through it. */
  background: var(--ink);
  box-shadow: 0 1px 0 rgba(255,255,255,.09);
  transition: background .3s var(--ease-out), box-shadow .3s;
}
.nav.is-scrolled {
  box-shadow: 0 1px 0 rgba(255,255,255,.09), 0 14px 30px -20px rgba(0,0,0,.6);
}
.nav__inner {
  pointer-events: auto;
  /* the same shell as the content, so the bar's edges line up with the text
     under it instead of sitting 1vw proud of it */
  width: var(--shell); margin-inline: auto;
  display: flex; align-items: center; justify-content: space-between;
  padding: .5rem 0;
  transition: padding .3s var(--ease-out);
}

/* Home page, before the first scroll: no bar, just type on the hero. The
   is-open guard is for the phone menu - the dropdown needs something solid
   behind the bar it hangs from. */
.nav--hero:not(.is-scrolled):not(.is-open) {
  background: transparent;
  box-shadow: none;
}
.nav--hero:not(.is-scrolled):not(.is-open) .nav__inner { padding: 1.55rem 0; }
.nav--hero:not(.is-scrolled):not(.is-open) .nav__wordmark { height: 40px; }

/* 2.5.5 Target Size (Enhanced), AAA: every non-inline target is at least 44x44. */
/* The wordmark alone. The moth mark that used to sit to its left is now painted
   large in the hero, where the linework actually reads; at 38px in a nav bar it
   was mud, which is where "the logo is too small" came from. */
.nav__brand { display: flex; align-items: center; min-height: 44px; }
.nav__wordmark {
  height: 24px; width: auto; display: block;
  transition: height .3s var(--ease-out), opacity .15s;
}
.nav__brand:hover .nav__wordmark { opacity: .75; }

.nav__links { display: flex; align-items: center; gap: 1.6rem; }
.nav__links a {
  position: relative;
  color: var(--text-paper); font-weight: 500; font-size: .95rem;
  white-space: nowrap;
  display: inline-flex; align-items: center; min-height: 44px;
  transition: color .15s;
}
.nav__links a:hover { color: #fff; }
/* The pill backgrounds are gone, so hover and current state are carried by a
   sand rule that wipes in from the left. It is drawn on the text baseline
   rather than the bottom of the 44px target, or it would float well clear of
   the word it belongs to. */
.nav__links a:not(.nav__cta)::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 12px; height: 2px;
  background: var(--sand); transform: scaleX(0); transform-origin: left;
  transition: transform .25s var(--ease-out);
}
.nav__links a:not(.nav__cta):hover::after,
.nav__links a[aria-current="page"]::after { transform: scaleX(1); }

.nav__links .nav__cta { color: var(--sand); font-weight: 600; gap: .4rem; }
.nav__links .nav__cta:hover { color: var(--sand); }
/* The same arrow the buttons and backlinks use, as a mask so it takes
   currentColor and needs no markup change on twenty-four pages. Behind
   @supports because a browser without masking would paint the box itself - a
   solid sand square - rather than nothing. */
@supports ((mask-image: none) or (-webkit-mask-image: none)) {
  .nav__links .nav__cta::after {
    content: ""; width: 15px; height: 15px; flex: none;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 8h11M9 3.5 13.5 8 9 12.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 8h11M9 3.5 13.5 8 9 12.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    transition: transform .2s var(--ease-out);
  }
  .nav__links .nav__cta:hover::after { transform: translateX(3px); }
}

.nav__burger {
  display: none; flex-direction: column; justify-content: center; align-items: center;
  gap: 6px;
  background: none; border: 0; padding: .8rem; cursor: pointer;
  /* Two 2px bars and a 6px gap is 10px of content; .8rem of padding took the
     button to 36, not the 44 the accessibility statement promises. This is the
     only way to open the menu on a phone, so it is the last control that should
     be under size. Centring keeps the bars where they were. */
  min-width: 44px; min-height: 44px; box-sizing: border-box;
}
.nav__burger span {
  width: 22px; height: 2px; background: #fff; border-radius: 2px;
  transition: transform .25s var(--ease-out);
}
.nav__burger[aria-expanded="true"] span:first-child { transform: translateY(4px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:last-child { transform: translateY(-4px) rotate(-45deg); }


/* ---------- hero ---------- */
.hero {
  position: relative;
  /* Flat ink, no glows. Three corner radials used to sit here - cyan top right,
     sand bottom left, leaf bottom centre - and together they read as the stock
     dark-landing-page wash rather than as anything of ours. Nothing on the page
     is lit from those directions, so the light they implied was decoration
     pretending to be depth. The white linework and the wave carry the hero on
     their own; the ink just has to stay out of their way. */
  background: var(--ink);
  color: #fff;
  /* a touch more ink above and below the introduction - the logo needs room to
     breathe now it is the first thing on the page rather than a nav trinket */
  padding: clamp(9rem, 17vh, 12rem) 0 clamp(7rem, 14vh, 9.5rem);
  overflow: hidden;
}
.hero__inner {
  width: var(--shell); margin-inline: auto;
  display: grid; grid-template-columns: 1.05fr .95fr;
  gap: clamp(2rem, 5vw, 4.5rem); align-items: center;
}
.hero__title {
  font-size: clamp(2.5rem, 5.4vw, 4.35rem);
  margin-bottom: 1.4rem;
}
.hero__sub {
  color: var(--text-paper-soft); font-size: 1.1rem;
  max-width: 52ch; margin-bottom: 2.2rem;
}
.hero__actions { display: flex; flex-wrap: wrap; gap: .9rem; }

.hero__frame {
  position: relative;
  border-radius: var(--radius-lg);
  transform: rotate(2deg);
  transition: transform .4s var(--ease-out);
  box-shadow: 0 30px 60px -20px rgba(0,0,0,.55);
}
.hero__frame:hover { transform: rotate(.5deg) translateY(-6px); }
.hero__frame img {
  border-radius: var(--radius-lg);
  border: 3px solid rgba(255,255,255,.14);
  aspect-ratio: 16/10; object-fit: cover; width: 100%;
}
.hero__frame .sticker--sand { top: -14px; left: -10px; }

/* The logo, at a size where the linework reads. It's transparent white vector
   on the dark hero, so it needs no plate behind it, and 400px is a layout
   choice rather than a sharpness ceiling now that the source is an SVG. */
.hero__logo {
  width: min(400px, 100%); height: auto;
  margin-inline: auto;
  filter: drop-shadow(0 18px 40px rgba(0, 0, 0, .45));
}

.wave { position: absolute; inset-inline: 0; bottom: -1px; line-height: 0; }
.wave svg { width: 100%; height: clamp(40px, 7vw, 90px); }
.wave--top { top: -1px; bottom: auto; }

/* ---------- recognition & programmes ---------- */
/* Names as type, not logos: we don't hold usage rights to any of these marks,
   and a text credit needs none.

   This band spent a long time as a bare strip of four programme names. When the
   Tranzfuser win finally had a photograph, the picture was bolted on beside them
   - and four short names have nothing like the visual weight of a photograph, so
   the band read as lopsided at every width no matter how the two were sized
   against each other. It now takes the shape the rest of the page already uses
   whenever an image has to carry a point: framed photo one side, kicker, heading
   and copy the other, exactly like the proof band and the studio band below.
   Giving the picture real copy to sit against is the thing that fixes the
   balance; sizing the two halves against each other only moved it around. */
.creds { background: var(--paper); padding-top: clamp(2.5rem, 5vw, 4rem); }
/* The section that follows is on the same paper and opens with its own heading,
   so it doesn't need a full section's worth of run-up on top of this one's. */
.creds + .section { padding-top: clamp(2rem, 4vw, 3.25rem); }
.creds__inner {
  display: grid; grid-template-columns: .95fr 1.05fr;
  gap: clamp(2.5rem, 6vw, 5rem); align-items: center;
}
.creds__frame { position: relative; }
.creds__frame img {
  width: 100%; height: auto; display: block;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-pop);
  transform: rotate(-2deg);
  transition: transform .35s var(--ease-out);
}
.creds__frame:hover img { transform: rotate(-.5deg) translateY(-5px); }
/* Bottom-left, matching the proof and studio frames. It used to have to be this
   side because bottom-right was where the crocheted turtle was; that photo has
   been replaced by the certificate shot, which has nothing in either bottom
   corner, so this is now just the house position. */
.creds__frame .sticker { bottom: -10px; left: 20px; }
.creds__copy h2 { font-size: clamp(1.9rem, 3.6vw, 2.9rem); margin-bottom: 1.1rem; }
/* :not(.kicker) because the kicker is a <p> too, and ".creds__copy p" outweighs
   ".kicker--leaf" - so the Recognition eyebrow has been rendering in body grey
   this whole time while the class on it said leaf. Same story in .studio__copy.
   Any new section rule that colours a bare <p> needs this exclusion. */
.creds__copy p:not(.kicker) { color: var(--text-soft); margin-bottom: 1.5rem; max-width: 56ch; }
/* The "Part of" block used to close this column, so the last paragraph's margin
   had something to push against. It is its own band now, and the leftover
   24px was quietly pulling the copy off centre against the photo. */
.creds__copy > p:last-child { margin-bottom: 0; }
/* The "Part of" block spans the band rather than sitting in the copy column -
   see the note on it in index.html for why the two lockups need the width. */
.creds__part {
  grid-column: 1 / -1;
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: clamp(1.5rem, 3vw, 2.2rem);
  border-top: 1px solid rgba(255, 255, 255, .12);
}
/* The label sits ABOVE the list rather than inline beside it - inline pushed the
   names to a left edge nothing else on the page shared. */
.creds__label {
  font-family: var(--font-display); font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--leaf-ink);
  margin-bottom: .7rem;
}
/* Four equal columns, not a wrapping row. The marks sit straight on the paper
   with no plate behind them, so the even columns and the gap are the only thing
   holding them together as one row - which is why the sizing below matters more
   here than it did when each had a box to be centred in. Two columns from
   1080px down, where four would squeeze the two single-line lockups below the
   size of the other two. */
.creds__list {
  list-style: none;
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.25rem, 2.5vw, 2rem);
  /* Rows size themselves, so once this wraps to 2x2 the row holding the tallest
     lockup came out deeper than the row below it, and the marks stopped sitting
     on an even rhythm. 1fr equalises the rows; each mark is then centred in an
     identical band. */
  grid-auto-rows: 1fr;
  /* No text left in here, and these stay anyway: every item is an image, so
     these rules are what a failed load falls back into. */
  font-family: var(--font-display); font-weight: 700; font-size: 1rem;
  color: var(--text-ink);
}
.creds__list li { display: flex; align-items: center; justify-content: center; text-align: center; }
/* Every mark is a multiple of ONE shared width, so the four stay in step at any
   size: all of them are on the percentage below ~230px of column, all of them
   are on the pixel cap above it, and they cross over together. Change --mark
   and the other three follow. The percentage is also what stops the wide ones
   overflowing on a phone, so none of them needs a max-width escape hatch.

   The multiples are not arithmetic. These four are wildly different shapes -
   3.28:1 for UK Games Fund, 2.85:1 for Game Republic, 6.48:1 and 6.71:1 for the
   two single-line ones - and their lettering takes up anywhere from 0.20 to 0.40
   of their artwork, because Game Republic's is a small wordmark under a tall
   badge while the Forum's fills its box. Matching any single dimension leaves
   one of them visibly wrong, and matching cap heights exactly - which is what
   the previous pair did - shrinks Tranzfuser to a 13px whisper, since it is one
   short line of thin, pale strokes rather than two of solid caps. These were
   set by rendering all four together and comparing, which is the only thing
   that works once a set is this mixed. Re-render before changing one.

   Both numbers came down when the plates came off. The percentage was measured
   against a tile's content box, inside 1rem of padding; with no plate there is
   no padding, so 65 would have run the widest mark almost edge to edge of its
   column - 58 leaves the Forum at ~84%, which is the margin the padding used to
   provide. And a mark with a plate behind it reads smaller than the same mark
   bare, because the plate gives the eye an edge to measure it against, so the
   cap came down from 144 to keep the row at the weight it had in boxes. */
.creds__list { --mark: min(132px, 58%); }
/* No plates. These four sit straight on the page like everything else.
   Getting there took a different answer per mark, because they are supplied
   artwork drawn for a light ground and we cannot redraw them:

   Game Republic ships a dark-mode lockup of its own now, so it just works.

   The Entrepreneurs' Forum mark is the one that can be inverted, and the
   only one: sampling the pixels, 100% of its ink is dark and 0% of it is
   chromatic, so it is pure monochrome and flipping it produces white rather
   than somebody else's brand in the wrong colour. Inverted, all of its ink
   clears 3:1 on --paper, worst case 3.62.

   Tranzfuser needs nothing. None of its ink is dark and 95% of it already
   clears 3:1 here.

   UK Games Fund is the awkward one and gets brightened rather than
   inverted. Its ink is entirely chromatic - magenta, pink and green - so an
   invert would print the wrong brand colours, but left alone only 40% of it
   clears 3:1 and over half sits in the 2-3:1 band, which reads as a smudge.
   A straight brightness lift keeps the hue (measured: about 3 degrees of
   drift at 1.5) and takes it to roughly 96% passing. It is a workaround for
   a mark we do not have a dark version of, not a house style - if the UK
   Games Fund ever supplies one, drop the filter and use it. */
.creds__mark--ef { filter: invert(1); }
.creds__mark--ukgf { filter: brightness(1.55); }
.creds__mark { width: var(--mark); height: auto; }
.creds__mark--gr { width: calc(var(--mark) * 1.100); }
.creds__mark--ef { width: calc(var(--mark) * 1.444); }
.creds__mark--tf { width: calc(var(--mark) * 1.250); }
/* Width has to be definite on the one mark that is wrapped in a link, or the
   percentage above resolves against the anchor's own shrink-to-fit width -
   which depends on the image - and the mark lands at its intrinsic size. It
   also gives the one clickable thing in the row a full-column hit area rather
   than a logo-sized one. */
.creds__list a { display: flex; align-items: center; justify-content: center; width: 100%; }
/* With the plates gone there is no border or background left to light up, so
   the lift does the whole job - the same move the Tranzfuser photo above makes
   on hover. It is on the mark, not the anchor, because the anchor is a full
   column wide and moving that would drag empty paper around with it. */
.creds__list a .creds__mark { transition: transform .3s var(--ease-out); }
.creds__list a:hover .creds__mark { transform: translateY(-4px); }
/* No box to hang a ring on any more, so it goes on the anchor and stands off
   far enough to clear the artwork. */
.creds__list a:focus-visible { outline: 3px solid var(--focus); outline-offset: 6px; border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: reduce) {
  .creds__list a:hover .creds__mark { transform: none; }
}
@media (max-width: 1080px) {
  .creds__list { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .creds__list { font-size: .92rem; }
}
/* One column, not a 2x2, on a phone. The Forum lockup is already at ~84% of its
   column - the rest is the breathing room the marks need with nothing behind
   them - so in two columns below this width its letters drop to around 8px, and
   "ENTREPRENEURS' FORUM" stops being readable long before it stops fitting.
   Tranzfuser, thinner and paler, goes first. Full width puts all four back on
   their pixel cap at the cost of ~350px of scroll, which is the right trade for
   marks you are meant to recognise. */
@media (max-width: 480px) {
  .creds__list { grid-template-columns: 1fr; }
}

/* ---------- sections ---------- */
/* Every section carried this top AND bottom, so consecutive sections put 230px
   of nothing between them - worst where neighbours share a background and there
   is no colour change to justify it. The second of any pair now gets a shorter
   run-up: the first section's bottom padding is already doing that job. The vw
   term is gentler too, because it is what made the gaps grow out of proportion
   to the text at browser zoom levels below 100%. */
.section { padding: clamp(3.5rem, 6vw, 5.5rem) 0; }
.section + .section { padding-top: clamp(2.5rem, 4vw, 3.5rem); }
/* A page that opens straight into a section instead of a hero still has to clear
   the fixed nav - otherwise the first heading sits behind it. The hero pages get
   this from the hero's own top padding. */
main > .section:first-child { padding-top: clamp(7.5rem, 12vh, 9.5rem); }
.section__head { max-width: 62ch; margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.section__head h2 { font-size: clamp(2.1rem, 4.2vw, 3.3rem); margin-bottom: 1.1rem; }
.section__lede { font-size: 1.13rem; color: var(--text-soft); }
/* smaller print under a lede: provenance, caveats, the honest asterisk.
   No opacity dimming here - it drops --text-soft under the 4.5:1 AA threshold. */
.section__note { font-size: .95rem; color: var(--text-soft); margin-top: .8rem; }
.section__head--light h2 { color: #fff; }
.section__head--light .section__lede { color: var(--text-paper-soft); }
.section__head--light .section__note { color: var(--text-paper-soft); }

/* ---------- game ---------- */
.game { background: var(--paper); padding-top: 3.5rem; }

/* Deliberately narrower than the shots below it. Full width, this clip reads as
   the trailer it isn't - it's two characters standing still while the camera
   drifts, and at hero size the lack of anything happening is the loudest thing
   on the page. Small enough to be an aside, it plays as one. */
.game__player {
  position: relative; border-radius: var(--radius-lg); overflow: hidden;
  background: var(--ink);
  box-shadow: var(--shadow-pop);
  max-width: 680px; margin-inline: auto;
  margin-bottom: clamp(2rem, 4vw, 3rem);
}
/* The img rule stays for whatever ends up in here next - a still, the trailer,
   or this clip again. */
.game__player video,
.game__player img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; }
.game__trailer-note {
  font-size: .93rem; font-style: italic;
  color: var(--text-soft);
  margin: 1rem auto 2.5rem; max-width: 60ch;
}

/* features: plain editorial list, no boxes */
.game__features {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 2rem 2.5rem;
  margin-bottom: clamp(2.5rem, 4vw, 3.5rem);
}
.game__features li {
  border-top: 3px solid var(--line);
  padding-top: 1.1rem;
  font-size: .97rem; color: var(--text-soft);
}
/* sea, sand, leaf, crab - the same order as .svc and .step. This row used to
   swap the middle two, which is invisible on its own page and obvious the
   moment you tab between this and the co-dev services. */
.game__features li:nth-child(1) { border-top-color: var(--sea); }
.game__features li:nth-child(2) { border-top-color: var(--sand); }
.game__features li:nth-child(3) { border-top-color: var(--leaf); }
.game__features li:nth-child(4) { border-top-color: var(--crab); }
.game__features strong {
  display: block; font-family: var(--font-display); font-weight: 700;
  color: var(--heading); font-size: 1.1rem; margin-bottom: .5rem; line-height: 1.25;
}

/* Two shots across on desktop, stacked on a phone. The clip above does the
   establishing work; these are the ones that show it's a whole world. */
.game__shots {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;
  margin-bottom: clamp(2.5rem, 4vw, 3.5rem);
}
.game__shots a {
  display: block; border-radius: var(--radius); overflow: hidden;
  border: 2px solid rgba(255, 255, 255, .12);
  transition: border-color .25s, box-shadow .25s, transform .25s var(--ease-out);
}
.game__shots a:hover {
  border-color: var(--line); box-shadow: var(--shadow-pop); transform: translateY(-3px);
}
.game__shots img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; }
@media (max-width: 760px) {
  .game__shots { grid-template-columns: 1fr; }
}

.game__cta {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1.5rem;
  border-top: 1px solid rgba(255, 255, 255, .15);
  border-bottom: 1px solid rgba(255, 255, 255, .15);
  padding: 1.8rem .2rem;
}
.game__status {
  font-family: var(--font-display); font-weight: 700; font-size: 1.15rem;
  display: flex; align-items: center; gap: .6rem;
}
.game__note { color: var(--text-soft); font-size: .98rem; }
.game__cta-buttons { display: flex; flex-wrap: wrap; gap: .8rem; }

/* ---------- for publishers ---------- */
/* A different audience to everything above it, so it gets a shift in background
   rather than being another block of the same page. The hairline rule it used
   to carry went when the community band moved in upstairs: its wavy bottom edge
   already draws the boundary, and a dead-straight line directly beneath a wave
   reads as a rendering mistake. Not dark: the footer directly below already is,
   and two dark bands in a row read as one. */
.pub {
  background: linear-gradient(180deg, var(--paper) 0%, var(--plate) 100%);
}
.pub__grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(1.6rem, 4vw, 3rem);
  align-items: start;
}
/* Scannable in one pass: a publisher is checking whether we clear a bar, not
   reading prose. Two columns so no answer is more than a line long. */
.factfile {
  display: grid; grid-template-columns: auto 1fr; gap: .7rem 1.2rem;
  background: var(--plate); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--rim), var(--lift);
  padding: 1.4rem 1.5rem;
}
.factfile dt {
  font-family: var(--font-display); font-weight: 700; font-size: .9rem;
  white-space: nowrap;
}
.factfile dd { color: var(--text-soft); font-size: .95rem; }
.pub__ask h3 { font-size: 1.15rem; margin-bottom: .5rem; }
.pub__ask > p { color: var(--text-soft); margin-bottom: 1.3rem; max-width: 48ch; }
.pub__buttons { display: flex; flex-wrap: wrap; gap: .8rem; }
.pub__note {
  font-size: .9rem; color: var(--text-soft); margin-top: 1.2rem; max-width: 48ch;
}
.pub__note a { color: var(--sea-ink); font-weight: 600; }
.pub__note a:hover { text-decoration: underline; }
@media (max-width: 820px) {
  .pub__grid { grid-template-columns: 1fr; }
}

/* ---------- discord community band (dark) ---------- */
/* Sand accent: a "join us" ask is the sell, and sand is the one accent that
   reads on ink. Same gradient recipe as .proof so the two ink bands feel like
   siblings rather than a new treatment. Centred because it carries exactly one
   thought and one button - a left-aligned band this short looks abandoned. */
.community {
  position: relative; /* anchors the wave dividers on both edges */
  background:
    radial-gradient(760px 480px at 8% 0%, rgba(36, 205, 242, .26), transparent 64%),
    var(--ink);
  color: var(--text-paper);
  text-align: center;
}
/* Both edges wear the wave, and each divider is up to 90px of svg plus 18px of
   drifting crests before any actual breathing room, so the band needs footer-
   grade padding on both sides. Doubled selector on purpose: a lone .community
   loses the padding-top fight to .section + .section. */
.section.community { padding: clamp(6rem, 10vw, 8.5rem) 0; }
.community__inner { display: grid; justify-items: center; }
.community h2 { color: #fff; font-size: clamp(1.8rem, 3.4vw, 2.6rem); margin-bottom: 1rem; }
.community__pitch { color: var(--text-paper-soft); max-width: 52ch; margin-bottom: 1.8rem; }
.community .btn svg { width: 1.25em; height: 1.25em; }
.community__note { font-size: .9rem; color: var(--text-paper-soft); margin-top: 1rem; }

/* ---------- newsletter strip ---------- */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.newsletter {
  padding: 1.8rem .2rem 0;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1.2rem 2rem;
}
.newsletter__copy h3 { font-size: 1.2rem; margin-bottom: .3rem; }
.newsletter__copy p { color: var(--text-soft); font-size: .95rem; max-width: 44ch; }
.newsletter__form { display: flex; flex-wrap: wrap; gap: .7rem; }
.newsletter__form input[type="email"] {
  font: inherit; padding: .8rem 1rem; min-width: 240px;
  border: 2px solid rgba(255, 255, 255, .25); border-radius: var(--radius);
  background: var(--plate); color: var(--text-ink);
}
.newsletter__form input[type="email"]:focus-visible {
  border-color: var(--sea-ink);
  /* --focus, matching the global :focus-visible ring. */
  outline: 3px solid var(--focus); outline-offset: 2px;
}

.newsletter .form-note { flex-basis: 100%; margin-bottom: 0; }
.form-note {
  font-family: var(--font-display); font-weight: 700; font-size: .98rem;
  border-radius: var(--radius);
  padding: .8rem 1.2rem;
  margin-bottom: 1rem;
}
.form-sent {
  color: var(--leaf-ink);
  background: color-mix(in srgb, var(--leaf) 14%, transparent);
  border: 2px solid var(--leaf-deep);
}
.form-error {
  color: var(--crab-ink);
  background: color-mix(in srgb, var(--crab) 12%, transparent);
  border: 2px solid var(--crab-deep);
}
.footer .form-sent {
  color: #b9e8c3;
  background: rgba(85,179,104,.15);
  border-color: rgba(85,179,104,.6);
  max-width: 480px; margin-inline: auto;
}
.footer .form-error {
  color: #f3b0a7;
  background: rgba(224,88,74,.15);
  border-color: rgba(224,88,74,.6);
  max-width: 480px; margin-inline: auto;
}

/* ---------- footer contact form ---------- */
.footer__form {
  max-width: 480px; margin: 0 auto 2.4rem;
  display: grid; gap: .8rem; text-align: left;
}
.footer__form input, .footer__form textarea {
  font: inherit; padding: .85rem 1rem;
  border: 2px solid rgba(255,255,255,.18); border-radius: var(--radius);
  background: rgba(255,255,255,.06); color: var(--text-paper);
  resize: vertical;
}
.footer__form input::placeholder, .footer__form textarea::placeholder { color: var(--text-paper-soft); }
.footer__form input:focus-visible, .footer__form textarea:focus-visible {
  border-color: var(--sand);
  background: rgba(255,255,255,.1);
  outline: 3px solid var(--sand); outline-offset: 2px;
}
.footer__form .btn { justify-self: center; }
/* honeypot: in the DOM for bots, genuinely zero-size for people. Without this the
   shared input padding above would inflate it back to a 36x31 box. */
.footer__form input[name="_gotcha"] { padding: 0; border: 0; }

/* ---------- co-dev enquiry form ----------
   Wider than the general contact form and split into two columns, because it
   asks the qualifying questions we would otherwise have to email back for:
   discipline, size, engine. Labels are visible rather than placeholder-only:
   this form is long enough that a placeholder disappearing on focus loses the
   question, and 3.3.2 wants the label to survive typing. */
.codev-form {
  max-width: 640px; margin: 0 auto 2.4rem;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem; text-align: left;
}
.codev-form .field { display: grid; gap: .35rem; min-width: 0; }
.codev-form .field--wide { grid-column: 1 / -1; }
/* The co-development group is hidden until the topic picker asks for it. It needs
   saying explicitly: .field is display:grid and .field--check is display:flex, and
   either one beats the [hidden] display:none the browser would otherwise apply. */
.codev-form .field[hidden], .codev-form .field--check[hidden] { display: none; }
.codev-form label {
  font-family: var(--font-display); font-weight: 700; font-size: .82rem;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-paper-soft);
}
.codev-form .label-optional { font-weight: 400; text-transform: none; letter-spacing: 0; opacity: .8; }
.codev-form input, .codev-form select, .codev-form textarea {
  font: inherit; padding: .8rem 1rem; width: 100%;
  border: 2px solid rgba(255,255,255,.18); border-radius: var(--radius);
  background: rgba(255,255,255,.06); color: var(--text-paper);
  resize: vertical;
}
.codev-form input::placeholder, .codev-form textarea::placeholder { color: var(--text-paper-soft); }
.codev-form input:focus-visible, .codev-form select:focus-visible, .codev-form textarea:focus-visible {
  border-color: var(--sand); background: rgba(255,255,255,.1);
  outline: 3px solid var(--sand); outline-offset: 2px;
}
/* the native menu is painted by the OS, so its options need a dark-on-light
   pairing of their own or they render as invisible text on Windows */
.codev-form select option { background: var(--ink-2); color: var(--text-paper); }
.codev-form .field--check {
  grid-column: 1 / -1;
  display: flex; align-items: flex-start;
  /* 2.5.5: the whole row is the target, not just the 20px box. The .7rem that
     used to sit here as a flex gap now lives on the label's padding-left, so it
     looks identical but is part of the target instead of dead space between two
     of them. Probing outwards from the box's centre used to leave the target
     11px later and land in the gap. */
  gap: 0;
  min-height: 44px; padding: .5rem 0;
}
.codev-form .field--check input {
  width: 24px; height: 24px; flex-shrink: 0; padding: 0;
  /* Line the box up with the middle of the label's FIRST line. At plain
     flex-start it sat flush with the top of the 44px row while the text sat
     centred in it, which read as the label hanging off the box's bottom edge. */
  margin-top: calc((44px - 24px) / 2);
  accent-color: var(--sand);
}
/* 2.5.5 again: a 24px box is the drawn control, not the target. Clicking the
   label toggles it too, so the label carries the row's full 44px height and the
   two together are comfortably tappable. That height is padding rather than a
   centred flex box on purpose: centring moves the first line down on one line
   and back up on two, so the box could only ever line up with one of them. */
.codev-form .field--check label {
  font-family: var(--font-body); font-weight: 400; font-size: .92rem;
  text-transform: none; letter-spacing: 0; line-height: 1.5;
  display: block; min-height: 44px;
  padding: calc((44px - 1.5em) / 2) 0 calc((44px - 1.5em) / 2) .7rem;
}
.codev-form .btn { grid-column: 1 / -1; justify-self: center; }
.codev-form .form-privacy {
  grid-column: 1 / -1;
  color: var(--text-paper-soft); text-align: center; max-width: none; margin-top: -.4rem;
}
.codev-form .form-privacy a:hover { color: #fff; }
/* The honeypot is .visually-hidden, but the rule above is more specific and was
   winning: width:100% on an absolutely positioned input resolved against the
   footer and pushed the page 147px wider than the viewport. Re-state the three
   properties .visually-hidden cares about that leak through. */
.codev-form input[name="_gotcha"] { padding: 0; border: 0; width: 1px; }
.footer .codev-form + .form-sent, .footer .codev-form + .form-error { max-width: 640px; }
@media (max-width: 620px) {
  .codev-form { grid-template-columns: 1fr; }
}

/* short "where this goes" note under each form, next to the submit button */
.form-privacy {
  flex-basis: 100%;
  font-size: .82rem; line-height: 1.5;
  color: var(--text-soft);
  max-width: 46ch; margin-top: -.1rem;
}
.form-privacy a { text-decoration: underline; text-underline-offset: 2px; }
.footer__form .form-privacy {
  color: var(--text-paper-soft);
  text-align: center; max-width: none; margin-top: -.4rem;
}
.footer__form .form-privacy a:hover { color: #fff; }

/* ---------- services (dark band) ---------- */
.services {
  background:
    radial-gradient(760px 500px at 0% 100%, rgba(36, 205, 242, .26), transparent 64%),
    var(--ink);
  color: var(--text-paper);
}
.services__grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  margin-bottom: 3rem;
  border-top: 1px solid rgba(255,255,255,.15);
}
.card {
  padding: 1.9rem 1.7rem 1.9rem 0;
  border-right: 1px solid rgba(255,255,255,.15);
  transition: transform .25s var(--ease-out);
}
.card:last-child { border-right: 0; }
.card:nth-child(2) { padding-left: 1.7rem; }
.card:nth-child(3) { padding-left: 1.7rem; padding-right: 0; }
.card:hover { transform: translateY(-4px); }
.card__num {
  font-family: var(--font-display); font-weight: 800; font-size: .9rem;
  color: var(--sand); letter-spacing: .1em; margin-bottom: 1.1rem;
}
.card h3 { color: #fff; font-size: 1.35rem; margin-bottom: .7rem; }
.card p { color: var(--text-paper-soft); font-size: .98rem; }

.services__cta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1.2rem;
}
.services__cta p { font-family: var(--font-display); font-weight: 700; font-size: 1.25rem; color: #fff; }

/* ---------- studio ---------- */
.studio { background: var(--paper); }
.studio__inner {
  display: grid; grid-template-columns: .9fr 1.1fr;
  gap: clamp(2.5rem, 6vw, 5rem); align-items: center;
}
.studio__photo { position: relative; }
.studio__photo img {
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-pop);
  transform: rotate(-2deg);
  transition: transform .35s var(--ease-out);
}
.studio__photo:hover img { transform: rotate(-.5deg) translateY(-4px); }
.studio__photo .sticker { bottom: -8px; left: 22px; }
.studio__copy h2 { font-size: clamp(2rem, 3.8vw, 3rem); margin-bottom: 1.2rem; }
.studio__copy p:not(.kicker) { color: var(--text-soft); margin-bottom: 1.1rem; max-width: 56ch; }
.studio__facts {
  display: flex; gap: 2.5rem; margin-top: 1.8rem; flex-wrap: wrap;
}
.studio__facts li { display: flex; flex-direction: column; }
.studio__facts strong {
  font-family: var(--font-display); font-weight: 800; font-size: 2rem; color: var(--leaf-ink);
  line-height: 1.1;
}
.studio__facts span { font-size: .92rem; color: var(--text-soft); }

/* ---------- team ---------- */
.team { background: var(--paper-2); }
.team__join {
  text-align: center; margin-top: 2.6rem;
  font-size: .95rem; color: var(--text-soft);
}
.team__join a { font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
.team__pool-head { margin-top: clamp(6rem, 11vw, 9rem); }
.team__grid {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.3rem;
}
/* Founders sit on their own row above the rest. They used to be centred in a
   calc()'d width, which put them on a fourth left edge - the grid keeps its four
   columns and the three of them simply fill the first three, so the row starts
   where every other block on the page starts. */
.team__grid--founders {
  padding-bottom: 2.2rem;
  margin-bottom: 2.2rem;
  border-bottom: 1px solid rgba(255, 255, 255, .15);
}
.member {
  display: block; min-width: 0;
  position: relative;
  text-align: center;
  padding-bottom: .4rem;
  transition: transform .25s var(--ease-out);
}
.member__status {
  position: absolute; top: .6rem; left: .6rem; z-index: 1;
  display: inline-flex; align-items: center; gap: .4rem;
  font-family: var(--font-display); font-weight: 700; font-size: .72rem;
  letter-spacing: .04em;
  padding: .25rem .6rem; border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--plate); color: var(--heading);
  box-shadow: var(--lift);
}
.member__status::before {
  content: ""; width: .5rem; height: .5rem; border-radius: 50%; flex-shrink: 0;
}
.member__status--active::before { background: var(--leaf); }
.member__status--former::before { background: rgba(255, 255, 255, .35); }
.team__label {
  font-family: var(--font-display); font-weight: 700; font-size: .85rem;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--sand-ink);
  margin-bottom: 1.2rem;
}
.member:hover { transform: translateY(-5px); }
.member__photo {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden; margin-bottom: 1rem;
  aspect-ratio: 1 / 1;
  border: 2px solid rgba(255, 255, 255, .12);
  transition: border-color .25s, box-shadow .25s;
}
.member:hover .member__photo { border-color: var(--line); box-shadow: var(--shadow-pop); }
.member__photo img {
  width: 100%; height: 100%; object-fit: cover; object-position: center 20%;
  transition: transform .4s var(--ease-out);
}
.member:hover .member__photo img { transform: scale(1.05); }
/* Zoom in on thumbnails where the subject is small in a wide shot.
   Tune per photo with --zx/--zy/--zs inline (shift right = +zx, shift up = -zy, zoom = --zs). */
.member__photo img.is-zoomed {
  --zx: 0%; --zy: -7%; --zs: 1.5;
  transform: translate(var(--zx), var(--zy)) scale(var(--zs));
  transform-origin: 50% 20%;
}
.member:hover .member__photo img.is-zoomed { transform: translate(var(--zx), var(--zy)) scale(calc(var(--zs) + .06)); }
.member__photo--empty {
  display: grid; place-items: center;
  background:
    radial-gradient(200px 200px at 70% 20%, rgba(36, 205, 242,.22), transparent 70%),
    var(--plate);
}
/* :not() so this only ever dresses the initials, never the "No profile yet"
   band that now shares this box - that inherited the 2.4rem and grew into a
   163px slab inside a 152px photo. */
.member__photo--empty > span:not(.member__flag) {
  font-family: var(--font-display); font-weight: 800; font-size: 2.4rem;
  color: rgba(255, 255, 255, .55);
}
.member h3 { font-size: 1.1rem; margin-bottom: .15rem; }
.member p { font-size: .88rem; color: var(--text-soft); }
/* "Profile coming soon" - a tile with no page behind it yet. It was in the link
   colour, which promised a destination it does not have. */
.member--placeholder p { color: var(--text-soft); font-weight: 500; }

/* The talent pool is guests and former shiverbugs, not the studio roster. Same
   tiles, packed tighter and typeset smaller, so the page shows the difference
   without needing the heading to explain it. */
.team__grid--pool {
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1rem;
}
.team__grid--pool h3 { font-size: .95rem; }
.team__grid--pool .member p { font-size: .82rem; }
.team__grid--pool .member__photo { margin-bottom: .7rem; }
.team__grid--pool .member__status { font-size: .65rem; padding: .2rem .5rem; }
.team__grid--pool .member__photo--empty > span:not(.member__flag) { font-size: 2rem; }

/* ---------- profiles with nothing written up yet ---------- */
/* These tiles are a <div>, not a link: there is no page worth landing on. JS
   turns them into buttons that reveal the note; until then they sit still, so
   the hover lift that promises "this goes somewhere" is dialled right back. */
.member--unfinished { cursor: default; }
.member--unfinished:hover { transform: none; }
.member--unfinished .member__photo { opacity: .72; }
.member--unfinished:hover .member__photo { border-color: rgba(255, 255, 255, .12); box-shadow: none; }
.member--unfinished:hover .member__photo img { transform: none; }
.member--unfinished:hover .member__photo img.is-zoomed {
  transform: translate(var(--zx), var(--zy)) scale(var(--zs));
}
/* only once JS has made it operable does it start behaving like a control */
.member--unfinished[role="button"] { cursor: pointer; }
.member--unfinished[role="button"]:hover .member__photo { opacity: .88; }
/* The tile says so up front. Everyone else in the grid is a link, so without
   this the only way to discover there's nothing behind this one was to click it
   and be told. A band across the foot of the photo rather than a corner chip:
   the corners are taken by the Active/Former status, and a flag that had to dodge
   it ended up sitting at a different height on different people. */
.member__flag {
  position: absolute; inset: auto 0 0 0; z-index: 1;
  /* The initials placeholder is a grid with place-items:center, and that
     centring is inherited by absolutely positioned children too - which
     stretched this band to 165px inside a 152px photo. Pin it explicitly. */
  place-self: end stretch;
  padding: .4rem .5rem;
  font-family: var(--font-display); font-weight: 700; font-size: .68rem;
  letter-spacing: .06em; text-align: center;
  /* No backdrop blur: 2px under an .88 fill is invisible, and it put a
     compositing layer on every tile in the roster. Opaquer instead. */
  background: rgba(20, 23, 28, .92); color: var(--text-paper);
}
.team__grid--pool .member__flag { font-size: .6rem; padding: .3rem .4rem; }
.member__soon {
  margin-top: .5rem;
  font-size: .82rem !important; line-height: 1.45;
  color: var(--text-ink) !important;
  background: var(--plate);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--rim), var(--lift);
  padding: .5rem .7rem;
}
.member__soon[hidden] { display: none; }

/* the same idea on the co-dev crew chips and artwork credits */
.person--unfinished { cursor: default; }
.person--unfinished:hover { transform: none; box-shadow: var(--rim), var(--lift); }
.person--unfinished[role="button"] { cursor: pointer; }
/* The artwork credit is attribution, not a way in, so it stays a plain label.
   Click-through keeps the whole tile opening the viewer instead of leaving a
   dead patch where the chip sits. */
.carousel__credit--unfinished { pointer-events: none; }
.svc__crew-note { max-width: 42ch; margin-top: .8rem; }

/* ---------- footer ---------- */
.footer {
  position: relative;
  background:
    radial-gradient(1000px 580px at 50% 0%, rgba(36, 205, 242, .26), transparent 68%),
    var(--ink);
  color: var(--text-paper);
  padding: clamp(6rem, 11vw, 9rem) 0 2.5rem;
  text-align: center;
}
/* No border-radius any more: the mark is transparent vector, so there is no
   black plate whose corners need softening. */
.footer__logo { width: 130px; height: auto; margin: 0 auto 1.6rem; }
.footer__main h2 {
  font-size: clamp(2.1rem, 4.5vw, 3.4rem); color: #fff; margin-bottom: 1.1rem;
}
/* "Get in touch" sends focus here (the heading carries #contact), and the ring
   was landing straight through the squiggle under "a Hand?" - same sand colour,
   same couple of pixels, so the underline read as broken. The squiggle hangs
   .18em below the heading box, so the offset is in em to clear it at every step
   of the clamp above, and the ring goes white to stay off the squiggle's colour. */
#contact:focus-visible {
  outline: 3px solid #fff;
  outline-offset: .3em;
  border-radius: 6px;
}
.footer__main > p { color: var(--text-paper-soft); max-width: 46ch; margin: 0 auto 2rem; }
.footer__email {
  display: inline-block; position: relative;
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.15rem, 2.6vw, 1.7rem);
  color: var(--sand);
  border-bottom: 3px solid color-mix(in srgb, var(--sand) 45%, transparent);
  margin-bottom: 2.2rem;
  transition: border-color .2s, color .2s;
}
/* This is a standalone link on its own line, not one inside a sentence, so the
   44px promise applies to it. At the small end of the clamp the text box is
   only 32 tall. Padding would work, but it would drag the underline away from
   the address it underlines; an overlay grows the target and leaves the type
   exactly where it is. */
.footer__email::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px;
}
.footer__email:hover { color: #fff; border-color: #fff; }
/* The direct social row, generated into every footer by tools/build-team.ps1.
   The profile-page treatment (white plate, hard ink drop shadow) has nothing to
   sit against down here on the ink, so these invert: a translucent disc that
   fills with sand on hover. 44px square is the target size, so the icon can
   stay small without the tap area following it down. */
.footer__socials { margin-bottom: 3.5rem; }
.footer__socials ul {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .7rem;
}
.footer__socials a {
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--text-paper);
  background: rgba(255, 255, 255, .07);
  border: 1px solid rgba(255, 255, 255, .18);
  transition: transform .2s var(--ease-out), background .2s, border-color .2s, color .2s;
}
.footer__socials svg { width: 20px; height: 20px; }
.footer__socials a:hover {
  transform: translateY(-3px);
  background: var(--sand); border-color: var(--sand); color: var(--ink);
}
/* The mini footer has no form above it, so the row sits much closer to the
   legal line and does not need the full gap the home page one carries. */
.footer--mini .footer__socials { margin-bottom: 2rem; }
.footer__meta {
  border-top: 1px solid rgba(255,255,255,.1);
  padding-top: 1.6rem;
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: .8rem;
  font-size: .9rem; color: var(--text-paper-soft);
}
.footer__tag { font-style: italic; }
/* Companies Act trading disclosure - required, so quiet but present on every page */
.footer__legal {
  flex-basis: 100%;
  font-size: .78rem; line-height: 1.5;
  color: var(--text-paper-soft); opacity: .75;
}
.footer__press { text-decoration: underline; text-underline-offset: 3px; }
.footer__press:hover { color: #fff; }

/* back button (2.4.8 Location). This replaced a Home / Team / Name crumb trail:
   the trail spent three small targets telling you where you'd been, when the
   only thing anyone ever wanted from it was the one step back. One button, named
   after where it goes, comfortably over the 44px target size. The two dark-hero
   pages (home, co-dev) don't carry one - they *are* the top of the trail. */
.backlink {
  display: inline-flex; align-items: center; gap: .55rem;
  min-height: 48px; padding: .7rem 1.35rem .7rem 1.1rem;
  margin-bottom: 1.6rem;
  font-family: var(--font-display); font-weight: 700; font-size: .95rem;
  color: var(--heading); background: var(--plate);
  border: 1px solid var(--line); border-radius: 999px;
  box-shadow: var(--rim), var(--lift);
  transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out), background .2s;
}
.backlink svg { width: 15px; height: 15px; flex: none; }
.backlink:hover { border-color: var(--line-hover); transform: translateY(-2px); box-shadow: var(--rim), var(--lift-hover); }
.backlink:active { transform: translateY(1px); box-shadow: var(--rim), var(--lift); }
/* the arrow leads the button, so it's the thing that should move on hover */
.backlink:hover svg { transform: translateX(-2px); }
.backlink svg { transition: transform .2s var(--ease-out); }

/* ---------- profile pages ---------- */
.profile {
  background: var(--paper);
  padding: clamp(7.5rem, 14vh, 10rem) 0 clamp(3rem, 6vw, 5rem);
  min-height: 60vh;
}
.profile__inner {
  display: grid; grid-template-columns: .8fr 1.2fr;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: start;
}
.profile__photo {
  position: sticky; top: 110px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-pop);
  transform: rotate(-1.5deg);
}
.profile__photo img { width: 100%; aspect-ratio: 3/4; object-fit: cover; object-position: center 10%; }
.profile__photo--empty {
  aspect-ratio: 3/4;
  display: grid; place-items: center;
  background:
    radial-gradient(300px 300px at 70% 20%, rgba(36, 205, 242,.22), transparent 70%),
    var(--plate);
}
.profile__photo--empty span {
  font-family: var(--font-display); font-weight: 800; font-size: 4.5rem;
  color: rgba(255, 255, 255, .55);
}
.profile__role { margin-bottom: .9rem; }
.profile__name { font-size: clamp(2.4rem, 5vw, 3.6rem); margin-bottom: .4rem; }
.profile__tagline {
  font-size: 1.25rem; font-style: italic; color: var(--text-soft);
  /* Sand, matching the rest of the team area, at the -ink tier: --sand is 1.90
     on paper and --sand-deep 2.67, both under 3:1, and this rule is 3px of
     colour with nothing else marking the quote. --sand-ink clears it at 4.94. */
  border-left: 3px solid var(--sand-ink); padding-left: 1rem;
  margin-bottom: 1.8rem; max-width: 48ch;
}
.profile__badges { display: flex; flex-wrap: wrap; gap: .7rem; margin-bottom: 2.2rem; }
.badge {
  display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--font-display); font-weight: 700; font-size: .9rem;
  padding: .5rem 1rem; border-radius: var(--radius-sm);
  border: 1px solid var(--line); box-shadow: var(--rim), var(--lift);
  background: var(--plate); color: var(--heading);
}
.badge--turtle { background: var(--leaf-chip); color: #fff; }
.badge--seagull { background: var(--sea-chip); color: #fff; }
.badge--crab { background: var(--crab-chip); color: #fff; }
.badge--founder { background: var(--sand); color: var(--brand-ink); }
.badge--active::before, .badge--former::before {
  content: ""; width: .6rem; height: .6rem; border-radius: 50%; flex-shrink: 0;
}
.badge--active::before { background: var(--leaf); }
.badge--former::before { background: rgba(255, 255, 255, .35); }
.profile__section { margin-bottom: 2.2rem; max-width: 60ch; }
.profile__about.is-clamped {
  max-height: 15rem; overflow: hidden; position: relative;
}
.profile__about.is-clamped::after {
  content: ""; position: absolute; inset-inline: 0; bottom: 0; height: 5rem;
  background: linear-gradient(to bottom, transparent, var(--paper));
}
.profile__more {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--font-display); font-weight: 700; font-size: .95rem;
  color: var(--sea-ink); margin-top: .5rem;
  /* Unstyled text on a bare button: 18 tall, on all sixteen profiles, and the
     only way to read the rest of someone's bio. It stays looking like a text
     link and gets a 44px target underneath. */
  position: relative;
}
.profile__more::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px;
}
.profile__more:hover { text-decoration: underline; }
.profile__section h2 {
  font-size: 1.05rem; text-transform: uppercase; letter-spacing: .12em;
  color: var(--sand-ink); margin-bottom: .8rem;
}
.profile__section p { color: var(--text-soft); margin-bottom: .9rem; }
.profile__section p a { color: var(--sea-ink); font-weight: 600; }
.profile__section p a:hover { text-decoration: underline; }
/* "bio coming soon" text. Muted, but #9a9284 was only 2.5:1 - this clears 4.5:1. */
.profile__section p.is-placeholder { font-style: italic; color: var(--text-faint); }
.profile__chips { display: flex; flex-wrap: wrap; gap: .6rem; }
.chip {
  font-size: .9rem; font-weight: 500;
  padding: .4rem .9rem; border-radius: var(--radius-sm);
  border: 1px solid rgba(255, 255, 255, .25);
}
.profile__socials p.is-placeholder { font-style: italic; color: var(--text-faint); }
.profile__socials { display: flex; flex-wrap: wrap; gap: 1.1rem; }
.social {
  display: flex; flex-direction: column; align-items: center; gap: .4rem;
  min-width: 52px;
}
.social__icon {
  width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center;
  border: 2px solid var(--line-strong); background: var(--plate); color: var(--heading);
  transition: transform .2s var(--ease-out), border-color .2s, color .2s;
}
.social__icon svg { width: 21px; height: 21px; }
.social:hover .social__icon { transform: translateY(-3px); border-color: var(--sand); color: var(--sand); }
.social__label { font-size: .74rem; font-weight: 600; color: var(--text-soft); }
.profile__nav {
  border-top: 1px solid rgba(255, 255, 255, .15);
  margin-top: clamp(2rem, 5vw, 3.5rem);
  padding-top: 1.6rem;
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  font-family: var(--font-display); font-weight: 700;
}
.profile__nav a { transition: color .15s; display: inline-flex; align-items: center; min-height: 44px; }
.profile__nav a:hover { color: var(--sea-ink); }
.profile__nav .is-back { color: var(--text-soft); }
.profile__nav .is-back:hover { color: var(--heading); }
.profile__missing { text-align: center; padding: 4rem 0; }
.profile__missing h1 { margin-bottom: 1rem; }
.profile__missing p { color: var(--text-soft); margin-bottom: 2rem; }

/* ---------- co-dev packages ---------- */
.packages { background: var(--paper-2); }
.packages__grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.4rem; align-items: start;
}
/* All three cards get the same treatment. The middle one used to be a solid
   slab of --sand next to two flat white ones, which read as a pricing-table
   gimmick rather than three equal options; the accent is now a rule along the
   top edge and a small tag, which points without shouting. */
.pkg {
  position: relative;
  display: flex; flex-direction: column; height: 100%;
  background: var(--paper);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--rim), var(--lift);
  padding: 1.7rem 1.5rem 1.5rem;
  transition: transform .25s var(--ease-out), box-shadow .25s var(--ease-out);
}
.pkg:hover { transform: translateY(-4px); border-color: var(--line-hover); box-shadow: var(--rim), var(--lift-hover); }
.pkg--feature::before {
  content: ""; position: absolute; inset: -2px -2px auto -2px; height: 6px;
  background: var(--sand);
  border: 2px solid var(--line); border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
}
.pkg--feature { padding-top: 2rem; }
.pkg__size {
  font-family: var(--font-display); font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--sand-ink); margin-bottom: .35rem;
}
.pkg__tag {
  display: inline-block; margin-left: .5rem;
  font-family: var(--font-body); font-weight: 600; font-size: .68rem;
  letter-spacing: .06em;
  padding: .15rem .5rem; border-radius: 999px;
  background: var(--sand); color: var(--ink);
}
.pkg__name { font-size: 1.35rem; margin-bottom: .6rem; }
.pkg__line { color: var(--text-soft); margin-bottom: 1.1rem; }
.pkg__list {
  display: flex; flex-direction: column; gap: .5rem;
  margin-bottom: 1.2rem; font-size: .93rem;
}
.pkg__list li { display: flex; gap: .6rem; color: var(--text-soft); line-height: 1.5; }
/* Sand, because packages is a sand section - these were the last teal thing in
   a card whose size label, top rule and tag are all sand. Deep tier, matching
   the .svc ticks. Both sit around 2.7:1 on paper, which is fine for a 7px dot:
   the text beside it carries the meaning, so 1.4.11 does not apply. */
.pkg__list li::before {
  content: ""; flex: none; width: 7px; height: 7px; margin-top: .5rem;
  border-radius: 50%; background: var(--tick-sand);
}
/* pinned to the bottom so the three "good when" lines sit on one row */
.pkg__for {
  margin-top: auto; padding-top: 1rem;
  border-top: 1px solid rgba(255, 255, 255, .15);
  font-size: .9rem; color: var(--text-soft);
}
.pkg__for strong { color: var(--text-ink); }
/* The whole card is the target, via this link stretched over it - one real link
   with a real name rather than a click handler on a <div>, so it tabs, it has a
   focus ring, and middle-click still works. Text inside stays selectable. */
.pkg__cta {
  display: inline-flex; align-items: center; gap: .45rem;
  margin-top: 1rem; min-height: 44px;
  font-family: var(--font-display); font-weight: 700; font-size: .92rem;
  color: var(--sea-ink);
}
.pkg__cta svg { width: 14px; height: 14px; transition: transform .2s var(--ease-out); }
.pkg__cta::after { content: ""; position: absolute; inset: 0; border-radius: var(--radius); }
.pkg:hover .pkg__cta { color: var(--heading); }
.pkg:hover .pkg__cta svg { transform: translateX(3px); }
/* the ring belongs on the card, not on the two words at the bottom of it */
.pkg:has(.pkg__cta:focus-visible) { outline: 3px solid var(--focus); outline-offset: 3px; }
/* Only take the ring off the link where :has() is around to put it back on the
   card. Unconditionally, a browser without :has() gets the outline:none and no
   replacement, which leaves these three CTAs with no focus indicator at all. */
@supports selector(:has(*)) {
  .pkg__cta:focus-visible { outline: none; }
}
.packages__note {
  margin-top: 1.8rem;
  color: var(--text-soft); max-width: 62ch;
}
.packages__note a { color: var(--sea-ink); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
.packages__note a:hover { color: var(--heading); }
@media (max-width: 900px) {
  .packages__grid { grid-template-columns: 1fr; }
}

/* ---------- press kit page ---------- */
.press .section__head { max-width: 68ch; }
.press .section__head h1,
.press .section__head h2 { font-size: clamp(2.1rem, 4.2vw, 3.3rem); margin-bottom: 1.1rem; }
.press .section__head a:not(.btn) { color: var(--sea-ink); font-weight: 600; }
.press .section__head .btn { margin-top: 1.4rem; }
.press__updated { font-size: .85rem; color: var(--text-soft); margin-top: 1rem; }

/* Each section gets a rule above it. On a page this long the headings alone
   stopped reading as boundaries - everything ran together into one column. */
.press__section { padding-top: clamp(2rem, 4vw, 3rem); }
.press__section + .press__section { border-top: 1px solid rgba(255, 255, 255, .12); margin-top: clamp(2rem, 4vw, 3rem); }
/* Ink, not the section's teal. These are h2s with .press__h3 nested under them,
   and the h3 is plain --text-ink - so tinting the parent any lighter than its
   own child inverts the hierarchy. The uppercase setting and the rule above it
   already mark the boundary; it does not need colour to do it as well, and in
   sea-ink it read as sixteen unclickable links down a reference page. */
.press__h {
  font-size: 1.05rem; text-transform: uppercase; letter-spacing: .12em;
  color: var(--heading); margin: 0 0 .9rem;
}
.press__h3 {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.15rem; margin: 2rem 0 .7rem;
}
.press section > p { color: var(--text-soft); margin-bottom: .9rem; max-width: 68ch; }
.press section > p a { color: var(--sea-ink); font-weight: 600; }
.press__note { font-size: .92rem; font-style: italic; }
.press__note a { color: var(--sea-ink); font-weight: 600; }
.press__grid { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(2rem, 5vw, 4rem); }
.press__list { list-style: none; display: grid; gap: .6rem; max-width: 60ch; }
.press__list li { color: var(--text-soft); font-size: .95rem; padding-left: 1.1rem; position: relative; }
.press__list li::before { content: ""; position: absolute; left: 0; top: .62em; width: 7px; height: 7px; border-radius: 50%; background: var(--sea); }
.press__list strong { color: var(--text-ink); }

/* ---- download bundles ---- */
.press__downloads { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0; }
.press__dl {
  display: grid; align-content: start; gap: .25rem;
  padding: 1.3rem 1.4rem; border-radius: var(--radius);
  background: var(--paper-2); border: 2px solid rgba(255, 255, 255, .12);
  transition: transform .25s var(--ease-out), border-color .25s, box-shadow .25s;
}
.press__dl:hover, .press__dl:focus-visible { transform: translateY(-3px); border-color: var(--line); box-shadow: var(--shadow-pop); }
/* On paper this was a black tile between two white ones, which made it the
   loudest thing in the row. Kept as-is here it becomes the DARKEST tile
   among dark ones, so the featured bundle reads as recessed - the same
   declaration saying the opposite of what it used to. It takes the sand
   edge and a little warm light instead, which is how everything else on
   this site says "look at this one". */
.press__dl--lead {
  background: var(--plate);
  border-color: color-mix(in srgb, var(--sand) 55%, transparent);
  box-shadow: var(--rim), var(--lift), 0 0 30px -10px rgba(237, 161, 77, .30);
}
.press__dl-name { font-family: var(--font-display); font-weight: 800; font-size: 1.15rem; }
.press__dl-meta { font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-soft); }
.press__dl-note { font-size: .9rem; color: var(--text-soft); margin-top: .3rem; }
.press__dl--lead .press__dl-name { color: #fff; }
.press__dl--lead .press__dl-meta { color: var(--sand); }
.press__dl--lead .press__dl-note { color: var(--text-soft); }

/* ---- jump links ---- */
.press__toc { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
.press__toc a {
  font-size: .85rem; font-weight: 600;
  padding: .35rem .8rem; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .18); color: var(--text-soft);
  transition: background .2s, color .2s, border-color .2s;
  /* Padding alone left these pills 36 tall. They are the fastest way around a
     very long page, and on a phone they are the first thing a journalist
     touches, so they get the full 44. */
  display: inline-flex; align-items: center; min-height: 44px;
}
.press__toc a:hover, .press__toc a:focus-visible { background: var(--invert-bg); border-color: var(--invert-bg); color: var(--invert-fg); }

/* ---- fact sheets ---- */
.press__facts dl { display: grid; grid-template-columns: auto 1fr; gap: .55rem 1.2rem; }
.press__facts dt { font-family: var(--font-display); font-weight: 700; font-size: .92rem; white-space: nowrap; }
/* A 1fr track floors at the item's min-content width, and some of these values
   are a registered address. Without this the whole page scrolls sideways on a
   phone rather than the value wrapping. */
.press__facts dd { color: var(--text-soft); font-size: .95rem; min-width: 0; overflow-wrap: anywhere; }
/* These three (website, press contact, socials) are the one place on the site
   where a link standing on its own is under 44 tall. A 44px hit overlay was
   tried and reverted: it fits on a phone, where the rows stack ~59px apart, but
   on a wide screen the two-column grid packs them to ~33px and neighbouring
   overlays then overlap, so a press near a row boundary opens the wrong link.
   A target that lies about where it is beats one that is merely small.
   They meet 2.5.8 by the spacing exception instead - 33px between centres, so
   no two 24px circles touch - and accessibility.html says so plainly rather
   than claiming a flat 44 everywhere. Loosening the row gap to fit 44 would
   spread a ten-row fact sheet for the sake of three rows of it. */
.press__facts dd a { color: var(--sea-ink); font-weight: 600; }
/* Two columns stop paying for themselves once the label column is wider than
   what's left for the value. */
@media (max-width: 620px) {
  .press__facts dl { grid-template-columns: 1fr; gap: 0; }
  .press__facts dt { white-space: normal; margin-top: .7rem; }
  .press__facts dt:first-child { margin-top: 0; }
}

/* ---- boilerplate ---- */
.press__copyblock {
  background: var(--paper-2); border-radius: var(--radius);
  padding: 1.1rem 1.3rem; margin-bottom: 1rem; max-width: 74ch;
}
.press__copyblock-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: .5rem; }
.press__copyblock h3 { font-family: var(--font-display); font-weight: 700; font-size: 1rem; }
.press__copyblock h3 span { font-weight: 400; color: var(--text-soft); font-size: .85rem; }
.press__copyblock p { color: var(--text-ink); font-size: .95rem; margin: 0; }
.press__copy {
  flex-shrink: 0; cursor: pointer; position: relative;
  font-family: var(--font-body); font-size: .8rem; font-weight: 600;
  padding: .3rem .8rem; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .25); background: transparent; color: var(--text-ink);
  transition: background .2s, color .2s, border-color .2s;
}
/* The drawn pill is 28 tall and stays that way: it sits on the baseline beside
   the block's heading, and a 44px pill there dwarfs the h3 it is aligned to.
   The target grows instead. Nothing else is within 8px of it, so the overlay
   cannot steal a press of anything. */
.press__copy::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px;
}
.press__copy:hover { background: var(--invert-bg); border-color: var(--invert-bg); color: var(--invert-fg); }
.press__copy.is-done { background: var(--leaf-chip); border-color: var(--leaf-chip); color: #fff; }

/* ---- recognition ---- */
.press__creds { list-style: none; display: flex; flex-wrap: wrap; gap: .8rem; }
.press__creds li {
  display: grid; gap: .1rem;
  padding: .8rem 1.1rem; border-radius: var(--radius);
  background: var(--paper-2); border: 2px solid rgba(255, 255, 255, .12);
}
.press__creds strong { font-family: var(--font-display); font-weight: 700; font-size: 1rem; }
.press__creds span { font-size: .85rem; color: var(--text-soft); }

/* ---- media ---- */
.press__media { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 1.2rem; }
.press__media a {
  display: block; border-radius: var(--radius); overflow: hidden;
  border: 2px solid rgba(255, 255, 255, .12);
  transition: border-color .25s, box-shadow .25s;
}
.press__media a:hover { border-color: var(--line); box-shadow: var(--shadow-pop); }
.press__media img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; }

/* ---- logos ---- */
/* Every tile carries the background its mark is FOR, so picking the wrong file
   takes deliberate effort. --light and --dark set that plate; the two lockups
   that ship with their own background get the neutral tile. */
.press__logos { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 1rem; }
.press__logo {
  border-radius: var(--radius); overflow: hidden;
  border: 2px solid rgba(255, 255, 255, .12); background: var(--paper-2);
  display: grid; grid-template-rows: 1fr auto;
}
.press__logo img { justify-self: center; align-self: center; width: auto; max-width: 76%; max-height: 140px; padding: 1.3rem 0; }
.press__logo--dark { background: var(--brand-ink); }
.press__logo--light { background: #fff; }
/* The two lockups that ship with a plate baked in are previewed as the same
   artwork on the same colour, at the same size as their siblings. Painting the
   supplied square edge to edge instead made those two tiles half again as tall
   as the rest and left the others floating in dead space. */
.press__logo--blue { background: #05cdf2; }
.press__logo--black { background: #010101; }
.press__logo figcaption {
  display: grid; gap: .15rem;
  padding: .8rem 1rem; background: var(--plate);
  border-top: 1px solid rgba(255, 255, 255, .12);
}
.press__logo strong { font-family: var(--font-display); font-weight: 700; font-size: .95rem; }
.press__logo figcaption > span { font-size: .84rem; color: var(--text-soft); }
.press__logo figcaption a { color: var(--sea-ink); font-weight: 600; }
.press__logo-links { display: flex; gap: .6rem; margin-top: .3rem; font-size: .82rem; text-transform: uppercase; letter-spacing: .06em; }

.press__rules { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.press__rule { padding: 1.1rem 1.3rem; border-radius: var(--radius); background: var(--paper-2); border-left: 4px solid var(--leaf-chip); }
.press__rule--dont { border-left-color: var(--crab-chip); }
.press__rule h4 { font-family: var(--font-display); font-weight: 700; font-size: 1rem; margin-bottom: .5rem; }
.press__rule ul { list-style: none; display: grid; gap: .45rem; }
.press__rule li { font-size: .92rem; color: var(--text-soft); }

/* ---- colour & type ---- */
.press__swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; }
.press__swatches--wide { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.press__swatch { border-radius: var(--radius); overflow: hidden; border: 2px solid rgba(255, 255, 255, .12); }
.press__chip { display: block; height: 96px; }
/* White needs a hairline or it reads as a hole in the card. */
.press__chip--bordered { box-shadow: inset 0 0 0 1px rgba(20, 23, 28, .15); }
.press__swatch figcaption { display: grid; gap: .12rem; padding: .8rem 1rem; background: var(--paper-2); }
.press__swatch strong { font-family: var(--font-display); font-weight: 700; font-size: .95rem; }
.press__swatch figcaption > span { font-size: .82rem; color: var(--text-soft); }
.press__swatch code { font-size: .85rem; font-weight: 600; letter-spacing: .02em; }
.press__swatch-vals { font-size: .78rem; line-height: 1.5; }

.press__type { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
.press__type > div { padding: 1.1rem 1.3rem; border-radius: var(--radius); background: var(--paper-2); display: grid; gap: .15rem; }
.press__type strong { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; }
/* The press kit is a sea section and these specimen labels are not links, so
   they go grey rather than borrowing the link colour - see .step__num. */
.press__type > div > span { font-size: .82rem; text-transform: uppercase; letter-spacing: .08em; color: var(--text-soft); font-weight: 600; }
.press__type p { font-size: .9rem; color: var(--text-soft); margin-top: .3rem; }

/* ---- team ---- */
.press__people { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: .5rem 1.2rem; margin-bottom: 1rem; }
.press__people li { display: flex; flex-wrap: wrap; gap: .5rem; align-items: baseline; font-size: .95rem; }
.press__people a { color: var(--sea-ink); font-weight: 600; }
.press__people span { font-size: .85rem; color: var(--text-soft); }

.press__cta { display: flex; flex-wrap: wrap; align-items: center; gap: 1.2rem; }
.press__cta .press__h { flex-basis: 100%; margin-bottom: 0; }
.press__cta > p:not(.press__note) { font-family: var(--font-display); font-weight: 700; font-size: 1.2rem; margin: 0; }

@media (max-width: 900px) {
  .press__downloads { grid-template-columns: 1fr; }
}
@media (max-width: 800px) {
  .press__grid { grid-template-columns: 1fr; }
  .press__media { grid-template-columns: 1fr; }
  .press__rules { grid-template-columns: 1fr; }
}

/* ---------- legal pages ---------- */
.legal .section__head h1,
.legal .section__head h2 { font-size: clamp(2.1rem, 4.2vw, 3.3rem); margin-bottom: 1.1rem; }
.legal__body { max-width: 68ch; }
.legal__body p { color: var(--text-soft); margin-bottom: .9rem; }
/* The join page brought the first prose list onto one of these pages. The global
   reset strips ul of its marker and indent, and the link rule below was scoped to
   `p a`, so a link inside a list item came out as plain body text - the one thing
   a link must never look like. Both are fixed here rather than by avoiding lists. */
.legal__body ul { margin: 0 0 .9rem; padding-left: 1.4rem; list-style: disc; }
.legal__body li { color: var(--text-soft); margin-bottom: .5rem; }
.legal__body li::marker { color: var(--sand); }
.legal__body p a, .legal__body li a { color: var(--sea-ink); font-weight: 600; }
.legal__body p a:hover, .legal__body li a:hover { text-decoration: underline; }
.legal__body strong { color: var(--text-ink); }

/* ---------- join page ---------- */
/* The face grid breaks out of .legal__body's 68ch measure deliberately, so the
   tiles get the shell's full width and read at the same size as the team grid on
   the home page rather than as thumbnails in a paragraph. */
.join-faces { margin: clamp(2.6rem, 5vw, 4rem) 0; }
.join-faces__lede { color: var(--text-soft); max-width: 68ch; margin-bottom: 1.8rem; }
.join-figure { margin: 1.6rem 0; }
.join-figure img {
  display: block; width: 100%; height: auto;
  border-radius: var(--radius);
}
.join-figure figcaption {
  font-size: .88rem; color: var(--text-soft); margin-top: .7rem;
}
/* A row each rather than a grid of tiles. The point of this section is what these
   people did, and a tile can only hold a name and a role, so the photo sits beside
   the sentence instead of on top of a caption. */
.join-people { list-style: none; margin: 0; padding: 0; max-width: 68ch; }
.join-person {
  display: flex; align-items: flex-start; gap: 1.1rem;
  margin-bottom: 1.6rem;
}
/* .member__photo brings the square, the crop and the .is-zoomed transform with it,
   so these faces are framed exactly as they are everywhere else on the site and
   this only has to set the size. */
.join-person__photo { width: 96px; flex: none; margin-bottom: 0; }
.join-person__copy { min-width: 0; }
.join-person__copy h3 {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.05rem; margin: 0 0 .1rem;
}
.join-person__copy h3 a { color: var(--sea-ink); }
.join-person__copy h3 a:hover { text-decoration: underline; }
.join-person__role {
  font-family: var(--font-display); font-weight: 700;
  font-size: .72rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--text-soft); margin: 0 0 .5rem;
}
.join-person__copy p:last-child { color: var(--text-soft); margin: 0; }
/* Under the photo rather than beside it, once beside it leaves the sentence
   setting three or four words to a line. */
@media (max-width: 560px) {
  .join-person { flex-direction: column; gap: .8rem; }
  .join-person__photo { width: 120px; }
}

/* footer (mini) for profile pages */
.footer--mini { padding-top: 3rem; }
.footer--mini .footer__meta { border-top: 0; padding-top: 0; }

/* ---------- reveal on scroll ---------- */
.reveal {
  opacity: 0; transform: translateY(22px);
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out);
}
.reveal.is-visible { opacity: 1; transform: none; }

/* ---------- responsive ---------- */
@media (max-width: 980px) {
  .hero__inner { grid-template-columns: 1fr; }
  .hero__art { max-width: 560px; }
  .game__features { grid-template-columns: repeat(2, 1fr); }
  .services__grid { grid-template-columns: 1fr; border-top: 0; }
  .card { border-right: 0; border-top: 1px solid rgba(255,255,255,.15); padding-left: 0 !important; padding-right: 0; }
  .studio__inner { grid-template-columns: 1fr; }
  .studio__photo { max-width: 480px; }
  .creds__inner { grid-template-columns: 1fr; }
  .creds__frame { max-width: 520px; }
  .team__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .team__grid--founders { width: 100%; }
  /* stays one column ahead of the studio grid, or the pool stops reading as the
     smaller thing the moment the breakpoint lands */
  .team__grid--pool { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .news__grid { grid-template-columns: 1fr; max-width: 560px; margin-inline: auto; margin-bottom: 2.8rem; }
  .profile__inner { grid-template-columns: 1fr; }
  .profile__photo { position: static; max-width: 380px; }
}

@media (max-width: 920px) {
  /* the dropdown hangs off the shell, not the viewport, so its edges keep
     agreeing with the bar above it */
  .nav__inner { position: relative; }
  .nav--hero:not(.is-scrolled):not(.is-open) .nav__inner { padding: .9rem 0; }
  .nav--hero:not(.is-scrolled):not(.is-open) .nav__wordmark { height: 28px; }
  .nav__links {
    position: absolute; top: calc(100% + 8px); right: 0; left: 0;
    flex-direction: column; align-items: stretch; gap: .1rem;
    background: var(--ink); border: 1px solid rgba(255,255,255,.1);
    border-radius: var(--radius); padding: .7rem;
    opacity: 0; visibility: hidden; transform: translateY(-8px);
    transition: opacity .25s, transform .25s var(--ease-out), visibility .25s;
  }
  .nav.is-open .nav__links { opacity: 1; visibility: visible; transform: none; }
  .nav__links a { padding: .7rem .9rem; }
  /* inset to match the padding, so the rule sits under the word rather than
     running the full width of the panel */
  .nav__links a:not(.nav__cta)::after { left: .9rem; right: .9rem; bottom: 8px; }
  .nav__burger { display: flex; }
}

@media (max-width: 760px) {
  .team__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .team__grid--founders { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .9rem; }
  .team__grid--pool { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .7rem; }
  .game__features { grid-template-columns: 1fr; gap: 1.4rem; }
  .game__cta { flex-direction: column; align-items: flex-start; }
  .footer__meta { justify-content: center; text-align: center; }
}

/* ---------- co-dev page ---------- */
.hero--codev .hero__inner { display: block; max-width: 820px; }
.hero--codev .hero__sub { max-width: 58ch; }

.codev { background: var(--paper); }
.svc {
  display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr);
  gap: clamp(2rem, 5vw, 4rem);
  border-top: 3px solid var(--line);
  padding: clamp(2.2rem, 4vw, 3.2rem) 0;
}
.svc:nth-of-type(1) { border-top-color: var(--sea); }
.svc:nth-of-type(2) { border-top-color: var(--sand); }
.svc:nth-of-type(3) { border-top-color: var(--leaf); }
/* a fourth service appeared when 3D art split into stylised and hard surface;
   without this it was the only one wearing the default ink rule */
.svc:nth-of-type(4) { border-top-color: var(--crab); }
.svc h3 { font-size: clamp(1.5rem, 2.6vw, 2rem); margin-bottom: .9rem; }
.svc__body > p { color: var(--text-soft); margin-bottom: 1.3rem; max-width: 54ch; }
.svc__list { display: grid; gap: .55rem; }
.svc__list li {
  position: relative; padding-left: 1.6rem;
  color: var(--text-ink); font-size: .98rem;
}
.svc__list li::before {
  content: ""; position: absolute; left: 0; top: .45em;
  width: .8em; height: .8em; border-radius: 3px;
  background: var(--tick-sea);
  clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 16%, 84% 0, 39% 70%);
}
/* Each tick matches the rule above its own service. The first was on the bright
   --sea while the other two were on the deep tier, and the fourth service - the
   one that appeared when 3D art split in two - had no rule at all, so it wore a
   crab stripe over four teal ticks. Deep tier throughout: these are 0.8em marks
   and the bright tier is a shade too thin at that size. */
.svc:nth-of-type(2) .svc__list li::before { background: var(--tick-sand); }
.svc:nth-of-type(3) .svc__list li::before { background: var(--tick-leaf); }
.svc:nth-of-type(4) .svc__list li::before { background: var(--tick-crab); }

/* gallery carousel: slots hold placeholders now, real <img> later */
.carousel { grid-column: 1 / -1; position: relative; margin-top: 1.4rem; min-width: 0; max-width: 100%; }
.carousel__track {
  display: flex; gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  /* The top padding is what stops a hovered tile losing its border. overflow-x:
     auto does not leave the other axis alone - a visible axis paired with a
     scrolling one computes to auto - so this strip clips vertically as well as
     horizontally, and a scroll container clips at its PADDING box. With no
     padding here the tiles start flush with that edge, and the 3px lift on
     hover pushed the tile's 2px top border straight through it, which read as
     the artwork being sliced. 6px clears the lift with room to spare. */
  padding-top: 6px;
  padding-bottom: .5rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, .25) transparent;
  cursor: grab;
}
.carousel__track.is-dragging { cursor: grabbing; scroll-snap-type: none; user-select: none; }
.carousel__slot {
  flex: 0 0 auto;
  height: clamp(150px, 19vw, 205px);
  aspect-ratio: 4 / 3;   /* placeholders; art tiles carry their image's real ratio inline */
  scroll-snap-align: start;
  border: 2px dashed rgba(255,255,255,.25);
  border-radius: var(--radius);
  background: var(--ink-2);
  display: grid; place-items: center; align-content: center; gap: .25rem;
}
.carousel__slot a { display: block; width: 100%; height: 100%; }
.carousel__slot img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  background: var(--ink);
  border-radius: calc(var(--radius) - 2px);
  -webkit-user-drag: none;
}
.carousel__slot video {
  width: 100%; height: 100%; object-fit: cover; display: block;
  background: var(--ink);
  border-radius: calc(var(--radius) - 2px);
}
.carousel__slot a:not(.carousel__credit), .carousel__slot video { cursor: pointer; }
.carousel__slot:has(img), .carousel__slot:has(video) {
  display: block;   /* the link and media fill the tile even before the image loads */
  border-style: solid; border-color: var(--line);
  background: var(--ink); padding: 0;
  position: relative;
  transition: transform .25s var(--ease-out), box-shadow .25s;
}
.carousel__slot:has(img):hover, .carousel__slot:has(video):hover { transform: translateY(-3px); box-shadow: var(--shadow-pop); }

/* artist credit chip on gallery art */
.carousel__slot .carousel__credit {
  position: absolute; left: .55rem; bottom: .55rem; z-index: 1;
  width: auto; height: auto;
  display: inline-flex; align-items: center; gap: .45rem;
  /* 2.5.5: the credit chip is a link, so it needs 44px of height too */
  min-height: 44px;
  padding: .45rem .8rem .45rem .3rem;
  background: rgba(10, 12, 15, .88);
  border: 1px solid rgba(255,255,255,.3); border-radius: 999px;
  font-size: .72rem; font-weight: 600; color: #fff;
  text-decoration: none; white-space: nowrap;
  transition: background .2s, border-color .2s;
}
.carousel__slot .carousel__credit img {
  width: 22px; height: 22px; border-radius: 50%;
  object-fit: cover; background: transparent;
}
.carousel__slot .carousel__credit:hover { background: rgba(10,12,15,.92); border-color: #fff; }

/* 2.2.2 Pause, Stop, Hide. One control per clip, sitting on the clip it governs.
   This used to be a single button under the section header, far enough from the
   galleries that it read as unrelated furniture and nobody connected it to the
   thing it stopped. Injected by js/main.js, so it only exists where something is
   actually playing. Mirrors the credit chip opposite it, 44px target included. */
.carousel__slot .clip-toggle {
  position: absolute; right: .55rem; bottom: .55rem; z-index: 1;
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(10, 12, 15, .88);
  border: 1px solid rgba(255,255,255,.3);
  color: #fff; cursor: pointer;
  transition: background .2s, border-color .2s;
}
.carousel__slot .clip-toggle:hover { background: rgba(10,12,15,.92); border-color: #fff; }
.carousel__slot .clip-toggle svg { width: 15px; height: 15px; }
.clip-toggle .icon-play { display: none; }
.clip-toggle[aria-pressed="true"] .icon-play { display: block; }
.clip-toggle[aria-pressed="true"] .icon-pause { display: none; }
.carousel__btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%;
  border: 2px solid var(--line-strong); background: var(--plate); color: var(--heading);
  font-size: 1.5rem; line-height: 1; display: grid; place-items: center;
  cursor: pointer; z-index: 2;
  box-shadow: var(--lift);
  transition: transform .2s var(--ease-out), border-color .2s, color .2s, opacity .2s;
}
.carousel__btn:hover { transform: translateY(calc(-50% - 2px)); border-color: var(--sand); color: var(--sand); }
.carousel__btn:disabled {
  opacity: 0; pointer-events: none; cursor: default;
  transform: translateY(-50%);
}
.carousel__btn--prev { left: -12px; }
.carousel__btn--next { right: -12px; }

.svc__crew-label {
  font-family: var(--font-display); font-weight: 700; font-size: .85rem;
  text-transform: uppercase; letter-spacing: .12em; color: var(--text-soft);
  margin-bottom: 1rem;
}
.svc__crew { display: flex; flex-wrap: wrap; gap: .7rem; align-content: start; }
.person {
  display: inline-flex; align-items: center; gap: .6rem;
  padding: .3rem .9rem .3rem .3rem;
  background: var(--plate); border: 1px solid var(--line); border-radius: 999px;
  box-shadow: var(--rim), var(--lift);
  transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out);
}
.person:hover { transform: translateY(-3px); border-color: var(--line-hover); box-shadow: var(--rim), var(--lift-hover); }
.person img {
  width: 34px; height: 34px; border-radius: 50%; object-fit: cover; object-position: center 20%;
  flex-shrink: 0;
}
.person strong { font-family: var(--font-display); font-weight: 700; font-size: .92rem; line-height: 1.1; display: block; }
.person span { font-size: .74rem; color: var(--text-soft); display: block; line-height: 1.2; }

/* proof band (dark) */
.proof {
  position: relative; /* anchors the wave dividers on both edges */
  background:
    radial-gradient(880px 500px at 0% 0%, rgba(36, 205, 242, .26), transparent 64%),
    var(--ink);
  color: var(--text-paper);
}
/* Both edges wear the wave, and each divider is up to 90px of svg plus 18px of
   drifting crests before any actual breathing room, so the band needs footer-
   grade padding on both sides. Doubled selector on purpose: a lone .proof loses
   the padding-top fight to .section + .section. Same reasoning as .community. */
.section.proof { padding: clamp(6rem, 10vw, 8.5rem) 0; }
.proof__inner {
  display: grid; grid-template-columns: .95fr 1.05fr;
  gap: clamp(2.5rem, 6vw, 5rem); align-items: center;
}
.proof__frame { position: relative; }
.proof__frame img {
  border-radius: var(--radius-lg);
  border: 3px solid rgba(255,255,255,.14);
  box-shadow: 0 30px 60px -20px rgba(0,0,0,.55);
  transform: rotate(-2deg);
  transition: transform .35s var(--ease-out);
  aspect-ratio: 16/10; object-fit: cover; width: 100%;
}
.proof__frame:hover img { transform: rotate(-.5deg) translateY(-5px); }
.proof__frame .sticker { bottom: -10px; left: 20px; }
.proof__copy h2 { color: #fff; font-size: clamp(1.9rem, 3.6vw, 2.9rem); margin-bottom: 1.1rem; }
.proof__copy p { color: var(--text-paper-soft); margin-bottom: 1.1rem; max-width: 56ch; }
.proof__copy .btn { margin-top: .8rem; }

/* how it works */
.how { background: var(--paper-2); }
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem 2.2rem; }
.step { border-top: 3px solid var(--line); padding-top: 1.1rem; }
.step:nth-child(1) { border-top-color: var(--sea); }
.step:nth-child(2) { border-top-color: var(--sand); }
.step:nth-child(3) { border-top-color: var(--leaf); }
.step:nth-child(4) { border-top-color: var(--crab); }
/* Grey rather than the section's teal: "How it works" is a sea section, and on
   a sea section sea-ink is the link colour and nothing else. The step already
   wears its accent on the rule above, so the numeral does not need to repeat
   it - and repeating it here would put link-coloured type beside real links. */
.step__num {
  font-family: var(--font-display); font-weight: 800; font-size: .85rem;
  letter-spacing: .1em; color: var(--text-soft); display: block; margin-bottom: .6rem;
}
.step strong {
  display: block; font-family: var(--font-display); font-weight: 700;
  font-size: 1.15rem; margin-bottom: .5rem; line-height: 1.25;
}
.step p { font-size: .95rem; color: var(--text-soft); }

.codev__note {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding: 1.2rem 1.5rem;
  background: var(--paper-2);
  border-left: 3px solid var(--sand);
  border-radius: var(--radius-sm);
  color: var(--text-soft);
  max-width: 62ch;
}
.codev__note a { font-weight: 600; color: var(--sea-ink); }
.codev__note a:hover { text-decoration: underline; }

.tools { margin-top: clamp(2.8rem, 5vw, 4rem); }
.tools__label {
  font-family: var(--font-display); font-weight: 700; font-size: .85rem;
  text-transform: uppercase; letter-spacing: .12em; color: var(--text-soft);
  margin-bottom: 1rem;
}
.tools__row { display: flex; flex-wrap: wrap; gap: .7rem; }
.tool {
  display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--font-display); font-weight: 700; font-size: .92rem;
  padding: .5rem 1.05rem; border-radius: 999px;
  background: var(--plate); border: 1px solid var(--line); box-shadow: var(--rim), var(--lift);
}
/* A chip can carry the tool's own mark: put <img class="tool__logo" alt=""> in
   front of the name. The name stays either way - it's what the chip means, and
   a mark alone would make the row unreadable to anyone who doesn't recognise
   seven vendor logos on sight. js/main.js strips any image that fails to load,
   so a chip whose logo we haven't been given yet falls back to the plain name
   rather than showing a broken image. */
.tool__logo { width: 20px; height: 20px; object-fit: contain; flex: none; }

/* lightbox */
.lightbox {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center; gap: 1.1rem;
  padding: 2rem;
  background: rgba(10, 12, 15, .82);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  opacity: 0; visibility: hidden;
  transition: opacity .25s, visibility .25s;
}
/* Opening drops visibility from the transition so it flips to visible immediately
   and the close button can take focus in the same tick. Closing keeps the base
   rule's visibility transition, so the overlay stays put while it fades out. */
.lightbox.is-open { opacity: 1; visibility: visible; transition: opacity .25s; }
/* fixed stage: every piece renders in the same box, so the arrows never move */
.lightbox__figure { width: min(1100px, 86vw); text-align: center; }
.lightbox__img, .lightbox__video {
  width: 100%; height: 74vh; object-fit: contain;
  background: var(--ink);
  border-radius: var(--radius-lg);
  box-shadow: 0 30px 80px -20px rgba(0,0,0,.8);
}
/* the base reset's `img, video { display: block }` would defeat the hidden attribute */
.lightbox__img[hidden], .lightbox__video[hidden] { display: none; }
.lightbox__caption { margin-top: .8rem; color: var(--text-paper-soft); font-size: .92rem; max-width: 70ch; margin-inline: auto; }
/* Optional piece title and write-up, from data-title / data-desc on the slot.
   Without them the caption falls back to the image's alt text, as before. */
.lightbox__cap-title {
  display: block; margin-bottom: .3rem;
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; color: #fff;
}
/* Every write-up is kept to roughly the same length, but "roughly" still lands
   on two lines or three depending on the viewport - and the figure is centred,
   so that one line's difference walks the image up and down as you browse.
   Reserving three lines holds the stage still whichever caption is showing. */
.lightbox__cap-text { display: block; line-height: 1.55; min-height: calc(3 * 1.55em); }
.lightbox__credit {
  display: inline-block; margin-top: .35rem;
  color: #fff; font-size: .85rem; font-weight: 600;
  text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.4);
}
.lightbox__credit:hover { border-color: #fff; }
.lightbox__btn, .lightbox__close {
  width: 46px; height: 46px; border-radius: 50%; flex-shrink: 0;
  border: 2px solid rgba(255,255,255,.4);
  background: rgba(255,255,255,.08); color: #fff;
  font-size: 1.7rem; line-height: 1;
  display: grid; place-items: center; cursor: pointer;
  transition: background .2s, border-color .2s;
}
.lightbox__btn:hover, .lightbox__close:hover { background: rgba(255,255,255,.18); border-color: #fff; }
.lightbox__close { position: absolute; top: 18px; right: 18px; font-size: 1.5rem; }
@media (max-width: 700px) {
  .lightbox { padding: 3.5rem .6rem 1rem; gap: .5rem; }
  /* The glyph shrinks on a narrow screen; the target does not. 40px was under
     the 44 the accessibility statement commits to, and these are the only way
     through the gallery once it is open. */
  .lightbox__btn { width: 44px; height: 44px; font-size: 1.4rem; }
}

/* faq */
.faq { background: var(--paper); }
.faq__list { max-width: 780px; }
.faq__list details { border-bottom: 1px solid rgba(255, 255, 255, .15); }
.faq__list summary {
  font-family: var(--font-display); font-weight: 700; font-size: 1.12rem;
  padding: 1.25rem 2.2rem 1.25rem 0;
  cursor: pointer; list-style: none; position: relative;
}
.faq__list summary::-webkit-details-marker { display: none; }
.faq__list summary::after {
  content: "+"; position: absolute; right: .3rem; top: 50%;
  transform: translateY(-50%);
  font-size: 1.5rem; font-weight: 400; color: var(--sea-ink);
  transition: transform .2s var(--ease-out);
}
.faq__list details[open] summary::after { transform: translateY(-50%) rotate(45deg); }
.faq__list details p { color: var(--text-soft); padding: 0 0 1.4rem; max-width: 62ch; }
/* Slide the answer open and closed. Native <details> has never animated, and it
   mattered more once these became an exclusive accordion: opening one made the
   last one vanish on the spot. Behind @supports because interpolating to auto
   height is recent - browsers without it snap, exactly as they always did. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq__list details::details-content {
    block-size: 0; overflow: hidden;
    transition: block-size .3s var(--ease-out), content-visibility .3s allow-discrete;
  }
  .faq__list details[open]::details-content { block-size: auto; }
  @media (prefers-reduced-motion: reduce) {
    .faq__list details::details-content { transition: none; }
  }
}

@media (max-width: 980px) {
  .svc { grid-template-columns: 1fr; gap: 1.6rem; }
  .proof__inner { grid-template-columns: 1fr; }
  .proof__frame { max-width: 520px; }
  .steps { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .steps { grid-template-columns: 1fr; gap: 1.4rem; }
}

/* ---------- the wash that used to live here ----------
   There was a radial tint on the section directly under each band - five
   hundredths of the sea, 460px tall, centred just above the section's top
   edge - to stop a flat section reading as a switched-off light one. It is
   deleted, and it should not come back in that form, because the idea has a
   flaw that no amount of tuning fixes.

   THE WAVE CANNOT CARRY IT. The divider is an svg path in the markup filled
   with a flat var(--paper) or var(--paper-2), so the wave has no wash on it
   by construction. The wash was at close to full strength at y=0 - its
   centre was only 8% above the top edge - so the boundary was flat colour
   above and tinted colour below, meeting in a dead straight line across the
   full width of the page, immediately under a shape whose entire job is to
   not be a straight line. Measured, that step was 1.09, which is small and
   completely visible, because a hairline running the whole width of a page
   is the easiest thing in the world for an eye to find.

   Any value above zero at the top edge draws that line. A gradient that
   ramps up from nothing instead would not, but then it is a glow in the
   middle of a section rather than light coming off a band, which is a
   different and worse idea.

   It also had a side effect nobody had noticed. It set background-image on
   `.community + .section`, which on games.html is .pub - and .pub declares
   its paper-to-plate fade as a `background` shorthand EARLIER in the file,
   at the same specificity. So this rule silently replaced it, and the
   publishers section had been rendering with no gradient and no background
   colour at all, just the wash over the body. Deleting this restores it.

   The saturation in the surface tokens is what carries this job now: the
   sections are a colour rather than a dimmed neutral, so they do not need
   help to look like something. */

/* ============================================================
   MOTION POLISH
   Three things, and deliberately only three. An earlier pass put movement on
   more or less everything that could take it - nav, headings, reveals, chips,
   stickers, counters, the viewer, the footer - and the verdict was that most of
   it read as jarring rather than alive. What is left is water that moves
   because it is water, and two small responses to a pointer.

   The hero is deliberately still. A drifting logo and a field of rising bubbles
   were both tried and both cut - see the branch history for them - so the only
   thing moving above the fold is the water at the foot of it. Nothing on the
   page now animates unless you point at it or scroll past the dividers.

   Two rules held throughout:

   NOTHING HERE IS LOAD-BEARING. Every from-state that hides content lives in a
   @keyframes block, never in a base rule, so a browser that never runs the
   animation shows the finished thing rather than an empty box.

   REDUCED MOTION IS AN OPT-OUT, NOT A SPEED-UP. The blanket at the foot of this
   file clamps durations to .01ms, which is right for a hover transition and
   wrong for an infinite loop. Everything looping here is gated on
   (prefers-reduced-motion: no-preference), so it is never applied at all.
   ============================================================ */

/* ---------- 1. the water moves ---------- */
/* One translucent cyan crest drifting behind the solid divider, so the edge
   between the ink and the paper reads as water rather than as a shape. It sits
   BEHIND the real wave, which is why only the crest peeking above it is ever
   visible - the solid path covers everything underneath.

   There was a second crest here in sand (#ffb055 at .16). Every surface it ever
   crossed is ink or near-ink, and warm at 16% over near-black does not read as
   light on water - it composites to a muddy brown lens sitting above the wave,
   which is exactly the thing it was meant to avoid looking like. Cyan survives
   the same treatment because it is lighter than the ink it sits on. One layer
   still drifts, so the motion the effect exists for is intact.

   The layer is twice the divider's width carrying two tiles, so translating it
   exactly one tile width loops with no seam, on the compositor, without
   touching background-position.

   That double width is why .wave grows an overflow:hidden. Without it the layer
   sticks a viewport's width out of the right-hand side of the page: body
   already carries overflow-x:hidden so nothing is visibly wrong, but the
   document's scrollWidth doubles, which is enough to give a phone a horizontal
   pan gesture that goes nowhere.

   The crest has to clear the solid path to be seen at all, and the room for
   that is padding on .wave rather than height on the layers - the box is
   anchored to one edge and auto-sized, so padding grows it away from that edge
   and the solid divider does not move a pixel. */
.wave { overflow: hidden; padding-top: 18px; }
.wave--top { padding-top: 0; padding-bottom: 18px; }
.wave svg { position: relative; z-index: 2; }
.wave::before {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 200%; height: 100%;
  background-repeat: repeat-x; background-size: 50% 100%;
  pointer-events: none;
  z-index: 0; opacity: .32;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0,52 C240,92 480,12 720,40 C960,68 1200,24 1440,52 L1440,90 L0,90 Z' fill='%2324cdf2'/%3E%3C/svg%3E");
}
/* The footer divider is the same shape upside down, so its drift layer is too.
   The flip is baked into the keyframes as well as sitting on the element,
   because a transform in a base rule and a transform in an animation do not
   compose - the animation would simply replace it, and the layer would run the
   right way up under an upside-down wave. */
.wave--top::before { top: 0; bottom: auto; transform: scaleY(-1); }
@media (prefers-reduced-motion: no-preference) {
  .wave::before { animation: wave-drift 26s linear infinite; }
  .wave--top::before { animation: wave-drift-flip 26s linear infinite; }
}
@keyframes wave-drift { to { transform: translateX(-50%); } }
@keyframes wave-drift-flip {
  from { transform: scaleY(-1) translateX(0); }
  to { transform: scaleY(-1) translateX(-50%); }
}

/* ---------- 2. buttons ---------- */
/* The arrow nudges. That is all.

   There used to be a diagonal band of light that swept across the button on
   hover, twice reworked to stop it reading as a highlight. It is gone, and the
   reason is not that the tuning failed: a light sweep across a button is on
   every third site on the web, so however carefully it is judged it can only
   ever say "this is a button on a website". The colour change underneath it -
   --sand to --sand-deep - was always the part doing the work, it reads faster
   than a .75s sweep, and it is the whole hover state now. */
.btn { position: relative; }
.btn svg { transition: transform .3s var(--ease-out); }
.btn:hover svg { transform: translateX(2px); }

/* ---------- 3. package cards lean ---------- */
/* Towards the pointer, by a degree and a half. The first pass used four and a
   half with a .12s follow, which tracked the cursor closely enough to read as
   a gimmick; the point is that the card notices you, not that it obeys you. The
   angles come from js/main.js as two custom properties, so the lean and the
   hover lift compose here in one place rather than the script fighting the
   stylesheet over style.transform.

   Fine pointers only - there is nothing to lean towards on a touchscreen. */
@media (hover: hover) and (pointer: fine) {
  .pkg {
    transform: perspective(900px) rotateX(var(--ry, 0deg)) rotateY(var(--rx, 0deg));
    transition: transform .4s var(--ease-out), box-shadow .25s var(--ease-out);
  }
  .pkg:hover {
    transform: perspective(900px) rotateX(var(--ry, 0deg)) rotateY(var(--rx, 0deg)) translateY(-4px);
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}
