/* ==========================================================================
   Kafha — site system
   A dark surface system. Everything is built from one near-black base and a
   ladder of elevations above it, with a single blue brand accent.
   Fluid type scale, sticky scroll accordions, marquee, drag carousel,
   full-screen mobile nav.
   ========================================================================== */

:root {
  /* --- surfaces -----------------------------------------------------
     A ladder, not a pair. Each step is a real elevation above the one
     below it, so a card on a band still separates from it without a
     border doing the work. Every step carries a slight blue cast picked
     up from the brand accent, so the greys never read as a foreign neutral
     dropped onto a tinted page.

     The brand sheet names pure #000000 as the ground; this sits a few
     points above it deliberately. Pure black bands on OLED and 8-bit
     panels, and it leaves nothing darker to put underneath, so the footer
     and every shadow would have no room to sit below the page. */
  --ground:     #09080C;   /* the page — off-black, never #000 */
  --band:       #0E0D13;   /* section bands, one step up */
  --raise:      #14131B;   /* cards and panels lifted off a band */
  --raise-2:    #1A1922;   /* hover state for a raised surface */

  /* --- brand accents ------------------------------------------------
     Blue is the primary and carries the identity; lime is the signal
     colour and is spent sparingly, on the things that genuinely need to
     be noticed. Two loud accents used evenly cancel each other out.

     --accent is the brand blue exactly as specified, and is a FILL: at
     3.5:1 on the ground it is not legible as text. --accent-ink is the
     same hue lifted until it clears AA (5.8:1) and is what type, links and
     icons use. Never swap them. */
  --accent:      #37CAEC;  /* brand light blue — the signature colour */
  --accent-ink:  #37CAEC;  /* 10.3:1 on --ground — reads as type too */
  --accent-deep: #3DD9D6;  /* hover lifts toward the teal end of the ramp */
  --accent-dim:  #125488;  /* the deep stop — fills that sit under text */
  /* The label that sits ON an --accent fill. The brand blue is light, so
     that label is the near-black (10.2:1), not white (1.9:1). It is a token
     because the light theme's accent is dark and flips this to white. */
  --accent-label: #09080C;
  /* The hero headline is TEXT, so its gradient runs between two ink-safe
     stops. The fill tokens above are too dark to read on this ground. */
  --accent-grad-a: #37CAEC;  /* 10.3:1 on --ground */
  --accent-grad-b: #3DD9D6;  /* 11.5:1 on --ground */

  /* Lime, reserved. 16.8:1 on the ground, so it shouts by design — it is
     for the status dot, the active marker and nothing routine. */
  --signal:      #C6FF33;
  --signal-ink:  #0A0A0A;  /* 16.7:1 — the only label that reads on lime */

  --ink:        #F4F6F7;   /* 17.4:1 on --ground — not pure white */

  /* Working aliases, all drawn from the ladder above. */
  --ground-2:   #0E0D13;
  --band-2:     #14131B;
  --slate:      #1A1922;   /* image placeholder */
  --paper:      #14131B;

  /* Tints toward the ground for hierarchy — no new hues, but carrying the
     same faint blue cast as the surfaces so the greys belong to the page. */
  --ink-soft:   #B7B5C0;   /* 9.7:1 on --ground */
  --ink-faint:  #82808F;   /*  5.1:1 on --ground */

  --rule:       #24222D;   /* hairlines — reads as an edge, not a line */

  /* Form error text. Lifted off the red so it still clears 4.5:1 on the
     dark ground; the light theme drops it to a true red for the same reason. */
  --err:        #F0907F;

  /* Shadows carry the ground's hue rather than neutral black, so a lifted
     card sits in the page instead of on top of a grey rectangle. */
  --shadow-sm:  0 1px 2px rgb(4 2 8 / .4);
  --shadow-md:  0 8px 24px -12px rgb(4 2 8 / .7);
  --shadow-lg:  0 24px 64px -24px rgb(4 2 8 / .85);

  /* Full-bleed layout: sections run edge to edge and content is held off the
     viewport edge by the gutter alone, rather than centred in a fixed column.
     44px at desktop, easing down to 20px on a phone. */
  --shell:  100%;
  --gut:    clamp(20px, 2.6vw, 44px);

  --ease:      cubic-bezier(.22,.61,.36,1);
  --ease-out:  cubic-bezier(.16,1,.3,1);

  /* One face across the whole site — Inter at every size. No serif anywhere;
     display weight and tracking do the work a second family would. */
  --display: "Inter",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --body:    "Inter",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --mono:    ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  /* No positive tracking anywhere. Letters are never spaced apart —
     size, weight and colour carry emphasis instead. */
}

/* --- light theme ----------------------------------------------------
   The same ladder, inverted: --ground is the paper and each step darkens
   rather than lightens, so every `.dark` / `.raise` rule above keeps its
   meaning without being rewritten. Only the token values move.

   The two accent tokens SPLIT here. --accent is the brand blue sky in both
   themes, so a filled button is the same colour on paper as on the dark
   page. But the blue sky is 1.9:1 on this ground and cannot carry text, so
   --accent-ink drops to the deep blue for links, icons and small type. The
   fill is the brand; the ink is whatever reads against the paper.

   Set by [data-theme="light"] on <html>, which the inline script in the
   head applies before first paint. */
:root[data-theme="light"] {
  --ground:     #FBFBFA;   /* the page — warm off-white, never #FFF */
  --band:       #F4F3F6;
  --raise:      #FFFFFF;   /* a card lifts toward white, away from the band */
  --raise-2:    #EFEDF3;

  /* The blue sky is the brand colour in BOTH themes, so the fill is the same
     #37CAEC the dark page uses and the button reads identically either way.
     It carries the near-black label here exactly as it does on dark
     (10.3:1); white on it is 1.9:1 and was never an option. */
  --accent:     #37CAEC;
  --accent-dim: #125488;
  --ink:        #16151A;   /* 16.2:1 on --ground — not pure black */

  --ground-2:   #F4F3F6;
  --band-2:     #FFFFFF;
  --slate:      #E8E6EE;
  --paper:      #FFFFFF;
  --accent-deep: #3DD9D6;  /* hover lifts toward teal, as it does on dark */
  --accent-label: #09080C; /* 10.3:1 on the blue sky */
  --accent-grad-a: #125488;  /*  7.7:1 on --ground */
  --accent-grad-b: #21356A;  /* 11.4:1 on --ground */

  /* Lime is a fill at both themes, but on paper it is 1.1:1 and cannot even
     host white. Where it appears, it carries the near-black label. */
  --signal:      #C6FF33;
  --signal-ink:  #0A0A0A;

  --accent-ink: #125488;   /* 7.7:1 on --ground, 6.8:1 on --raise-2 */

  --ink-soft:   #4C4A55;   /*  8.6:1 on --ground */
  /* Set against the deepest surface it ever lands on, not the page. At the
     value that cleared 4.5:1 on --ground it fell to 4.27:1 on --raise-2 and
     the footer, where most faint text actually sits. 5.3 / 5.0 / 4.8:1
     across ground, band and raise-2. */
  --ink-faint:  #65626F;

  --rule:       #E3E1E8;

  --err:        #B3261E;   /* 6.4:1 on --ground */

  /* Neutral-cool and much shallower: the deep spreads that give a dark page
     its depth read as smudges on paper. */
  --shadow-sm:  0 1px 2px rgb(22 21 26 / .06);
  --shadow-md:  0 8px 24px -12px rgb(22 21 26 / .16);
  --shadow-lg:  0 24px 64px -24px rgb(22 21 26 / .22);
}

/* The whole type scale hangs off a fluid root, so every step moves
   together between phone and desktop instead of stepping at breakpoints. */
html {
  font-size: clamp(15px, 0.55vw + 13.2px, 17px);
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

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

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1.08rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}

h1,h2,h3,h4 { margin: 0; font-weight: 400; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
/* height:auto is load-bearing, not tidying.
   Every <img> carries width/height attributes so the browser can reserve the
   right box before the file arrives and the page does not shift as it loads.
   Those attributes are also presentational hints, though: max-width:100%
   shrinks the width to fit the column but leaves the HEIGHT pinned at the
   attribute value, so a 1600x915 photo in a 1058px column rendered 1058x915
   — squashed to 1.16:1 from its natural 1.75:1. Letting the height follow the
   width keeps the aspect ratio intact while the attributes still do their
   layout-stability job. */
img, video { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }

.shell { width: 100%; max-width: var(--shell); margin-inline: auto; padding-inline: var(--gut); }

/* Visible keyboard focus everywhere — one rule, both grounds. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: fixed; top: 0; left: 0; z-index: 200;
  width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
}
.skip:focus {
  width: auto; height: auto; clip-path: none;
  inset-block-start: 12px; inset-inline-start: 12px;
  background: var(--accent); color: var(--accent-label);
  padding: 12px 20px; font-weight: 600;
}

/* --- display type -------------------------------------------------- */

/* Inter needs to be pulled in tighter than a serif at display sizes, or the
   words drift apart and the heading loses its block. */
.display {
  font-family: var(--display);
  font-weight: 500;
  line-height: 1.08;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
.d-xl { font-size: clamp(2.6rem, 6.4vw, 5.6rem); }
.d-lg { font-size: clamp(2.1rem, 4.6vw, 4.2rem); }
.d-md { font-size: clamp(1.7rem, 3vw, 2.6rem); }

.eyebrow {
  font-family: var(--mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 1.6rem;
}
.light .eyebrow { color: var(--ink-faint); }

.lede {
  font-size: clamp(1.05rem, 1.5vw, 1.32rem);
  line-height: 1.55;
  color: var(--ink-soft);
  max-width: 58ch;
  text-wrap: pretty;
}
.light .lede { color: var(--ink-soft); }

/* --- section rhythm ------------------------------------------------ */

/* Vertical rhythm: enough air to separate sections, tight enough that a
   two-line heading doesn't float in an empty band. */
section { padding-block: clamp(64px, 6.4vw, 112px); }
/* The bands fade into the page rather than butting against it, so there is
   no hard line where one section ends. The stops are fixed px, not
   percentages — a percentage fade scales with section height and reads as a
   wash on a tall section and a hard edge on a short one. */
.light {
  background: linear-gradient(
    180deg,
    var(--ground) 0,
    var(--band) 140px,
    var(--band) calc(100% - 140px),
    var(--ground) 100%
  );
  color: var(--ink);
}
.light-2 {
  background: linear-gradient(
    180deg,
    var(--ground) 0,
    var(--band-2) 140px,
    var(--band-2) calc(100% - 140px),
    var(--ground) 100%
  );
  color: var(--ink);
}

.dark { background: var(--ground); color: var(--ink); }
.dark-2 { background: var(--ground-2); color: var(--ink); }

/* A fixed grain overlay across the whole page. Large flat dark areas band
   badly on 8-bit displays and read as sterile vector fill; a little noise
   breaks both. Two SVG octaves rather than an image so it costs no request,
   and pointer-events:none so it never intercepts a click. */
body::after {
  content: "";
  position: fixed; inset: 0; z-index: 300;
  pointer-events: none;
  opacity: .028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
@media (prefers-reduced-motion: reduce) { body::after { opacity: .02; } }

/* The CTA band carries the same pale mint as the home page's .light
   sections, faded into the page at both edges. --accent is the signal mint:
   it is for fills and marks, and across a full-bleed band it overwhelms
   everything sitting on it. */
.gold-band {
  background: linear-gradient(
    180deg,
    var(--ground) 0,
    var(--band) 140px,
    var(--band) calc(100% - 140px),
    var(--ground) 100%
  );
  color: var(--ink);
}


.sec-head { margin-bottom: clamp(34px, 3.8vw, 64px); }
.sec-head.center { text-align: center; }
.sec-head.center .display { margin-inline: auto; max-width: 20ch; }

/* --- buttons ------------------------------------------------------- */

/* Tracking is positive here and nowhere else: at 0.86rem uppercase the
   letterforms close up, and the label is short enough that opening it costs
   no line length. */
.btn {
  display: inline-flex; align-items: center; gap: 12px;
  padding: 15px 28px;
  border-radius: 100px; border: 1px solid transparent;
  font-size: 0.86rem; font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform .18s var(--ease-out), background-color .3s var(--ease),
              color .3s var(--ease), border-color .3s var(--ease),
              box-shadow .3s var(--ease);
}
.btn svg { width: 15px; height: 15px; transition: transform .4s var(--ease-out); }
.btn:hover svg, .btn:focus-visible svg { transform: translateX(4px); }
/* Pressed feedback. Without it a button that only lifts on hover feels like
   an image; the dip on :active is what makes it read as a physical control. */
.btn:active { transform: translateY(1px) scale(.99); }

/* The one loud control on the page: solid accent, page-dark label. Dark text
   on the mint rather than white — the fill is light enough that white sits at
   1.9:1 on it and effectively disappears. */
.btn-gold { background: var(--accent); color: var(--accent-label); box-shadow: var(--shadow-sm); }
.btn-gold:hover, .btn-gold:focus-visible {
  background: var(--accent-deep); transform: translateY(-2px);
  box-shadow: 0 10px 30px -10px rgb(55 202 236 / .35);
}

/* Light fill on a dark page — the inverse of what it was, and still the
   quiet counterpart to the accent button. */
.btn-ink { background: var(--ink); color: var(--ground); }
.btn-ink:hover, .btn-ink:focus-visible {
  background: var(--accent); color: var(--accent-label); transform: translateY(-2px);
  box-shadow: 0 10px 30px -10px rgb(55 202 236 / .32);
}

.btn-line { border-color: var(--rule); color: var(--ink); }
.btn-line:hover, .btn-line:focus-visible {
  background: var(--raise); color: var(--accent-ink);
  border-color: var(--accent-ink); transform: translateY(-2px);
}

/* ==========================================================================
   HEADER
   ========================================================================== */

/* The header is its own surface and carries a local ink ramp. It opens
   transparent over the hero, so the hero reads full-bleed, and condenses into
   a blurred glass bar once the page moves. */
.site-header {
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: 100;
  --ink:        #F4F6F7;
  --ink-soft:   #D6DADE;
  --ink-faint:  #8A9199;
  --rule:       transparent;
  --accent-ink: #2A93D5;
  background: transparent;
  color: var(--ink);
  border-bottom: 1px solid var(--rule);
  transition: background-color .35s var(--ease), border-color .35s var(--ease),
              color .35s var(--ease), backdrop-filter .35s var(--ease);
}
/* Real glass, not a flat translucent panel: the blur plus a 1px top highlight
   simulates the refraction at the edge of a pane, which is what separates
   this from a plain rgba() fill. */
.site-header.is-stuck {
  --ink-faint:  #8A9199;
  --rule:       #21252B;
  background: rgb(8 9 11 / .72);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
          backdrop-filter: blur(16px) saturate(1.4);
  border-bottom-color: var(--rule);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .05), 0 1px 24px -8px rgb(2 4 6 / .8);
}
/* In the light theme the header's local ramp has to invert outright, not just
   when it sticks: the hero underneath is the light --ground, so the light ink
   the dark theme relies on leaves the wordmark and nav nearly invisible over
   it. Applied to the header itself so it covers the transparent state too. */
:root[data-theme="light"] .site-header {
  --ink:        #14171A;
  --ink-soft:   #4A5157;
  --ink-faint:  #65626F;
  --accent-ink: #125488;
}
:root[data-theme="light"] .site-header.is-stuck {
  --rule:       #E3E1E8;
  background: rgb(251 251 250 / .78);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .6), 0 1px 24px -8px rgb(22 21 26 / .16);
}
/* Safari before 18 and Firefox without the pref shipped no backdrop-filter;
   a 72%-opaque bar over scrolling content is unreadable without the blur, so
   those browsers get an opaque bar instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header.is-stuck { background: var(--band); }
}
/* Wordmark left, everything else pushed to the right edge. */
.header-inner {
  display: flex; align-items: center;
  gap: 40px; height: 76px;
}
.header-inner > nav { margin-inline-start: auto; }
.wordmark {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 1.32rem; font-weight: 700; letter-spacing: -0.03em;
}
.wordmark img { width: 30px; height: 30px; }

/* Header carries the mark alone. The name stays in the DOM but is taken off
   screen, so the home link keeps an accessible name instead of resolving to
   an unlabelled image. The footer still shows the full lockup.
   The mark ships as dark artwork and the bar is dark at every scroll
   position now, so it is inverted unconditionally rather than on .is-stuck. */
.site-header .wordmark span {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}
.site-header .wordmark img {
  width: 36px; height: 36px;
  filter: invert(1);
}
/* The artwork is already dark, so on paper the HEADER mark needs no
   inversion — inverted there it turned white and disappeared into the bar.
   The footer is deliberately not listed: its ground is pure black in both
   themes, so its mark stays inverted regardless. */
:root[data-theme="light"] .site-header .wordmark img { filter: none; }

/* Ambient pixel block sitting under the service list in the rail. */
.acc-rail { display: flex; flex-direction: column; }
.pixel-art {
  flex: 1 1 auto;
  width: 100%; min-height: 200px;
  align-self: center;
  max-width: 340px;
}
@media (max-width: 620px) {
  .team-grid { grid-template-columns: 1fr; }
}

@media (max-width: 900px) { .pixel-art { display: none; } }

.nav-desk { display: flex; align-items: center; gap: 30px; }
.nav-desk > li { position: relative; }
.nav-link {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 24px 0;
  font-size: 0.79rem; font-weight: 500;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: none; border: 0; cursor: pointer;
  transition: color .25s var(--ease);
}
.nav-link:hover, .nav-link:focus-visible, .nav-link[aria-expanded="true"] { color: var(--accent-ink); }
.nav-link .caret { width: 9px; height: 9px; transition: transform .3s var(--ease); }
.nav-link[aria-expanded="true"] .caret { transform: rotate(180deg); }
.nav-link[aria-current="page"] { color: var(--accent-ink); }

/* Mega menu. It hangs off the black bar, so it states its own surface
   explicitly — inheriting the page's --ground-2 (white) while the header
   ramp sets --ink to white left every label invisible. */
.mega {
  position: fixed; inset-inline: 0; inset-block-start: 76px;
  background: rgb(13 15 18 / .88);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
          backdrop-filter: blur(20px) saturate(1.4);
  border-top: 1px solid var(--rule);
  box-shadow: var(--shadow-lg);
  display: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .mega { background: var(--band); }
}
.mega[data-open="true"] { display: block; }
/* The panel is a full-bleed row of hairline-separated cells, like .ind-grid
   and .prod-grid. Its markup wraps the grid in a .shell, whose gutter inset
   the cells ~37px from each edge while the bar above them ran edge to edge —
   so the panel stopped short and the page showed through as a dark strip
   down both sides. The gutter is cancelled here rather than by removing the
   wrapper, which is shared markup lifted into every built page. */
.mega > .shell { padding-inline: 0; max-width: none; }
.mega-grid {
  display: grid; gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  background: var(--rule);
}
.mega-item {
  position: relative;
  background: var(--band);
  padding: 30px 26px 34px;
  display: flex; flex-direction: column; gap: 18px;
  transition: background-color .3s var(--ease);
}
.mega-item:hover, .mega-item:focus-visible { background: var(--raise); }
/* A hairline of accent along the top edge of the hovered cell, drawn rather
   than bordered so it costs no layout and can fade rather than snap. */
.mega-item::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: var(--accent); opacity: 0;
  transition: opacity .3s var(--ease);
}
.mega-item:hover::before, .mega-item:focus-visible::before { opacity: 1; }
.mega-item h3 { font-size: 1rem; font-weight: 500; line-height: 1.3; color: var(--ink); }
/* Two lines are RESERVED whether the text needs them or not.
   The descriptions are not the same length, and in Arabic the longest wraps
   while its three neighbours do not. Because the grid is auto-fit, each cell
   sized to its own content: one card ran 25px taller, its arrow dropped
   below the others, and the row closed on a ragged edge. min-height holds
   the block at two lines everywhere, so the arrows share one baseline and
   the panel bottom is straight in both languages.

   Set in em so it tracks this element's own font-size: 2 lines x 1.5
   line-height. A px value would drift the moment the type scale moves. */
.mega-item p {
  font-size: 0.83rem; color: var(--ink-faint); line-height: 1.5;
  min-height: 3em;
}
/* Each destination gets its own mark — no glyph is reused across two meanings. */
.mega-mark {
  width: 34px; height: 34px; flex: 0 0 auto;
  color: var(--accent-ink);
}
.mega-mark svg { width: 100%; height: 100%; }
.mega-foot {
  display: flex; align-items: center; justify-content: flex-end;
  margin-top: auto; color: var(--ink-faint);
  transition: color .3s var(--ease), transform .35s var(--ease-out);
}
.mega-item:hover .mega-foot { color: var(--accent-ink); transform: translateX(4px); }

/* header actions */
.header-actions { display: flex; align-items: center; gap: 18px; }
.nav-toggle {
  display: none;
  width: 42px; height: 42px;
  align-items: center; justify-content: center;
  background: none; border: 1px solid var(--rule); border-radius: 100px;
  cursor: pointer;
}
.nav-toggle span {
  position: relative; display: block;
  width: 15px; height: 1.5px; background: currentColor;
  transition: transform .35s var(--ease-out);
}
.nav-toggle span::before, .nav-toggle span::after {
  content: ""; position: absolute; left: 0;
  width: 15px; height: 1.5px; background: currentColor;
  transition: transform .35s var(--ease-out);
}
.nav-toggle span::before { transform: translateY(-5px); }
.nav-toggle span::after  { transform: translateY(5px); }
.nav-toggle[aria-expanded="true"] span { background: transparent; }
.nav-toggle[aria-expanded="true"] span::before { transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] span::after  { transform: rotate(-45deg); }

/* --- mobile panel --------------------------------------------------- */

.nav-mobile {
  position: fixed; inset: 72px 0 0; z-index: 90;
  background: var(--ground);
  overflow-y: auto;
  padding-bottom: 60px;
  transform: translateY(-8px); opacity: 0;
  visibility: hidden; pointer-events: none;
  transition: transform .4s var(--ease-out), opacity .3s var(--ease), visibility .4s;
}
.nav-mobile[data-open="true"] {
  transform: none; opacity: 1; visibility: visible; pointer-events: auto;
}
.nav-mobile h2 {
  font-family: var(--display);
  font-size: 1.7rem;
  padding: 26px var(--gut) 14px;
  color: var(--ink);
}
/* :not(.btn) matters. These rules style the panel's list rows, but the
   closing CTA is an <a class="btn btn-gold"> inside the same panel, and
   `.nav-mobile a` outranks `.btn-gold` on specificity — so without the
   exclusion the button lost its dark label to --ink-soft and rendered light
   text on the mint fill at 1.2:1. */
.nav-mobile a:not(.btn) {
  display: block;
  padding: 15px var(--gut);
  font-size: 1rem;
  color: var(--ink-soft);
  border-bottom: 1px solid var(--rule);
}
.nav-mobile a:not(.btn):hover,
.nav-mobile a:not(.btn):focus-visible { color: var(--accent-ink); background: var(--ground-2); }
.nav-mobile .m-top { border-top: 1px solid var(--rule); }
.nav-mobile .m-cta { padding: 28px var(--gut) 0; }

/* ==========================================================================
   HERO
   ========================================================================== */

/* No top margin: the header is transparent until scroll, so the hero runs
   under it to the top of the viewport and the glow is not cut off by a bar. */
.hero {
  position: relative;
  min-height: min(94dvh, 900px);
  display: grid; place-items: center;
  padding-block: 0;
  overflow: hidden;
  text-align: center;
  background: var(--ground);
}
/* Two ambient washes, offset from each other and from centre, rather than one
   symmetrical glow behind the headline. An even radial centred on the text
   reads as a vignette artefact; two off-axis pools read as light in a room. */
.hero::before {
  content: ""; position: absolute; inset: -20% -10% auto -10%; height: 130%;
  background:
    radial-gradient(58% 46% at 50% 8%,  rgb(55 202 236 / .18), transparent 68%),
    radial-gradient(42% 38% at 82% 62%, rgb(18 84 136 / .13), transparent 70%);
  pointer-events: none; z-index: 0;
}
/* A hairline across the foot of the hero, brightest in the middle and fading
   out at both ends, so the section closes on a horizon rather than an edge. */
.hero::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--rule) 22%, rgb(55 202 236 / .45) 50%, var(--rule) 78%, transparent);
  pointer-events: none; z-index: 1;
}
.hero-body { position: relative; z-index: 2; padding-block: 150px 110px; }
.hero h1 { margin-bottom: 1.6rem; }
/* The accent line is what the eye lands on first, so it carries the accent
   colour rather than sitting at the same weight as the line below it. */
.hero h1 .gold {
  font-weight: inherit;
  background: linear-gradient(180deg, var(--accent-grad-a) 0%, var(--accent-grad-b) 130%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.hero .lede { margin-inline: auto; margin-bottom: 2.6rem; }

/* Small capsule above the headline — the "what this is" line that a hero
   needs before the claim lands. */
.hero-badge {
  display: inline-flex; align-items: center; gap: 9px;
  margin-bottom: 2rem;
  padding: 7px 15px 7px 11px;
  border: 1px solid var(--rule); border-radius: 100px;
  /* --raise at 70%, so the capsule lifts off whichever ground is under it
     instead of staying the dark fill it was written as. */
  background: color-mix(in srgb, var(--raise) 70%, transparent);
  font-family: var(--mono); font-size: .71rem; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-soft);
}
/* Lime, not blue. This is the reserved signal colour and this dot is one
   of the few places it is spent: a live status light reads as lime and would
   be invisible as one more blue mark among the blue ones around it. */
.hero-badge .dot {
  width: 6px; height: 6px; border-radius: 100px;
  background: var(--signal); flex: 0 0 auto;
  box-shadow: 0 0 0 3px rgb(198 255 51 / .18);
}
/* The pulse is the only thing on the page that loops without provocation, so
   it is kept slow and shallow — a status light, not an attention grab. */
@media (prefers-reduced-motion: no-preference) {
  .hero-badge .dot { animation: dotPulse 2.6s var(--ease) infinite; }
  @keyframes dotPulse {
    0%, 100% { box-shadow: 0 0 0 3px rgb(198 255 51 / .18); }
    50%      { box-shadow: 0 0 0 6px rgb(198 255 51 / .05); }
  }
}
[dir="rtl"] .hero-badge { letter-spacing: 0; padding: 7px 11px 7px 15px; }

/* ==========================================================================
   LOGO WALL
   ========================================================================== */

.logo-wall {
  display: grid; gap: 1px;
  grid-template-columns: repeat(4, 1fr);
  background: var(--rule);
  border-block: 1px solid var(--rule);
}
.logo-wall li {
  background: var(--ground);
  display: grid; place-items: center;
  padding: 42px 20px;
  font-family: var(--display);
  font-size: clamp(1.1rem, 1.8vw, 1.55rem);
  color: var(--ink-soft);
  letter-spacing: -0.01em;
  transition: color .3s var(--ease), background-color .3s var(--ease);
}
.logo-wall li:hover { color: var(--accent-ink); background: var(--ground-2); }

/* ==========================================================================
   MARQUEE (stats)
   ========================================================================== */

.marquee { overflow: hidden; border-block: 1px solid var(--rule); padding-block: 34px; }
.marquee-track {
  display: flex; align-items: center; gap: 64px;
  width: max-content;
  animation: slide 34s linear infinite;
}
.marquee:hover .marquee-track { animation-play-state: paused; }
.marquee-item {
  display: flex; align-items: baseline; gap: 14px;
  font-family: var(--display);
  font-size: clamp(1.5rem, 2.6vw, 2.3rem);
  white-space: nowrap;
}
.marquee-item b { color: var(--accent-ink); font-weight: 400; font-variant-numeric: tabular-nums; }
.marquee-item span { font-family: var(--body); font-size: 0.8rem; text-transform: uppercase; color: var(--ink-faint); }
@keyframes slide { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ==========================================================================
   STICKY SCROLL ACCORDION  (services + process)
   Left rail of labels stays pinned; the right panel swaps as you scroll.
   ========================================================================== */

/* Roughly a third / two thirds: the rail is wide enough to hold a two-word
   service name on one line, the panel wide enough for a real measure. */
.acc { display: grid; grid-template-columns: minmax(0, 0.327fr) minmax(0, 0.673fr); }
.acc-rail { border-inline-end: 1px solid var(--rule); }
.dark .acc-rail, .dark-2 .acc-rail { border-inline-end-color: var(--rule); }

.acc-btn {
  position: relative;
  display: flex; align-items: center; gap: 18px;
  width: 100%;
  padding: 26px clamp(16px, 2.2vw, 34px);
  background: none; border: 0;
  border-bottom: 1px solid var(--rule);
  text-align: start; cursor: pointer;
  font-size: clamp(1rem, 1.45vw, 1.28rem);
  color: var(--ink-faint);
  transition: color .3s var(--ease), background-color .3s var(--ease);
}
.dark .acc-btn, .dark-2 .acc-btn { border-bottom-color: var(--rule); color: var(--ink-faint); }
.acc-btn:hover { color: var(--ink); }
.dark .acc-btn:hover, .dark-2 .acc-btn:hover { color: var(--ink); }
/* The selected row lifts out of the rail, matching the panel it opens, and
   carries an accent bar on its leading edge so the selection is legible
   without relying on the background step alone. */
.acc-btn[aria-selected="true"] { color: var(--ink); background: var(--raise); }
.acc-btn::after {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0;
  /* --accent-ink, not --accent: a 2px bar in the brand blue sits at 3.5:1
     against the rail and effectively disappears. The lifted hue reads. */
  width: 2px; background: var(--accent-ink);
  transform: scaleY(0); transform-origin: center;
  transition: transform .35s var(--ease-out);
}
.acc-btn[aria-selected="true"]::after { transform: scaleY(1); }
.dark .acc-btn[aria-selected="true"], .dark-2 .acc-btn[aria-selected="true"] { color: var(--ink); }
.acc-mark { width: 26px; height: 26px; flex: 0 0 auto; color: var(--ink-faint);
  transition: color .3s var(--ease); }
.acc-btn[aria-selected="true"] .acc-mark { color: var(--accent-ink); }
.acc-mark svg { width: 100%; height: 100%; }

/* The panel sits one elevation above the rail, so the active pane reads as a
   card lifted out of the list rather than another row of it. */
.acc-panels { position: relative; background: var(--raise); overflow: hidden; }
.acc-panel {
  padding: clamp(30px, 3.6vw, 58px) clamp(24px, 3vw, 54px) clamp(46px, 5vw, 78px);
  display: none;
}
.acc-panel[data-active="true"] { display: block; animation: panelIn .4s var(--ease-out); }
@keyframes panelIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* Panel title is set in the interface face, not the display serif — the serif
   belongs to the section heading above, and repeating it here flattens the
   hierarchy between the two. */
.acc-panel h3 {
  font-family: var(--body);
  font-size: clamp(1.4rem, 2.1vw, 1.95rem);
  font-weight: 400; letter-spacing: -0.02em;
  margin-bottom: 1.4rem;
}
.acc-panel .phase {
  font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 1rem;
}
.acc-panel > p { color: var(--ink-soft); max-width: 56ch; margin-bottom: 1.6rem; }

/* Small gold pill, sized to the label rather than the button grid. */
.acc-more {
  display: inline-flex; align-items: center; gap: 10px;
  /* White, not --ink. This is a blue fill, and --ink follows the theme, so
     on the light page it resolved to near-black on blue at 3.2:1. White is
     5.7:1 on the brand blue in both themes. */
  background: var(--accent); color: var(--accent-label);
  border-radius: 100px; padding: 11px 20px;
  font-size: 0.74rem; font-weight: 600;
  text-transform: uppercase;
  transition: background-color .3s var(--ease), transform .35s var(--ease-out);
}
.acc-more svg { width: 14px; height: 14px; transition: transform .35s var(--ease-out); }
.acc-more:hover, .acc-more:focus-visible { background: var(--accent-deep); color: var(--accent-label); transform: translateY(-1px); }
.acc-more:hover svg, .acc-more:focus-visible svg { transform: translateX(4px); }

/* Capability list: label left, mark right, hairline between. */
.acc-list {
  margin-top: clamp(30px, 4vw, 60px);
  border-top: 1px solid var(--rule);
  max-width: 46rem;
}
.acc-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 15px 2px;
  border-bottom: 1px solid var(--rule);
  font-size: 0.98rem;
  transition: padding-inline-start .3s var(--ease-out), color .25s var(--ease);
}
.acc-list li:hover { padding-inline-start: 12px; color: var(--accent-ink); }
.acc-list .tick { width: 18px; height: 18px; flex: 0 0 auto; color: var(--ink-faint); }
.acc-list li:hover .tick { color: var(--accent-ink); }
.acc-list .tick svg { width: 100%; height: 100%; }


/* ==========================================================================
   INDUSTRY GRID
   ========================================================================== */

.ind-grid {
  display: grid; gap: 1px;
  grid-template-columns: repeat(6, 1fr);
  background: var(--rule);
  border-block: 1px solid var(--rule);
}
.ind-grid li {
  position: relative;
  background: var(--band);
  padding: 34px 22px 40px;
  transition: background-color .35s var(--ease);
}
.ind-grid li:hover { background: var(--raise); }
.ind-grid li:hover .ind-icon { color: var(--accent-ink); }
/* display:block matters — a bare <span> is inline, so width/height would be
   ignored and the SVG would paint at its intrinsic size. */
.ind-icon {
  display: block; width: 30px; height: 30px;
  color: var(--ink-faint); margin-bottom: 2.4rem;
  transition: color .3s var(--ease);
}
.ind-icon svg { width: 100%; height: 100%; }
.ind-grid h3 { font-size: 1rem; font-weight: 500; }

/* ---------- products ------------------------------------------------
   Same hairline-separated cell language as .ind-grid, but the cells carry
   more copy, so they get their own column count and a taller rhythm. */
/* One row of five. Three columns left the last two stranded on a half-empty
   row with a visible dead cell, and there is no column count below five that
   divides five evenly — so the row stays whole and the cards narrow instead,
   dropping to a grid only once five would be too tight to read. */
.prod-grid {
  display: grid; gap: 1px;
  grid-template-columns: repeat(5, 1fr);
  background: var(--rule);
  border: 1px solid var(--rule);
}
/* The card holds only a mark and a label, so both centre: left-aligning two
   lone elements against a tall cell leaves the weight in one corner. */
.prod-card {
  position: relative;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 30px;
  background: var(--band-2);
  padding: 54px 28px 48px;
  min-height: 210px;
  transition: background-color .35s var(--ease);
}
.prod-card:hover { background: var(--raise-2); }
/* The supplied marks are horizontal wordmarks, not square glyphs, so the slot
   is sized by height and lets each one keep its own aspect ratio. They arrive
   in brand colour; height is capped so the three sit on a common baseline
   despite their differing viewBox proportions. */
.prod-logo {
  display: block; width: auto; height: 30px;
  max-width: 74%; object-fit: contain; margin: 0;
}

/* Brand marks on a dark ground.
   These files ship as dark artwork drawn for a white page: Salla is #004956,
   Zid #1c0531, and Replit and Claude each carry a near-black wordmark beside
   a coloured glyph. On #131519 all of those sit at roughly 1.2:1 and read as
   an empty cell.
   invert(1) alone would fix the luminance but also rotate every hue to its
   complement — Claude's orange would come out cyan, Replit's red-orange
   turquoise. Following it with hue-rotate(180deg) turns the hues back, so
   what changes is lightness and what survives is the brand colour. This is
   the standard pairing for exactly this problem, and it is applied only to
   the marks that need it rather than to the row as a whole. */
.prod-logo-claude,
.prod-logo-replit,
.prod-logo-salla,
.prod-logo-zid { filter: invert(1) hue-rotate(180deg); }

/* Lovable's mark is a full-spectrum gradient with a black wordmark. The
   invert trick mangles a gradient, so the whole lockup is knocked back to a
   flat page-ink silhouette instead — losing the colour, but staying legible
   and consistent with its neighbours. */
.prod-logo-lovable { filter: brightness(0) invert(.92); }

/* Every correction above exists to rescue dark artwork from a dark ground.
   The light theme is the page these files were drawn for, so they are handed
   back their own colours — Salla's teal and Zid's aubergine included. */
:root[data-theme="light"] .prod-logo-claude,
:root[data-theme="light"] .prod-logo-replit,
:root[data-theme="light"] .prod-logo-salla,
:root[data-theme="light"] .prod-logo-zid { filter: none; }
:root[data-theme="light"] .prod-logo-lovable { filter: none; }
/* Lovable's wordmark carries less padding inside its own viewBox than the
   other two, so at a matched height it reads noticeably larger. Trimmed 15%
   to sit optically level with them rather than mathematically level. */
.prod-logo-sm { height: 25.5px; }

/* Salla and Zid arrive in a 1000x1000 viewBox, but the artwork inside is a
   wide lockup — mark plus name — floating in a mostly empty square. Height
   alone therefore sizes the padding rather than the logo, so these are driven
   by width and given a generous height budget to grow into. */
/* Salla and Zid ship in a 1000x1000 viewBox with the artwork in a band across
   the middle, so sizing by height sizes the empty padding. The fix is to crop
   the box to the band — but their artwork is 2.37:1 and 3.79:1 respectively,
   so a single shared height clipped the squarer of the two. Each therefore
   gets its own ratio, and `contain` guarantees nothing is ever cut. */
/* Salla and Zid arrived in a 1000x1000 viewBox with the artwork in a narrow
   band across the middle, so a height applied to the file sized mostly empty
   padding. Cropping in CSS was the wrong tool: object-view-box resolves its
   percentages against the SVG's intrinsic size, which on these files is 150px
   rather than the 1000 of the viewBox, and object-fit then re-fit the result
   inside the box. The viewBox in each file is now trimmed to the artwork, so
   both size exactly like the three wordmarks. A touch more height than their
   30px because these are lockups: the name sits beside the mark. */
.prod-logo-sq { width: auto; height: 38px; max-width: 82%; }
/* Says what the mark means. A logo on its own reads as anything from "built
   with" to "we integrate with", and the caption settles it. */
.prod-for {
  font-family: var(--mono); font-size: .68rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint);
  margin: -18px 0 0; text-align: center;
}
.prod-status {
  font-family: var(--mono); font-size: .68rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-faint); margin-bottom: .7rem;
}
.prod-card h3 { font-size: 1.18rem; font-weight: 500; margin-bottom: .6rem; }
.prod-note { font-size: .94rem; color: var(--ink-soft); margin: 0; }
/* margin-top:auto pins the label to the foot of the cell, so the three cards
   align on it however long their descriptions run. */
/* Tall enough for the glyphs to survive being resampled onto a 7px lattice:
   at 34px the string had roughly four module rows to work with and came out
   an illegible smear. Letterforms need ~9 rows before they read. */
/* Width is capped as well as height: the string is fitted to whichever axis
   runs out first, so a full-width canvas in a short box sets tiny glyphs on a
   wide lattice and the letterforms collapse into a smear. */
.prod-soon { display: block; width: 82%; max-width: 240px; height: 74px; margin: 0; }

/* Off-canvas rather than display:none — a hidden element is skipped by screen
   readers, and these labels exist precisely to be read. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.ind-tag {
  position: absolute; inset-block-start: 14px; inset-inline-end: 14px;
  background: var(--accent); color: var(--accent-label);
  font-family: var(--mono); font-size: 0.62rem; font-weight: 700;
  padding: 4px 8px; border-radius: 100px;
}

/* ==========================================================================
   CASE CARDS  (drag carousel)
   ========================================================================== */

.rail-wrap { position: relative; }
.rail {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(270px, 1fr);
  gap: 1px; background: var(--rule);
  overflow-x: auto; scroll-snap-type: x mandatory;
  scrollbar-width: none;
  cursor: grab;
}
.rail::-webkit-scrollbar { display: none; }
.rail.is-drag { cursor: grabbing; scroll-snap-type: none; }
.case {
  background: var(--ground);
  scroll-snap-align: start;
  padding: 26px 26px 30px;
  display: flex; flex-direction: column; gap: 20px;
  transition: background-color .35s var(--ease);
}
.case:hover { background: var(--ground-2); }
.case-img { aspect-ratio: 16/10; overflow: hidden; background: var(--slate); }
.case-img img { width: 100%; height: 100%; object-fit: cover; transition: transform .7s var(--ease-out); }
.case:hover .case-img img { transform: scale(1.05); }
.case h3 { font-size: 1.02rem; font-weight: 400; line-height: 1.45; color: var(--ink); }
.case .arrow { margin-top: auto; color: var(--ink-faint); transition: transform .4s var(--ease-out), color .3s var(--ease); }
.case:hover .arrow { color: var(--accent-ink); transform: translateX(5px); }
.rail-hint {
  font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; color: var(--ink-faint);
  margin-top: 20px; text-align: center;
}

/* ==========================================================================
   ENGAGEMENT PANELS
   ========================================================================== */

.eng {
  display: grid; gap: 1px;
  grid-template-columns: repeat(3, 1fr);
  background: var(--rule);
  border-block: 1px solid var(--rule);
}
/* At rest a panel shows its name over a generated motif. On hover, focus or
   tap it lifts an elevation and the motif swaps for the description. */
.eng-card {
  position: relative;
  background: var(--band);
  padding: clamp(26px, 3vw, 44px);
  outline: none;
  transition: background-color .45s var(--ease);
}
.eng-card:hover, .eng-card:focus-visible, .eng-card:focus-within { background: var(--raise); }
.eng-card:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }
/* Spotlight border: a soft accent pool tracking the cursor, masked to the
   card's 1px edge so only the border lights up, not the face of the card.
   --mx/--my are set from JS; the fallback keeps it centred without them. */
.eng-card::before {
  content: ""; position: absolute; inset: 0;
  padding: 1px; border-radius: inherit;
  background: radial-gradient(200px circle at var(--mx, 50%) var(--my, 50%),
              rgb(55 202 236 / .65), transparent 70%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  opacity: 0; transition: opacity .35s var(--ease);
  pointer-events: none;
}
.eng-card:hover::before, .eng-card:focus-within::before { opacity: 1; }

.eng-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; margin-bottom: 2.4rem;
}
.eng-head h3 { font-size: clamp(1.35rem, 2.2vw, 1.9rem); font-weight: 400; }

/* Drawn rather than typed, so the two strokes stay optically even and the
   horizontal can rotate away on hover. */
.eng-head .plus {
  position: relative; flex: 0 0 auto;
  width: 26px; height: 26px; margin-top: .35rem;
}
.eng-head .plus::before, .eng-head .plus::after {
  content: ""; position: absolute; inset: 50% 0 auto 0;
  height: 1.5px; background: var(--ink);
  transition: transform .45s var(--ease-out);
}
.eng-head .plus::after { transform: rotate(90deg); }
.eng-card:hover .plus::after,
.eng-card:focus-within .plus::after { transform: rotate(180deg); }

/* Motif and copy share one grid cell, so the swap costs no layout shift. */
.eng-body { display: grid; min-height: clamp(280px, 26vw, 360px); }
.eng-body > * { grid-area: 1 / 1; }

/* The block sits smaller than its cell — the copy that replaces it on hover
   needs the room more than the decoration does. */
/* A fixed 256px — not a fluid clamp. The motif is a 64-row grid drawn in whole
   device pixels, so the box has to be a multiple of 64 device pixels or the
   grid cannot divide it evenly and the leftover shows up as a ragged edge.
   256 divides exactly at 1x, 2x and 3x (pitch 4, 8 and 12), which a clamp()
   that resolves to an arbitrary viewport-dependent width can never guarantee.
   This also sits close to .team-face, so motif and portrait modules read at
   the same weight. */
.eng-motif {
  width: 256px;
  /* Backstop only. 256 fits the card at every breakpoint where the motif is
     visible, but below ~350px the content box is narrower than that and the
     canvas would overflow. Clamping costs the exact-division property the
     fixed width buys, so it engages only where the alternative is overflow. */
  max-width: 100%;
  aspect-ratio: 1;
  justify-self: center; align-self: center;
  opacity: 1;
  transition: opacity .4s var(--ease);
}
.eng-card:hover .eng-motif,
.eng-card:focus-within .eng-motif { opacity: 0; }

.eng-reveal {
  opacity: 0; transform: translateY(8px);
  pointer-events: none;
  transition: opacity .45s var(--ease-out), transform .5s var(--ease-out);
}
.eng-card:hover .eng-reveal,
.eng-card:focus-within .eng-reveal { opacity: 1; transform: none; pointer-events: auto; }

.eng-reveal > p { color: var(--ink-soft); font-size: 1rem; max-width: 42ch; }
.eng-benefits { margin-top: clamp(28px, 4vw, 56px); }
.eng-benefits h4 {
  font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 0.9rem;
}
.eng-benefits li {
  display: flex; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--rule);
  font-size: 0.95rem; color: var(--ink-soft);
}
.eng-benefits li::before { content: "•"; color: var(--accent-ink); }

/* No hover on touch: show the copy outright rather than making it
   unreachable, and drop the motif that would sit under it. */
@media (hover: none) {
  .eng-card { background: var(--ground); }
  .eng-motif { display: none; }
  .eng-reveal { opacity: 1; transform: none; pointer-events: auto; }
  .eng-body { min-height: 0; }
}

/* ==========================================================================
   CTA BAND
   ========================================================================== */

.cta-band h2 { max-width: 18ch; margin-bottom: 2rem; }
/* --ink-soft, not the warm brown that was tuned for the saturated mint: it
   goes muddy on the pale band, and this is the tone the rest of the site
   sets body copy in (9.4:1 on the band). */
.cta-band p { color: var(--ink-soft); max-width: 52ch; margin-bottom: 2.4rem; font-size: 1.06rem; }
.cta-band .btn-line { border-color: var(--ink); }
.cta-band .btn-line:hover { background: var(--ink); color: var(--accent-ink); border-color: var(--ink); }

/* ==========================================================================
   CONTACT FORM
   ========================================================================== */

.contact-grid {
  display: grid; gap: clamp(32px, 5vw, 80px);
  grid-template-columns: minmax(0, 0.6fr) minmax(0, 1.4fr);
  align-items: start;
}
/* Two columns, so the short fields pair up and the whole form is visible
   without scrolling. Message, consent and the submit row run full width. */
.contact-card {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(20px, 2.4vw, 36px);
  align-content: start;
}
.contact-card > .field-wide,
.contact-card > .consent,
.contact-card > .btn,
.contact-card > .form-status { grid-column: 1 / -1; }

/* A grid item stretches to fill its track by default, which pulls the submit
   button across the full width. It sizes to its own label instead. */
.contact-card > .btn { justify-self: start; }

.form-field { margin-bottom: 18px; }
.form-field label {
  display: block;
  font-family: var(--mono); font-size: 0.74rem;
  text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: 6px;
}
/* Tells the engine this is a dark UI, so the select popup, the caret, the
   text cursor and the scrollbars all render dark instead of the light
   defaults they would otherwise inherit from the platform. */
.form-field input, .form-field textarea, .form-field select {
  width: 100%;
  font: inherit; font-size: 1rem;
  color: var(--ink);
  background: transparent;
  color-scheme: dark;
  border: 0; border-bottom: 1px solid var(--rule);
  padding: 8px 0;
  border-radius: 0;
  transition: border-color .3s var(--ease);
}
.form-field select { cursor: pointer; }
/* The option list is a native popup, so it takes its colours from the OS
   rather than the page. color-scheme above makes it dark; these keep the
   text on it consistent with the form on browsers that honour the rule. */
.form-field select option { background: var(--raise); color: var(--ink); }
.form-field textarea { resize: vertical; min-height: 84px; }

/* Autofill paints a solid light background that no `background` declaration
   can override. The workaround is an inset shadow large enough to cover the
   field, plus -webkit-text-fill-color for the text the same rule would
   otherwise leave black. The long delay keeps the shadow from ever animating
   back to the light default. */
.form-field input:-webkit-autofill,
.form-field input:-webkit-autofill:hover,
.form-field input:-webkit-autofill:focus,
.form-field textarea:-webkit-autofill,
.form-field select:-webkit-autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 1000px var(--band) inset;
  caret-color: var(--ink);
  transition: background-color 600000s 0s, color 600000s 0s;
}
.form-field ::placeholder { color: var(--ink-faint); opacity: 1; }
.form-field input:focus, .form-field textarea:focus, .form-field select:focus {
  outline: none; border-bottom-color: var(--accent-ink);
}
.form-field input:focus-visible, .form-field textarea:focus-visible, .form-field select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 4px;
}
.consent { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 28px; font-size: 0.88rem; color: var(--ink-soft); }
.consent input { width: 18px; height: 18px; flex: 0 0 auto; margin-top: 2px; accent-color: var(--accent-ink); }
.consent a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 3px; }
.form-status { margin-top: 18px; font-size: 0.92rem; min-height: 1.4em; }
.form-status:empty { display: none; }
.form-status[data-kind="ok"] { color: var(--accent-ink); }
.form-status[data-kind="err"] { color: var(--err); }

.contact-detail { display: flex; flex-direction: column; gap: 30px; }
.contact-detail h2,
.contact-detail h3 {
  font-family: var(--mono); font-size: 0.74rem; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 8px;
}
.contact-detail a, .contact-detail p { font-size: 1.05rem; color: var(--ink); }
.contact-detail a:hover { color: var(--accent-ink); }

/* ==========================================================================
   FOOTER
   ========================================================================== */

/* Pure black in BOTH themes — the one surface on the site that never follows
   the theme. Because the light page would otherwise put its dark ink on this
   black ground, the footer carries its own ramp rather than inheriting the
   page's: every token below is restated for a dark surface, so the same
   markup reads correctly whichever theme is active. */
.site-footer {
  --ink:        #F4F6F7;
  --ink-soft:   #B7B5C0;
  --ink-faint:  #82808F;
  --rule:       #1E1C26;
  --accent-ink: #2A93D5;
  --ground-2:   #0E0D13;

  background: #000000;
  color: var(--ink);
  border-top: 1px solid var(--rule);
  padding-block: clamp(52px, 6vw, 80px) 36px;
}
/* The mark ships as dark artwork, so it inverts to read on this ground —
   unconditionally, since the footer is pure black in both themes. */
.site-footer .wordmark img { filter: invert(1); }

.footer-grid {
  display: grid; gap: clamp(32px, 4vw, 56px);
  grid-template-columns: 1.6fr 1fr 1fr 1.1fr;
  padding-bottom: clamp(40px, 5vw, 64px);
}
.footer-grid h4 {
  font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 1.1rem;
}
.footer-grid nav { display: flex; flex-direction: column; gap: 11px; }
.footer-grid nav a { font-size: 0.94rem; color: var(--ink-soft); transition: color .25s var(--ease); }
.footer-grid nav a:hover { color: var(--accent-ink); }
.footer-brand p { color: var(--ink-soft); font-size: 0.94rem; margin-top: 1rem; max-width: 34ch; }
.footer-bottom {
  display: flex; justify-content: space-between; align-items: center; gap: 20px; flex-wrap: wrap;
  padding-top: 26px;
  border-top: 1px solid var(--rule);
  font-size: 0.83rem; color: var(--ink-faint);
}
.social { display: flex; gap: 10px; }
.social a {
  display: grid; place-items: center;
  width: 38px; height: 38px;
  border: 1px solid var(--rule); border-radius: 100px;
  color: var(--ink-soft);
  transition: color .3s var(--ease), border-color .3s var(--ease), background-color .3s var(--ease);
}
.social a:hover, .social a:focus-visible { color: var(--accent-label); background: var(--accent); border-color: var(--accent-ink); }
.social svg { width: 17px; height: 17px; }

/* ==========================================================================
   INNER PAGES
   ========================================================================== */

/* Like .hero, this runs under the transparent header rather than starting
   below it, so the ambient glow reaches the top of the viewport. The top
   padding absorbs the 76px bar. */
.page-head {
  padding-block: clamp(150px, 14vw, 230px) clamp(44px, 5vw, 74px);
  /* Holds the pixel field the same way .hero does. */
  position: relative;
  overflow: hidden;
}
.page-head::before {
  content: ""; position: absolute; inset: -30% -10% auto -10%; height: 150%;
  background: radial-gradient(50% 44% at 34% 0%, rgb(55 202 236 / .14), transparent 70%);
  pointer-events: none; z-index: 0;
}
.page-head > .shell { position: relative; z-index: 1; }
.page-head h1 { margin-bottom: 1.4rem; }
.breadcrumb {
  font-family: var(--mono); font-size: 0.74rem; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 1.6rem;
}
.breadcrumb a:hover { color: var(--accent-ink); }

.prose-grid {
  display: grid; gap: clamp(28px, 4vw, 70px);
  grid-template-columns: minmax(180px, 250px) minmax(0, 1fr);
  padding-block: clamp(34px, 4vw, 54px);
  border-top: 1px solid var(--rule);
}
.prose-grid h2 {
  font-family: var(--mono); font-size: 0.74rem; font-weight: 400;
  text-transform: uppercase; color: var(--ink-faint);
}
.prose-grid p { color: var(--ink-soft); max-width: 62ch; margin-bottom: 1.1rem; }
.prose-grid p:last-child { margin-bottom: 0; }
.prose-grid ul { display: flex; flex-direction: column; gap: 10px; }
.prose-grid ul li { color: var(--ink-soft); padding-inline-start: 20px; position: relative; }
.prose-grid ul li::before {
  content: ""; position: absolute; inset-inline-start: 0; inset-block-start: .7em;
  width: 8px; height: 1px; background: var(--accent);
}

.team-grid { display: grid; gap: 1px; grid-template-columns: repeat(3, 1fr);
  background: var(--rule); border-block: 1px solid var(--rule); }
.team-grid li { background: var(--ground); padding: 34px 24px 38px; text-align: center; }
.team-face { display: block; width: clamp(224px, 24vw, 336px); aspect-ratio: 1;
  margin: 0 auto 26px; }
.team-grid .role { font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; color: var(--accent-ink); margin-bottom: .7rem; }
/* Set in caps to match the role label above it. Done here rather than in the
   markup so the name stays sentence-cased for screen readers and search, and
   so the Arabic names — which have no case — are untouched. */
.team-grid h3 {
  font-size: 1.05rem; font-weight: 500; margin-bottom: .5rem;
  text-transform: uppercase; letter-spacing: .04em;
}
.team-grid p { font-size: 0.9rem; color: var(--ink-faint); }

.job-list { border-top: 1px solid var(--rule); }
.job {
  display: grid; grid-template-columns: 1fr auto auto; gap: 24px; align-items: center;
  padding: 26px 4px;
  border-bottom: 1px solid var(--rule);
  transition: padding-inline-start .35s var(--ease-out), background-color .3s var(--ease);
}
.job:hover { padding-inline-start: 18px; background: var(--ground-2); }
.job h3 { font-size: clamp(1.1rem, 1.8vw, 1.45rem); font-weight: 400; font-family: var(--display); }
.job .meta { font-size: 0.85rem; color: var(--ink-faint); }
.job .arrow { color: var(--ink-faint); transition: transform .4s var(--ease-out), color .3s var(--ease); }
.job:hover .arrow { color: var(--accent-ink); transform: translateX(5px); }

/* ==========================================================================
   SCROLL-DRIVEN MOTION
   Everything here runs off scroll position rather than a timer, so nothing
   moves unless the reader is moving. All of it is inside a
   no-preference guard: with reduced motion the page is completely still and
   nothing below has any effect.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  /* --- reading progress ------------------------------------------------
     A hairline across the top of the header that fills as the document is
     read. animation-timeline does the work on Chrome and Edge; browsers
     without it simply never show the bar, which is why it starts at
     scaleX(0) and is decorative only. */
  @supports (animation-timeline: scroll()) {
    .site-header::after {
      content: "";
      position: absolute; inset: auto 0 0 0; height: 2px;
      background: linear-gradient(90deg, var(--accent), var(--signal));
      transform-origin: 0 50%;
      transform: scaleX(0);
      animation: readProgress linear;
      animation-timeline: scroll(root block);
    }
    [dir="rtl"] .site-header::after { transform-origin: 100% 50%; }
    @keyframes readProgress { to { transform: scaleX(1); } }
  }

  /* view() ties motion to the element's own passage through the viewport,
     so it is driven by the reader's scroll rather than by a fixed duration.

     Deliberately NOT applied to section headings: main.js already fades
     those in via .reveal-group, and two independent systems animating the
     same opacity and transform would fight over them. Only elements the
     observer does not touch are listed here. */
  @supports (animation-timeline: view()) {
    /* The cover drifts slightly slower than the page. A small range: real
       parallax on a tall image reads as the layout coming apart. */
    .wcase-media img,
    .prose-grid .case-shot img {
      animation: coverDrift linear both;
      animation-timeline: view(block);
      animation-range: cover 0% cover 100%;
    }
    @keyframes coverDrift {
      from { transform: scale(1.06) translateY(-1.2%); }
      to   { transform: scale(1.06) translateY(1.2%); }
    }
    /* Hover still owns the scale, so the drift must not fight it. */
    .wcase:hover .wcase-media img,
    .prose-grid .case-shot:hover img { animation-play-state: paused; }

  }

  /* --- link and row micro-interactions ---------------------------------
     An underline that grows from the leading edge rather than fading in. */
  .footer-grid nav a, .footer-contact a {
    position: relative;
    display: inline-block;
  }
  .footer-grid nav a::after, .footer-contact a::after {
    content: ""; position: absolute;
    inset-block-end: -2px; inset-inline-start: 0;
    width: 100%; height: 1px;
    background: currentColor;
    transform: scaleX(0); transform-origin: 0 50%;
    transition: transform .32s var(--ease-out);
  }
  [dir="rtl"] .footer-grid nav a::after,
  [dir="rtl"] .footer-contact a::after { transform-origin: 100% 50%; }
  .footer-grid nav a:hover::after, .footer-grid nav a:focus-visible::after,
  .footer-contact a:hover::after, .footer-contact a:focus-visible::after { transform: scaleX(1); }

  /* The nav items carry the same underline, so the header and footer agree. */
  .nav-desk .nav-link { position: relative; }
  .nav-desk .nav-link::after {
    content: ""; position: absolute;
    inset-block-end: 18px; inset-inline-start: 0;
    width: 100%; height: 1.5px;
    background: var(--accent-ink);
    transform: scaleX(0); transform-origin: 0 50%;
    transition: transform .3s var(--ease-out);
  }
  [dir="rtl"] .nav-desk .nav-link::after { transform-origin: 100% 50%; }
  .nav-desk .nav-link:hover::after,
  .nav-desk .nav-link:focus-visible::after,
  .nav-desk .nav-link[aria-current="page"]::after { transform: scaleX(1); }

  /* The team portraits lift on hover, matching the engagement cards. */
  .team-grid li {
    transition: transform .45s var(--ease-out), background-color .35s var(--ease);
  }
  .team-grid li:hover { transform: translateY(-4px); background: var(--raise); }

  /* Products: the mark lifts a touch out of its cell. */
  .prod-logo { transition: transform .45s var(--ease-out); }
  .prod-card:hover .prod-logo { transform: translateY(-4px) scale(1.04); }
}

/* ==========================================================================
   REVEAL
   ========================================================================== */

/* A short, shallow lift. Long fades look smooth in isolation but catch
   headings half-transparent when someone scrolls at speed, which reads as a
   rendering fault rather than an effect. */
/* Gated on .js — set by an inline script in the head, so it is true before
   first paint and the page never flashes its content before hiding it.

   Without the gate a reader with JavaScript disabled gets a page whose
   sections are permanently at opacity 0: nothing ever adds .in, so the
   content is there in the markup, selectable and read aloud by a screen
   reader, but invisible. Every rule below therefore only applies once we
   know something is around to reveal it again. */
.js .reveal { opacity: 0; transform: translateY(12px); }
.js .reveal.in {
  opacity: 1; transform: none;
  transition: opacity .45s var(--ease-out), transform .55s var(--ease-out);
}

/* A group whose children arrive one after another rather than as one block.
   The delay is a custom property set per item by the observer, so the cascade
   follows document order and costs no extra selectors as items are added.

   The step is deliberately short. At 70ms a row of three cards is fully in
   within a fifth of a second — enough to read as a sequence, not so much that
   the last card is still arriving after the eye has moved on. The delay
   applies only on the way in: `.reveal-item` alone carries no transition, so
   an item that is already visible never animates back out.

   A section head is always an eyebrow above a heading, sometimes with a lede
   under it. Those children are the items by definition, so they are matched
   structurally rather than tagged one by one on every page. */
.js .reveal-item,
.js .sec-head.reveal-group > * { opacity: 0; transform: translateY(14px); }
.js .reveal-group.in .reveal-item,
.js .sec-head.reveal-group.in > * {
  opacity: 1; transform: none;
  transition: opacity .5s var(--ease-out) var(--reveal-delay, 0ms),
              transform .6s var(--ease-out) var(--reveal-delay, 0ms);
}

/* ==========================================================================
   REDUCED MOTION — everything stops, nothing is lost
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .marquee-track { animation: none; flex-wrap: wrap; width: auto; justify-content: center; }
  .reveal, .reveal.in,
  .js .reveal, .js .reveal.in { opacity: 1; transform: none; }
  /* The delay has to go explicitly: the blanket rule above collapses
     transition-duration but leaves transition-delay intact, which would
     otherwise hold each item invisible for its full stagger. */
  .js .reveal-item, .js .reveal-group.in .reveal-item,
  .js .sec-head.reveal-group > *, .js .sec-head.reveal-group.in > * {
    opacity: 1; transform: none; transition-delay: 0ms !important;
  }
  .acc-panel[data-active="true"] { animation: none; }
}

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */

@media (max-width: 1080px) {
  /* Five tracks stop holding a logo and its caption around here. */
  .prod-grid { grid-template-columns: repeat(3, 1fr); }
  .ind-grid { grid-template-columns: repeat(3, 1fr); }
  .logo-wall { grid-template-columns: repeat(3, 1fr); }
  .team-grid { grid-template-columns: repeat(2, 1fr); }
  .eng { grid-template-columns: 1fr; }
  .contact-grid { grid-template-columns: 1fr; }
}

@media (max-width: 900px) {
  .nav-desk, .header-actions .btn { display: none; }
  .nav-toggle { display: inline-flex; }
  .acc { grid-template-columns: 1fr; }
  .acc-rail { border-inline-end: 0; border-bottom: 1px solid var(--rule); }
  .dark .acc-rail, .dark-2 .acc-rail { border-bottom-color: var(--rule); }
  .prose-grid { grid-template-columns: 1fr; gap: 14px; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .footer-brand { grid-column: 1 / -1; }
}

@media (max-width: 620px) {
  .logo-wall { grid-template-columns: repeat(2, 1fr); }
  .ind-grid { grid-template-columns: repeat(2, 1fr); }
  .prod-grid { grid-template-columns: repeat(2, 1fr); }
  .team-grid { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; }
  .job { grid-template-columns: 1fr auto; }
  .job .meta.hide-sm { display: none; }
  .hero-body { padding-block: 90px 70px; }
}

/* ==========================================================================
   ARABIC / RTL
   The layout mirrors on its own — everything above uses logical properties.
   What Arabic needs is its own type treatment, not a flipped Latin one.
   ========================================================================== */

@font-face {
  font-family: "Thmanyah";
  src: url("../fonts/ThmanyahSans-Light.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/* One face, everywhere, no exceptions.

   This was a list of specific selectors, and everything not named on it kept
   the Latin stack. Two visible failures came from that: anything on --mono
   (.hero-badge, .prod-for, .breadcrumb) fell through to Menlo and Courier,
   neither of which has a single Arabic glyph, so the browser drew tofu boxes;
   and .lede, .skip, .t-label, option and label silently set in Inter.

   The universal selector is the point — a rule that has to be extended by
   hand every time a component is added is the bug, not the fix. Latin runs
   inside Arabic (Claude Code, iOS, API) still resolve correctly because the
   Latin families stay in the stack behind Thmanyah. */
/* :where() holds this at zero specificity on the right-hand side, so the
   [dir="rtl"] alone carries it — but a two-class component rule like
   `.acc-panel .phase` would still outrank it and pull the mono stack back.
   !important is the blunt instrument that is actually correct here: this is
   a document-wide script constraint, not a component preference, and there
   is no valid reason for any component to set a Latin family on Arabic. */
[lang="ar"], [dir="rtl"],
[lang="ar"] :where(*), [dir="rtl"] :where(*) {
  font-family: "Thmanyah", "Geeza Pro", "Noto Naskh Arabic", system-ui, sans-serif !important;
  /* House rule: Western digits everywhere, in both languages. */
  font-variant-numeric: lining-nums;
  font-feature-settings: "lnum";
}
[lang="ar"], [dir="rtl"] { font-weight: 300; }

/* The face ships at 300 only, so nothing may ask for a heavier cut — the
   browser would synthesise a fake bold by smearing the outline, which on
   Arabic thickens the joins and fills in the counters. */
[dir="rtl"] b, [dir="rtl"] strong,
[dir="rtl"] h1, [dir="rtl"] h2, [dir="rtl"] h3,
[dir="rtl"] h4, [dir="rtl"] h5, [dir="rtl"] h6,
[dir="rtl"] .display, [dir="rtl"] .btn,
[dir="rtl"] .nav-link, [dir="rtl"] .wordmark { font-weight: 300; }

/* Uppercase is meaningless in Arabic and the wide Latin tracking pulls the
   joins apart, so every label reverts to normal setting rather than being
   listed one class at a time. */
[dir="rtl"] * {
  text-transform: none;
  letter-spacing: 0;
}

/* Headings need the looser leading Arabic asks for; the family, weight and
   tracking are already handled by the universal rules above. */
[dir="rtl"] .display,
[dir="rtl"] h1, [dir="rtl"] h2, [dir="rtl"] h3, [dir="rtl"] h4 {
  line-height: 1.4;
}
[dir="rtl"] .d-xl { font-size: clamp(2.1rem, 5vw, 4.4rem); }
[dir="rtl"] .d-lg { font-size: clamp(1.8rem, 3.8vw, 3.4rem); }
[dir="rtl"] .d-md { font-size: clamp(1.5rem, 2.6vw, 2.2rem); }
/* Arabic drops the gradient fill and takes a flat accent instead.

   `background-clip: text` paints the glyph through a gradient measured over
   the line box. Arabic hangs well below the baseline — the two dots under a
   final yeh, the tail of a noon — and at the bottom of that ramp there is
   effectively no paint left, so the dots rendered invisible and "بُني" read
   as "بُنى". Latin has almost nothing below the baseline, which is why the
   same rule is fine in English. A flat colour paints every part of the glyph
   at full strength. */
[dir="rtl"] .hero h1 .gold {
  font-weight: 300;
  background: none;
  -webkit-background-clip: border-box;
          background-clip: border-box;
  /* --accent-grad-a, not --accent: this is text, and the fill token is
     2.5:1 on the dark ground. The gradient's first stop is the same hue
     already tuned to read as type in both themes. */
  color: var(--accent-grad-a);
}
[dir="rtl"] .lede, [dir="rtl"] p { line-height: 1.8; letter-spacing: 0; }

/* The arrow points the way the language reads. */
[dir="rtl"] .btn svg,
[dir="rtl"] .acc-more svg,
[dir="rtl"] .case .arrow,
[dir="rtl"] .mega-foot { transform: scaleX(-1); }
[dir="rtl"] .btn:hover svg,
[dir="rtl"] .btn:focus-visible svg { transform: scaleX(-1) translateX(4px); }
[dir="rtl"] .acc-list li:hover { padding-inline-start: 0; padding-inline-end: 12px; }

/* Language switch */
.lang-toggle {
  font-size: 0.79rem; font-weight: 500;
  color: var(--ink-faint);
  padding: 8px 12px;
  border: 1px solid var(--rule);
  border-radius: 100px;
  transition: color .25s var(--ease), border-color .25s var(--ease);
}
.lang-toggle:hover, .lang-toggle:focus-visible { color: var(--accent-ink); border-color: var(--accent-ink); }

/* Sits beside the language switch and borrows its pill, but is square rather
   than text-width: it carries an icon, and a pill sized to a 16px glyph reads
   as a lozenge next to the word next to it. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: none;
  padding: 0;
  color: var(--ink-faint);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 100px;
  cursor: pointer;
  transition: color .25s var(--ease), border-color .25s var(--ease);
}
.theme-toggle:hover, .theme-toggle:focus-visible {
  color: var(--accent-ink); border-color: var(--accent-ink);
}
.theme-toggle svg { width: 16px; height: 16px; display: block; }

/* One button, two glyphs: each theme shows the one it would switch *to*, so
   the icon reads as the destination rather than as a status light. */
.theme-toggle .i-sun { display: none; }
:root[data-theme="light"] .theme-toggle .i-sun  { display: block; }
:root[data-theme="light"] .theme-toggle .i-moon { display: none; }

/* In the mobile panel it sits in the link stack, so it takes the row shape
   of the links around it rather than the header's pill. */
.nav-mobile .theme-toggle {
  width: auto; height: auto;
  gap: 12px;
  justify-content: flex-start;
  border: 0; border-radius: 0;
  padding: 0;
  font: inherit;
  color: inherit;
}
.nav-mobile .theme-toggle:hover { color: var(--accent-ink); }

/* The word only appears in the mobile stack, where the rows around it are
   words. In the header the icon stands alone and aria-label carries it. */
/* The label is for the mobile panel only — in the header the icon carries the
   button and the accessible name comes from aria-label. Its text names the
   theme a press switches TO, and is set from data-label-* by main.js. */
.t-label { display: none; }
.nav-mobile .t-label { display: inline; }
[dir="rtl"] .lang-toggle { letter-spacing: 0; font-family: var(--body); }

/* ==========================================================================
   WORK — pinned head, cases flowing past
   ========================================================================== */

.work-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.4fr);
  gap: clamp(32px, 5vw, 90px);
  align-items: start;
}

/* align-self:start is what lets sticky work here — a stretched grid item is
   already full height and has nothing to travel against. */
.work-head {
  position: sticky;
  inset-block-start: calc(76px + clamp(24px, 4vw, 64px));
  align-self: start;
  display: flex; flex-direction: column; align-items: flex-start;
}
 /* The pinned column is narrow, so the display step used across full-width
    heads overpowers it. One step down keeps the hierarchy without shouting. */
.work-head h2 {
  font-size: clamp(1.5rem, 2.4vw, 2.2rem);
  margin-bottom: 1.8rem;
}

.work-cases { display: flex; flex-direction: column; }
.wcase {
  padding-block: clamp(30px, 3.4vw, 54px);
  border-top: 1px solid var(--rule);
}
.wcase:first-child { border-top: 0; padding-block-start: 0; }
/* These are real product screenshots, so most of them are bright. Against a
   near-black page a full-bleed bright rectangle reads as a hole punched in
   the section, so the frame takes a small radius and a hairline to give the
   image an edge to sit against rather than ending on nothing. */
.wcase-media {
  position: relative;
  aspect-ratio: 16 / 9; overflow: hidden;
  background: var(--slate); margin-bottom: 1.6rem;
  border-radius: 8px;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .07), var(--shadow-md);
}
.wcase-media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .8s var(--ease-out), filter .5s var(--ease);
}
/* Held slightly back at rest and restored on hover, so a row of screenshots
   sits in the page rather than competing with the copy beside it. The card
   the reader is actually looking at comes up to full strength. */
:root:not([data-theme="light"]) .wcase-media img { filter: brightness(.9) saturate(.95); }
:root:not([data-theme="light"]) .wcase:hover .wcase-media img { filter: none; }
.wcase:hover .wcase-media img { transform: scale(1.04); }

/* ---------- case cover: hover reveals a glass CTA --------------------
   The whole cover is the link. At rest it is just the screenshot; on hover
   or keyboard focus a frosted pill rises over it, so the affordance is
   explicit rather than relying on the reader guessing the image is
   clickable. The .case-shot-cta is aria-hidden because the <a> already
   carries a full accessible name — announcing both would read the
   destination twice. */
.case-shot {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 8px;
  /* The work page places the cover directly in the prose column, where it
     needs its own top margin; on the homepage .wcase-media supplies the
     frame and this is a plain wrapper. */
  isolation: isolate;
}
.prose-grid .case-shot { margin-top: 1.8rem; box-shadow: var(--shadow-md); }
.prose-grid .case-shot img { border-radius: inherit; }
.wcase-media .case-shot { border-radius: 0; height: 100%; }

/* Darkens the photo so a light-on-glass label stays readable whatever the
   screenshot underneath happens to be. */
.case-shot-veil {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgb(9 8 12 / .18), rgb(9 8 12 / .55));
  opacity: 0;
  transition: opacity .4s var(--ease);
  pointer-events: none;
}

/* Real glass: blur plus a 1px inner highlight along the top edge, which is
   what reads as the lit rim of a pane rather than a flat translucent chip. */
.case-shot-cta {
  position: absolute;
  inset-block-end: clamp(14px, 2vw, 22px);
  inset-inline-start: clamp(14px, 2vw, 22px);
  display: inline-flex; align-items: center; gap: 10px;
  padding: 12px 20px;
  border-radius: 100px;
  background: rgb(255 255 255 / .12);
  -webkit-backdrop-filter: blur(14px) saturate(1.6);
          backdrop-filter: blur(14px) saturate(1.6);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .45),
              inset 0 0 0 1px rgb(255 255 255 / .18),
              0 8px 24px -10px rgb(4 2 8 / .8);
  color: #FFFFFF;
  font-size: .8rem; font-weight: 600; letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
  /* --cx/--cy are written by main.js while the pointer is over the cover, and
     removed on the way out so the pill eases back to this corner. They default
     to zero, which is the resting position — with JS off, or on touch, the
     pill simply never moves and the rest of the interaction is unchanged.
     --rise is the 10px lift-in, folded into the same transform so the two do
     not fight over the property. */
  --cx: 0px; --cy: 0px; --rise: 10px;
  opacity: 0;
  transform: translate(var(--cx), calc(var(--cy) + var(--rise)));
  transition: opacity .4s var(--ease-out), transform .45s var(--ease-out);
  pointer-events: none;
}
/* Without backdrop-filter the pill would be a 12%-white wash over a photo —
   unreadable. Those browsers get a solid blue chip instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .case-shot-cta { background: var(--accent); box-shadow: var(--shadow-md); }
}
.case-shot-cta svg {
  width: 13px; height: 13px;
  transition: transform .4s var(--ease-out);
}

.case-shot:hover .case-shot-veil,
.case-shot:focus-visible .case-shot-veil { opacity: 1; }
/* Only the lift-in is cancelled; the transform itself still carries --cx/--cy
   so the pill keeps following the cursor. Setting `transform: none` here would
   throw the tracking away the moment the hover began. */
/* Only the lift-in is cancelled; the transform itself still carries --cx/--cy
   so the pill keeps following the cursor. Setting `transform: none` here would
   throw the tracking away the moment the hover began.

   The transition also shortens: at the .45s used for the entrance the pill
   trails the cursor by a visible distance and reads as laggy rather than
   attached. .18s still smooths the per-frame steps without that drag. */
.case-shot:hover .case-shot-cta,
.case-shot:focus-visible .case-shot-cta {
  opacity: 1;
  --rise: 0px;
  transition: opacity .4s var(--ease-out), transform .18s var(--ease-out);
}
.case-shot:hover .case-shot-cta svg { transform: translate(3px, -3px); }
.prose-grid .case-shot:hover img { transform: scale(1.03); }
.prose-grid .case-shot img { transition: transform .8s var(--ease-out); }

/* The arrow points the way the language reads. */
[dir="rtl"] .case-shot-cta svg { transform: scaleX(-1); }
[dir="rtl"] .case-shot:hover .case-shot-cta svg { transform: scaleX(-1) translate(3px, -3px); }

/* Touch has no hover, so the CTA would never appear and the cover would look
   inert. It rests visible there instead. */
@media (hover: none) {
  .case-shot-veil { opacity: 1; }
  .case-shot-cta  { opacity: 1; transform: none; }
}

/* Compact card: cover, then a tight title and a muted line under it. The
   copy block is deliberately quieter than the section head above it. */
.wcase-copy h3 {
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: -0.01em;
  margin-bottom: .45rem;
}
.wcase-copy > p {
  color: var(--ink-soft);
  font-size: 0.9rem;
  line-height: 1.55;
  max-width: 64ch;
  margin-bottom: 0;
}

/* Nothing to pin against once the columns stack. */
@media (max-width: 900px) {
  .work-grid { grid-template-columns: 1fr; }
  .work-head { position: static; margin-bottom: 8px; }
}

/* --- work cases: each pins and the next scrolls up to cover it -------
   Sticky only; the page scrolls natively throughout. Cards must be opaque
   or the one beneath shows through, and each needs a higher stacking order
   than the card it covers. */
@media (prefers-reduced-motion: no-preference) and (min-width: 901px) {
  .work-cases { display: flow-root; }
  .wcase {
    position: sticky;
    inset-block-start: calc(76px + clamp(24px, 4vw, 64px));
    background: var(--ground);
    padding-block: clamp(24px, 2.6vw, 40px);
  }
  .wcase:nth-child(1) { z-index: 1; }
  .wcase:nth-child(2) { z-index: 2; }
  .wcase:nth-child(3) { z-index: 3; }
  /* Nothing follows the last card to cover it, so pinning would just hold it
     on screen while the page scrolled past underneath. */
  .wcase:nth-child(4) { z-index: 4; position: relative; inset-block-start: auto; }
  /* The cover runs out to the viewport edge. A taller card leaves less room
     to pin against, so the offset tightens and the copy block closes up to
     keep each card inside the pin window. */
  /* The cover bleeds off the trailing edge here, so the radius and the inset
     hairline are dropped on that side — a rounded corner hanging past the
     viewport reads as a rendering fault. */
  .wcase-media {
    aspect-ratio: 16 / 9;
    max-width: none;
    margin-inline-end: calc(var(--gut) * -1);
    margin-bottom: 1.15rem;
    border-start-end-radius: 0;
    border-end-end-radius: 0;
  }
  .wcase { inset-block-start: calc(76px + 14px); padding-block: 16px; }
  .wcase-copy > p { margin-bottom: .8rem; }
}


/* --- footer columns ------------------------------------------------- */

.footer-col nav { display: flex; flex-direction: column; gap: 11px; }
.footer-contact {
  font-style: normal;
  display: flex; flex-direction: column; gap: 9px;
  margin-bottom: 1.6rem;
}
.footer-contact a, .footer-contact span {
  font-size: 0.94rem; color: var(--ink-soft);
  transition: color .25s var(--ease);
}
.footer-contact a:hover, .footer-contact a:focus-visible { color: var(--accent-ink); }
.footer-bottom a { color: var(--ink-faint); transition: color .25s var(--ease); }
.footer-bottom a:hover { color: var(--accent-ink); }

/* --- work index: fills the pinned column --------------------------- */

.work-index {
  display: flex; flex-direction: column;
  width: 100%;
  margin: 0 0 2.2rem;
  border-top: 1px solid var(--rule);
}
.work-index li { border-bottom: 1px solid var(--rule); }
.work-index button {
  display: flex; align-items: baseline;
  width: 100%; padding: 15px 0;
  background: none; border: 0; cursor: pointer;
  text-align: start;
  /* Set as a label rather than a title now the numbers are gone: uppercase
     with the tracking opened up, which is how every other small label on the
     site is set. Stepped down slightly because caps read larger than
     lowercase at a matched size.

     The casing is scoped to :not([dir="rtl"] *) rather than left to the RTL
     reset above: this rule sits later in the file at equal specificity, so it
     would otherwise win and put uppercase and open tracking back onto Arabic,
     where caps do not exist and the tracking pulls the joins apart. */
  font-size: 0.86rem; font-weight: 500;
  color: var(--ink-faint);
  transition: color .3s var(--ease), padding-inline-start .3s var(--ease-out);
}
.work-index button:not([dir="rtl"] *) {
  text-transform: uppercase;
  letter-spacing: .08em;
}
.work-index button:hover { color: var(--ink); padding-inline-start: 8px; }
/* The entry matching the case on screen. */
.work-index li[data-current="true"] button { color: var(--ink); }
@media (max-width: 900px) { .work-index { display: none; } }

/* --- contact: details plain, form as the card ---------------------- */

/* The details sit on the band; only the form is lifted onto paper, so the
   card marks the thing you act on rather than the whole section. */
.contact-card {
  background: var(--ground);
  padding: clamp(26px, 3.2vw, 48px);
  border: 1px solid var(--rule);
  border-radius: 4px;
}

/* --- process: a numeral anchors each phase ------------------------- */




/* --- hero character texture ---------------------------------------- */

.hero-noise {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  z-index: 0;
  pointer-events: none;
  /* The field dissolves at both ends instead of stopping on the section
     boundary, so the texture reads as carrying into the next section. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 55%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 55%, transparent 100%);
}
/* The copy sits above the texture. */
.hero-body { position: relative; z-index: 1; }


/* --- hero headline responds to the cursor -------------------------- */

/* A small lift only. The headline is the page's anchor, so it acknowledges
   the pointer without becoming a toy. */
.hero h1 {
  transition: transform .5s var(--ease-out);
  will-change: transform;
}

/* One column once there is no room to pair. */
@media (max-width: 620px) {
  .contact-card { grid-template-columns: 1fr; }
}

/* The work page names the client above each case; the homepage cards do not.
   Kept quiet so it reads as an attribution, not a heading. */
.case-client {
  font-family: var(--mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The live URL under each case. It is evidence rather than a call to action,
   so it stays the size of the mono attribution above it and only picks up the
   accent on hover — the page's own CTA keeps the loud treatment. */
.case-link {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--ink-faint);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
  transition: color .25s, border-color .25s;
}
.case-link svg { width: .75em; height: .75em; }
.case-link:hover,
.case-link:focus-visible { color: var(--accent-ink); }

/* On the homepage the link sits below the case copy, which has no bottom
   margin of its own. */
.wcase-copy .case-link { margin-top: 1rem; }
