/* === Fonts === */
/* IBM Plex Sans - self-hosted woff2 (upright Latin variable cut) */
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-stretch: 100%;
    font-display: swap;
    src: url('/static/fonts/ibm-plex-sans-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Cairo';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('/static/fonts/cairo-arabic.woff2') format('woff2');
    unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
@font-face {
    font-family: 'Cairo';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('/static/fonts/cairo-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* === Reset === */
*, *::before, *::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Visually hidden (clip-based, direction-safe - unlike left:-9999px which
   widens the scrollable area in RTL layouts) */
.honeypot {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* === Design Tokens === */
:root {
    /* Surface */
    --bg:          #FAFAFA;
    --bg-elevated: #FFFFFF;
    --border:      rgba(0, 0, 0, 0.12);

    /* Foreground */
    --fg:          #111;
    --fg-muted:    #555;
    --fg-subtle:   #888;

    /* Brand. The palette is the two colours the site is built from plus the RGB
       triples its translucent surfaces are mixed from. */
    --purple:       #600080;
    --purple-deep:  #3d0055;
    --purple-rgb:   96, 0, 128;
    --navy-rgb:     0, 0, 128;
    --teal:         #006080;
    --teal-deep:    #004d66;
    --teal-light:   #0088b3;
    --teal-rgb:     0, 96, 128;

    /* Hero tilt - the one angle in the site */
    --slant:      -11.35deg;
    --slant-back: calc(-1 * var(--slant));

    /* Typographic scale.
       Fluid between a phone and a comfortable desktop, and it stops there: the
       content column is capped at 48rem, so type that kept growing with the
       viewport would only be widening the measure it is read in. The floors are
       the phone sizes; the ceilings are reached around 1100px, which is where the
       column has finished growing as well. */
    --text-hero:    clamp(2.5rem, 8vw + 0.5rem, 6rem);
    /* A section's own heading. A step above the panel headings below it, because
       the outline says so: the practice slide's title is its `h2`, and a service's
       name inside that section is an `h3`. Until now the `h2` took the label size
       and read smaller than the names it introduced.
       Same slope as the panel heading, a higher floor and a higher ceiling, so the
       two are the same shape and the section's is above at every width - the two
       used to cross in the middle of the range when they had different slopes. */
    --text-section: clamp(1.4rem, 1.9vw + 0.7rem, 2.35rem);
    /* Panel headings: the contact panel's heading and a service's name on the
       practice slide both take this, so the two slides cannot drift apart. It
       used to top out at 2.5rem, which made the contact heading the loudest thing
       on the page after the hero - the size the user called too big. The hero
       keeps the display size to itself. */
    --text-heading: clamp(1.3rem, 1.9vw + 0.6rem, 2.1rem);
    --text-subhead: clamp(1rem, 1.35vw + 0.52rem, 1.45rem);
    --text-body:    clamp(0.95rem, 0.5vw + 0.83rem, 1.15rem);
    /* Small print and the chrome's own labels. It used to stop at 0.8rem, which
       left the catalogue's tabs and the overview's lines of copy at 12.8px on a
       desktop while a phone got 11.5 of the same - a scale that barely moved, and
       the sizes the user read as "not responsive". */
    --text-label:   clamp(0.72rem, 0.45vw + 0.6rem, 0.9rem);

    /* Primary button sizes. `.cta` is the regular one; the top bar's language
       control is the small variant. The menu that control opens is not a second
       size: a compact dropdown is one size top to bottom, which is also why the
       trigger and its rows end up the same height (both take the same `0.25rem`
       block padding).
       The padding is split into its axes because a component that lines itself up
       with the button needs the inline half on its own, not the shorthand. */
    --btn-font-md:  0.85rem;
    --btn-font-sm:  0.675rem;
    --btn-pad-md-block:  0.5rem;
    --btn-pad-md-inline: 1.5rem;
    --btn-pad-sm-block:  0.25rem;
    --btn-pad-sm-inline: 0.75rem;

    /* Spacing scale */
    --space-section: clamp(5rem, 10vh, 14rem);
    --space-inline:  clamp(1rem, 3vw, 3rem);

    /* Content width - shared across all sections and nav. It stops at the md
       breakpoint on purpose: a 48rem column is a designed page, and nothing on
       the site needs a wider measure than that. Slides are laid out inside this
       measure too - widening them was wrong ("why did you enlarge it? Didn't we
       say md is the last breakpoint?"), so a slide that felt crowded is fixed by
       its own layout, not by a wider container. */
    --content-width: 48rem;

    /* Typography */
    --font-heading: 'IBM Plex Sans', system-ui, sans-serif;
    --font-body:    'IBM Plex Sans', system-ui, sans-serif;

    /* Duration scale */
    --duration-150: 0.15s;
    --duration-200: 0.2s;
    --duration-300: 0.3s;
    --duration-400: 0.4s;
    --duration-500: 0.5s;
    --duration-700: 0.7s;
    --duration-800: 0.8s;
    --duration-900: 0.9s;

    /* The colour-pool drift on the closing and practice slides. Deliberately off
       the scale above: that scale describes interface feedback, where anything
       past a second feels broken. Ambient background motion is the opposite, and
       a slow half-cycle is what keeps it from reading as a shimmer. */
    --duration-pool: 38s;

    /* Easing */
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-out-back: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-slide:    cubic-bezier(0.86, 0, 0.07, 1);

    /* Semantic */
    --error:    #dc2626;

    /* Dark surface */
    --bg-dark:          #0e0b14;
    --bg-dark-elevated: #1a1620;
    --fg-dark:          #f0ecf4;
    --fg-dark-m:        rgba(240, 236, 244, 0.55);

    /* Border-radius scale */
    --radius-sm:   3px;
    --radius-md:   6px;
    --radius-lg:   12px;
    --radius-full: 9999px;
    /* The radius the primary button has always used (`.cta`), named so the
       language control can share it instead of copying the number. */
    --radius-btn:  5px;

    /* Elevation / box-shadow - tone-based, matching surface hierarchy. The two the
       site uses, one of them for a dark surface. */
    --shadow-3: 0 4px 16px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04);
    --shadow-dark-1: 0 1px 2px rgba(0,0,0,0.2), 0 1px 4px rgba(0,0,0,0.15);

    /* Breakpoints - as a comment, not as tokens, because `@media` does not accept
       `var()`: a custom property named `--bp-md` would look configurable and do
       nothing. The three the site has are 56rem (the bar collapses into the menu
       sheet), 48rem (the catalogue and the contact panel stack) and 32rem (the
       phone pass); the content width above stops at the middle one. */

    /* The one z-index the site names: the language card, which has to sit above
       the bar's own content. Everything else that stacks uses the literal in its
       own rule, where the reason for the number is written beside it. */
    --z-dropdown: 100;

    /* Opacity for the two tinted states the site uses: a hover wash and a focus
       ring. */
    --opacity-hover:    0.08;
    --opacity-focus:    0.12;
}

/* Arabic: Cairo font - landing page only, tilted by the hero styles */
[lang="ar"] .page-landing {
    --font-heading: 'Cairo', 'Segoe UI', sans-serif;
    --font-body:    'Cairo', 'Segoe UI', sans-serif;
}

/* ============================================================
   HERO TILT
   The hero is the only surface that leans: its tagline and its
   buttons shear by --slant. Every other section, the top bar, and
   their items are upright.
   ============================================================ */
.hero-tagline,
.hero-buttons .cta {
    transform: skewX(var(--slant));
}

/* === Base === */
html {
    color-scheme: light;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    font-family: var(--font-body);
    font-size: var(--text-body);
    font-style: normal;
    line-height: 1.65;
    overflow: hidden;
    block-size: 100vh;
    block-size: 100dvh;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.page-landing {
    font-style: normal;
}

/* === Utilities === */
/* Read by assistive tech, invisible on screen. The clip path is what does the
   work - not `display: none` and not `visibility: hidden`, both of which would
   take the text out of the accessibility tree as well. */
.visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* === Focus === */
/* The global :focus-visible at the bottom of the file gives links/buttons a
   purple ring by default. Dark surfaces (nav over dark slides, the expertise
   slide) switch to the teal accent. Cards and tabs on the dark slides set
   their own themed ring; text inputs keep their border highlight instead. */
.site-nav.on-dark a:focus-visible,
.site-nav.on-dark button:focus-visible {
    outline-color: var(--teal-light);
}

/* === Layout === */
main {
    transition: transform var(--duration-900) var(--ease-slide);
}
/* Promote `main` to its own compositor layer only while a slide is actually
   moving. At rest the element is several viewports tall, and holding a layer
   that size in GPU memory on a phone is wasteful. The carousel toggles this
   class for the 900ms of each transition. */
main.is-sliding {
    will-change: transform;
}

.section {
    padding-block: var(--space-section);
}

/* === Parallax Slides === */
.slide {
    block-size: 100vh;
    block-size: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    overflow: hidden;
}

.slide-hero {
    text-align: center;
    overflow: visible;
}

.slide-404 .hero-content { text-align: center; }
.slide-404 .hero-name { font-size: var(--text-hero); font-weight: 700; color: var(--purple); }

/* The dark slide. The landing page has one today, but the class is what the
   carousel reads to switch the top bar to its dark palette (see main.js), so any
   dark slide carries it. */
.slide-dark {
    background: var(--bg-dark);
    color: var(--fg-dark);
}

/* --- Closing slide (contact + footer) --- */
.slide-closing {
    flex-direction: column;
    background: var(--bg);
    color: var(--fg);
    overflow: hidden;
}

/* Two soft pools of brand colour instead of the wireframe the slide used to
   carry: cheap, and it reads at every viewport. They drift, slowly enough that
   the movement is felt rather than watched - this is where someone reads an
   address and types a message, so nothing here may pull the eye. */
.slide-closing::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        radial-gradient(ellipse 55% 45% at 22% 34%, rgba(var(--purple-rgb), 0.07), transparent 70%),
        radial-gradient(ellipse 45% 55% at 86% 70%, rgba(var(--teal-rgb), 0.05), transparent 70%);
    animation: pool-drift var(--duration-pool) ease-in-out infinite alternate;
}

/* The contact slide is two panels: who to write to on the one side, the form on
   the other. */
/* Copy and form on one line, both of them on the viewport's middle line: this is a
   grid so the pair can do the two things at once. `align-content: center` centres
   the row - whose height is the form's, the taller of the two - inside the body,
   and `align-items: start` pins the copy to the top of that row, which is the
   form's own top edge. A flex row could only hold one of those at a time: centred
   items drifted apart, top-aligned items hung from the top of a box the height of
   the slide. */
.closing-body {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    align-content: center;
    align-items: start;
    /* Tighter than it was: at 6vw the gutter ate most of the md container and
       squeezed both columns - the heading wrapped into three lines beside a
       form whose fields were narrower than the 30rem they ask for. */
    column-gap: clamp(2rem, 4vw, 4.5rem);
    flex: 1;
    max-inline-size: var(--content-width);
    margin-inline: auto;
    inline-size: 100%;
    padding-inline: var(--space-inline);
}

.closing-panel {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    text-align: start;
    max-inline-size: 32rem;
    min-inline-size: 0;
}

/* The contact block opens with a heading, not an eyebrow: it is where the contact
   copy is read from and it names what the studio does and for whom. Brand purple,
   like the address below it and the button beside it - the slide's only two other
   marks of colour - rather than the near-black body colour. */
.closing-title {
    font-family: var(--font-heading);
    font-size: var(--text-heading);
    font-weight: 600;
    line-height: 1.15;
    letter-spacing: -0.01em;
    color: var(--purple);
    max-inline-size: 20ch;
    margin-block-end: clamp(0.6rem, 1.6vh, 1rem);
}

/* The positioning line under it: the market the studio works in, in the words a
   search engine reads on the page it is already crawling. */
.closing-lead {
    font-size: var(--text-body);
    font-weight: 300;
    line-height: 1.6;
    color: var(--fg-muted);
    max-inline-size: 36ch;
    margin-block-end: clamp(1.25rem, 3.5vh, 2.25rem);
}

.closing-email {
    display: inline-block;
    align-self: flex-start;
    font-family: var(--font-heading);
    /* The address is the slide's action, so it is a step up from the body - not
       the display size it carried, which read as a second heading. */
    font-size: var(--text-subhead);
    font-weight: 600;
    color: var(--purple);
    text-decoration: none;
    /* No rule under it. At this size and weight the colour already marks it as the
       action, and the underline made a mail link look like a form field. */
    transition: color var(--duration-200) var(--ease-out-expo);
}

.closing-email:hover {
    color: var(--purple-deep);
}

/* Until the script has built the address there is nothing to show, and an empty
   underlined link would be worse than no link. */
.closing-email:empty {
    display: none;
}

/* Where else to find the studio. Text, not icons: the row renders only when the
   settings carry at least one profile, so an unconfigured site shows nothing
   rather than a dead circle, and a name is legible at this size where a 16px
   brand glyph is a smudge. */
.closing-social {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-block-start: clamp(0.9rem, 2.2vh, 1.5rem);
}

.closing-social a {
    display: inline-flex;
    align-items: center;
    padding: 0.4rem 0.8rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    color: var(--fg-muted);
    font-size: var(--text-label);
    text-decoration: none;
    transition: color var(--duration-200) var(--ease-out-expo),
                border-color var(--duration-200) var(--ease-out-expo),
                background var(--duration-200) var(--ease-out-expo);
}

.closing-social a:hover {
    color: var(--purple);
    border-color: var(--purple);
    background: rgba(var(--purple-rgb), var(--opacity-hover));
}

/* The form is not a card. It used to sit in a white box on a near-white slide,
   which is what the user did not like about it: the fields already carry their own
   surface, so the form is just the stack. */
.closing-form {
    inline-size: min(100%, 26rem);
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0;
}

.slide-closing .cta {
    background: var(--purple);
    box-shadow: none;
    color: #fff;
    font-style: normal;
}

.slide-closing .cta:hover {
    background: var(--purple-deep);
}

/* Footer. One dark block, the same on every page: it is the landing page's last
   slide and the closing strip of a document. The mark is a render, drawn in the
   slide's teal and set as the tile's background. It used to be the purple render
   cut to shape by a CSS mask; a second file is one fewer thing that can render
   nothing at all, which is what a mask does when its image fails to load. */
.site-footer {
    position: relative;
    z-index: 1;
    background: var(--bg-dark);
    color: var(--fg-dark);
    padding-block: clamp(2rem, 5vh, 3.25rem) clamp(1rem, 3vh, 1.6rem);
    padding-inline: var(--space-inline);
}

.site-footer-inner {
    display: grid;
    gap: clamp(1.25rem, 3vh, 2rem);
    max-inline-size: var(--content-width);
    margin-inline: auto;
}

.site-footer-top {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: clamp(1.25rem, 4vw, 3rem);
}

.site-footer-brand {
    display: grid;
    gap: clamp(0.75rem, 2vh, 1.1rem);
    max-inline-size: 30ch;
}

.site-footer-logo {
    display: block;
    inline-size: clamp(8rem, 22vw, 11rem);
    block-size: 2.5rem;
    /* The render is wider than the tile it sits in, so it is fitted rather than
       stretched, and kept at the left edge the way the old masked background was. */
    object-fit: contain;
    object-position: left center;
}

/* The two pieces of prose in the footer, in their intended order: the studio's own
   line is the brightest thing here, and the column headings sit back with the links
   they head. It was the other way round - the tagline at the muted colour and the
   headings on the full one - which made "Our expertise" and "Information" the
   loudest words in the block and the positioning line the quietest (measured at
   1536: tagline 23.2px/400 at 55%, headings 14.4px/600 at full). The headings keep
   their form - uppercase, 600 - so they are still read as headings. */
.site-footer-text {
    font-size: var(--text-label);
    line-height: 1.7;
    color: var(--fg-dark);
}

.site-footer-cols {
    display: flex;
    flex-wrap: wrap;
    gap: clamp(1.5rem, 5vw, 3.5rem);
}

.site-footer-col {
    display: grid;
    gap: 0.45rem;
    align-content: start;
}

.site-footer-h {
    font-family: var(--font-heading);
    font-size: var(--text-label);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--fg-dark-m);
    margin-block-end: 0.1rem;
}

.site-footer-col a {
    font-size: var(--text-label);
    color: var(--fg-dark-m);
    text-decoration: none;
    transition: color var(--duration-200) var(--ease-out-expo);
}

.site-footer-col a:hover {
    color: var(--teal-light);
}

.site-footer-bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.7rem 1.1rem;
    padding-block-start: clamp(0.9rem, 2vh, 1.25rem);
    border-block-start: 1px solid rgba(255, 255, 255, 0.1);
    font-size: var(--text-label);
    color: var(--fg-dark-m);
}

.site-footer-bottom a {
    color: var(--fg-dark-m);
    text-decoration: none;
}

.site-footer-bottom a:hover {
    color: var(--teal-light);
}

.site-footer-sep {
    inline-size: 1px;
    block-size: 1em;
    background: rgba(255, 255, 255, 0.18);
}

/* The language control sits at the end of the bottom row. */
.site-footer-bottom .lang-switcher {
    margin-inline-start: auto;
}

/* The footer's copy of the control is the one place a card cannot open downward: it
   sits at the very end of a document, and on the landing at the end of the last
   slide, so the card would run past the last thing on the page and land below the
   fold (measured: 91px past the end of the privacy page at 390 wide). It opens
   upward out of its own inline end instead. The menu sheet's copy is the same case
   and says the same thing in the phones block. */
.site-footer-bottom .lang-menu {
    inset-block-start: auto;
    inset-block-end: calc(100% + var(--lang-beat-block));
    transform: translateY(var(--lang-beat-block)) scale(0.98);
    transform-origin: 100% 100%;
}

[dir="rtl"] .site-footer-bottom .lang-menu {
    transform-origin: 0 100%;
}

/* On the landing page the footer is a slide of its own, so it is laid out as one:
   the block fills the height, its rhythm is measured in viewport units, and the
   mark and the copy scale up to match. The same component on a document stays the
   compact strip it was - a slide and a page's end are not the same thing. */
.slide-footer {
    flex-direction: column;
    justify-content: center;
}

/* The footer wears the practice slide's dot field, with the one motion a grid can
   carry that a drifting pool cannot: the field turns. The layer is oversized and
   its mask is circular, so the fade is rotation-invariant and no corner ever
   enters the frame while it spins. */
.slide-footer::before {
    content: '';
    position: absolute;
    inset: -25%;
    pointer-events: none;
    background-image: radial-gradient(rgba(var(--teal-rgb), 0.16) 1px, transparent 1px);
    background-size: 30px 30px;
    mask-image: radial-gradient(circle at 50% 50%, #000 26%, transparent 68%);
    -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 26%, transparent 68%);
    animation: grid-turn 180s linear infinite;
}

/* And the same two pools the practice slide carries, in the same colours, so the
   two dark slides read as one another. */
.slide-footer::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        radial-gradient(ellipse 52% 46% at 16% 30%, rgba(var(--teal-rgb), 0.28), transparent 68%),
        radial-gradient(ellipse 44% 50% at 84% 72%, rgba(var(--purple-rgb), 0.22), transparent 70%);
    animation: pool-drift var(--duration-pool) ease-in-out infinite alternate;
}

.slide-footer .site-footer {
    flex: 1;
    display: flex;
    align-items: center;
    background: transparent;
    inline-size: 100%;
}

.slide-footer .site-footer-inner {
    inline-size: 100%;
    max-inline-size: var(--content-width);
    gap: clamp(2.5rem, 10vh, 6rem);
}

.slide-footer .site-footer-top {
    gap: clamp(2rem, 8vw, 6rem);
}

.slide-footer .site-footer-logo {
    inline-size: clamp(10rem, 24vw, 15rem);
    block-size: 3.4rem;
}

.slide-footer .site-footer-text {
    font-size: var(--text-subhead);
    line-height: 1.5;
    max-inline-size: 34ch;
}

/* A footer link is a navigation target, not a link inside a sentence, so it takes
   the 24px minimum the target size rule asks for: at the body size the line box is
   about 19px, which is under it. */
.site-footer-col a,
.site-footer-bottom a {
    display: inline-flex;
    align-items: center;
    min-block-size: 1.5rem;
}

.slide-footer .site-footer-col a {
    font-size: var(--text-body);
}

.slide-footer .site-footer-bottom {
    font-size: var(--text-label);
}

/* Small devices stack the footer, so it reads down its middle instead of being
   left-aligned in a column that no longer exists beside it. The bottom row centres
   with it, which is also what puts the language control under the copyright rather
   than off at the end of a line. */
@media (max-width: 48rem) {
    .site-footer-top {
        justify-content: center;
        text-align: center;
    }

    .site-footer-brand {
        justify-items: center;
        max-inline-size: none;
    }

    .site-footer-logo {
        object-position: center;
    }

    .site-footer-cols {
        justify-content: center;
    }

    .site-footer-col {
        justify-items: center;
    }

    .site-footer-bottom {
        justify-content: center;
        text-align: center;
    }

    .site-footer-bottom .lang-switcher {
        margin-inline-start: 0;
    }
}

/* === Site Navigation === */
.site-nav {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: 100;
    background: transparent;
    pointer-events: none;
    transition: background var(--duration-300) var(--ease-out-expo), box-shadow var(--duration-300) var(--ease-out-expo);
}

/* Top-edge gradient - depth indicator in all states */
/* ::before = dark gradient (hidden by default), ::after = light gradient (visible by default).
   Cross-fade between the two via opacity so the transition is smooth.               */
.site-nav::before {
    content: '';
    position: absolute;
    inset: 0;
    block-size: 100%;
    background: linear-gradient(to bottom,
        rgba(0, 0, 0, 0.4) 0%,
        rgba(0, 0, 0, 0.12) 50%,
        transparent 100%);
    pointer-events: none;
    z-index: -1;
    opacity: 0;
    transition: opacity var(--duration-500) var(--ease-out-expo);
}
.site-nav::after {
    content: '';
    position: absolute;
    inset: 0;
    block-size: 100%;
    background: linear-gradient(to bottom,
        rgba(0, 0, 0, 0.08) 0%,
        rgba(0, 0, 0, 0.03) 50%,
        transparent 100%);
    pointer-events: none;
    z-index: -1;
    opacity: 1;
    transition: opacity var(--duration-500) var(--ease-out-expo);
}

/* Dark slides: show dark gradient, hide light one */
.site-nav.on-dark::before {
    opacity: 1;
}
.site-nav.on-dark::after {
    opacity: 0;
}

.site-nav.on-dark .nav-link { color: var(--fg-dark-m); }
.site-nav.on-dark .nav-link:hover { color: var(--teal-light); }
.site-nav.on-dark .nav-link.active { color: var(--teal-light); }

/* The bar's mark above is now a background image, so the dark palette swaps the
   file instead of the colour. */

/* The hamburger is the bar's only control that had no dark-palette rule, so on the
   practice and footer slides it stayed near-black on a near-black surface. Bright
   rather than muted, because a glyph at this size needs more contrast than a link's
   label does, and teal on hover to match the links beside it. */
.site-nav.on-dark .mobile-nav-trigger {
    color: var(--fg-dark);
}

.site-nav.on-dark .mobile-nav-trigger:hover {
    color: var(--teal-light);
}

/* The palette flip is driven from JS in the middle of a slide change, so the bar's
   text has to fade on the same half-second as the surface underneath it - at its
   resting duration the words land first and the background catches up, which reads
   as a snap. Scoped to the slide's own 900ms, so hover keeps the shorter change it
   has at rest. */
body.is-sliding .site-nav .nav-link,
body.is-sliding .site-nav .nav-brand,
body.is-sliding .site-nav .lang-btn,
body.is-sliding .site-nav .nav-mark {
    transition-duration: var(--duration-500);
}

.nav-inner {
    position: relative;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    max-inline-size: var(--content-width);
    margin-inline: auto;
    padding: clamp(1rem, 2.2vh, 1.4rem) var(--space-inline);
    pointer-events: auto;
}

.nav-left {
    min-inline-size: 0;
    grid-column: 1;
    justify-self: start;
}

/* The brand emblem is drawn slanted, so it renders as-is without an extra
   skew. It is shown on every page and every slide - except the hero, which
   carries the same mark at hero size itself (see `.site-nav.on-hero` below). */
.nav-brand {
    display: block;
    position: relative;
    text-decoration: none;
    white-space: nowrap;
    /* The mark is a render (see the Brand logo section of the README). It is an
       `<img>` in the markup rather than a background here, because that is what lets
       `asset()` content-hash the file's URL: a background an editor swaps is served
       from a browser cache for an hour, an `img` with a new hash is fetched at once.
       The box is the emblem's own aspect ratio, so the image fills it instead of
       being letterboxed. */
    block-size: clamp(1.9rem, 3vw, 2.5rem);
    aspect-ratio: 393 / 329;
    /* Opacity is the hero, which hides the bar's mark because it shows the same one
       at hero size. Without it in the list the mark popped in and out at the hero
       boundary - the loudest thing the bar does between slides. */
    transition: opacity var(--duration-300) var(--ease-out-expo);
    opacity: 1;
    pointer-events: auto;
}

/* The palette flip, as a cross-fade: two stacked images, one colourway each, and
   their opacity cross-over is the fade. A single image swapping its source, or a
   `background-image` changing, does not interpolate - it snaps, which is the one
   thing in the bar that would not follow the surface. */
.nav-brand .nav-mark {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
    transition: opacity var(--duration-300) var(--ease-out-expo);
}

.nav-mark-light {
    opacity: 1;
}

.nav-mark-dark {
    opacity: 0;
}

/* The dark slides take the second colourway. */
.site-nav.on-dark .nav-mark-light {
    opacity: 0;
}

.site-nav.on-dark .nav-mark-dark {
    opacity: 1;
}

/* The bar carries the emblem alone: the hero shows the full lockup a few
   centimetres above, so the wordmark never appears here. That wordmark is why the
   bar's render is the emblem's own file and not the lockup's. */

/* The close button, the language bar and the sheet's own parts - its head, its
   numbering, the row arrows and the foot that carries the way out - live in the
   mobile overlay menu only, so out here they are not rendered at all. (They used
   to be: a sheet-only element that is only *styled* inside the phones block still
   exists at every other width, and the sheet's title and a second Contact button
   turned up in the middle of the desktop bar. The whole foot is hidden rather than
   the button inside it, which is what keeps that second Contact button out of the
   bar without a rule written just to defeat `.cta`'s own `display`.) */
.nav-close,
.nav-sheet-head,
.nav-sheet-foot,
/* The scrim is a box out here, and that was a real bug: `.nav-inner` is a grid, and
   a grid child takes a cell whether or not it paints anything. The scrim's own
   `display: none` was written inside the phones block, so above 56rem it was a plain
   div occupying column 3 - which pushed `.nav-right` onto a second grid row and made
   the bar 94px tall with the Contact button and the language control sitting *below*
   the centre row of nav links. That is the user's "I can see the contact button,
   language button below the menu", where the menu is that row. The phones block still
   turns it on when the sheet is open. */
.nav-scrim {
    display: none;
}

/* And the links' wrapper is not a box out here: `display: contents` hands the
   four links back to the bar as its own flex items, spaced by the bar's row, which
   is what they were before the menu needed something to wrap them in. */
.nav-sheet-links {
    display: contents;
}

/* The bar is one component and carries the same things everywhere: brand, the
   same links, the same controls. The one exception is the hero, which shows the
   mark itself at hero size - repeating it a few centimetres above would be a
   duplicate, so the bar's copy steps aside on that slide and only that slide. */
.site-nav.on-hero .nav-brand {
    opacity: 0;
    pointer-events: none;
}

.nav-center {
    display: flex;
    gap: clamp(0.2rem, 0.5vw, 0.4rem);
    justify-content: center;
    grid-column: 2;
    justify-self: center;
}

.nav-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.4rem clamp(0.5rem, 1vw, 0.75rem);
    color: var(--fg);
    text-decoration: none;
    font-family: var(--font-heading);
    font-weight: 500;
    font-size: var(--text-label);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    transition: color var(--duration-300) var(--ease-out-expo);
}

.nav-link:hover { color: var(--purple); }

.nav-link.active {
    color: var(--purple);
}

.nav-right {
    min-inline-size: 0;
    grid-column: 3;
    justify-self: end;
    /* The bar's controls, on one row: the Contact CTA and the language control. */
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The bar's call to action: the primary button, small, with a filled mark before
   the label. Like every other control in the chrome it stays put under the
   pointer: the lift and glow belong to CTAs sitting in page content, not to the
   bar. Its gap matches the language control's, so the two read as one pair. */
.nav-contact {
    gap: 0.5em;
}

.nav-contact:hover {
    translate: none;
    box-shadow: none;
}

/* === Language switcher ===
   The site's **ghost** button in its small variant: the language control is a
   secondary feature, and a filled slab in the middle of the chrome competed with
   the bar's one real call to action (the Contact button beside it). So it takes
   no fill at all: same colour as the bar's links, same hover tint, and only the
   box (padding, radius) says it is a control rather than a link.
   It opens a menu of the site's languages with the **disclosure** pattern, not a
   menu widget: a `button[aria-expanded]` followed by a plain list of links, so the
   options stay in the normal tab order and Enter follows them, Escape closes, and
   screen readers get a button that says whether it is open.
   The state lives in `aria-expanded` and nowhere else. CSS reads that attribute,
   so the button, the chevron and the card cannot disagree about what is open. The
   JS only writes it.
   It opens on click, for everyone. There is deliberately no hover reveal: hover is
   not a state a keyboard or touch user can hold, it fired while the pointer was
   only crossing the bar, and it left the chevron pointing down while the card was
   open.
   The spacing is not invented here either: the card re-uses the button's own box
   padding as its beat, so the button's inset, the card's inset and the gap between
   them are one measure that follows whatever size the button is.
   The type is one size top to bottom (`--btn-font-sm`), with no tracking, and
   everything inside the component is `em`, so the marks follow it.
   The mark is Bootstrap Icons' `translate`, inlined because this site ships its own
   icons rather than an icon font; the menu is anchored to the bar's inline end so
   it can never run off the viewport, and it lists every language in its own words
   with a check on the one being read. */
.lang-switcher {
    /* The component's two rhythms, both taken from the button's own box: the card
       insets its rows by the block measure and sits that far below the button, and
       the inline measure is what each surface insets its content by - the rows
       derive theirs so that the card's padding plus a row's always adds up to it. */
    --lang-beat-block: var(--btn-pad-sm-block);
    --lang-beat-inline: var(--btn-pad-sm-inline);

    position: relative;
    /* The component's type, said once: the trigger, the rows and every mark in
       them take it from here (`em` everywhere below). No tracking either - the
       label is uppercase and small, and letter spacing on top of that reads loose.
       Declared here rather than on each child so the two cannot drift apart. */
    font-size: var(--btn-font-sm);
    letter-spacing: normal;
}

.lang-btn {
    display: inline-flex;
    align-items: center;
    /* Proportional to the label, like the marks beside it. */
    gap: 0.5em;
    padding: var(--btn-pad-sm-block) var(--btn-pad-sm-inline);
    /* No fill: a ghost control reads as one of the bar's links. The tint is what
       hover and the open state add, and nothing else. */
    background: transparent;
    color: var(--purple);
    border: none;
    border-radius: var(--radius-btn);
    font-family: var(--font-heading);
    /* A button inherits neither the page's font size nor its line-height - the UA
       resets both - so take the component's size and say the line-height: with the
       default `normal` the control's line box is shorter than the labels beside it. */
    font-size: inherit;
    line-height: inherit;
    /* The bar's own label treatment: weight and case, without its tracking. */
    font-weight: 500;
    text-transform: uppercase;
    cursor: pointer;
    /* Two durations, because two things change here. The background is this
       control's own hover and open tint, which stays immediate. The colour is the
       bar's palette, flipped from JS as the carousel crosses a dark slide: with no
       colour transition at all it snapped, which is the instant switch the user
       saw - the mark, its label and the chevron all take their colour from here. */
    transition: color var(--duration-500) var(--ease-out-expo),
                background var(--duration-200) var(--ease-out-expo);
}

/* Hover is a tint and nothing else: a control in the chrome must not move or glow
   under the pointer before it has done anything. The open state holds a slightly
   stronger tint, so the button reads as held while its card is showing. Both tints
   come from the system's opacity steps. */
.lang-btn:hover {
    background: rgba(var(--purple-rgb), var(--opacity-hover));
}
.lang-btn[aria-expanded="true"] {
    background: rgba(var(--purple-rgb), var(--opacity-focus));
}

.lang-icon {
    /* A step larger than the label: the mark's detail needs the room, and this is
       the bar's only affordance for changing language. */
    inline-size: 1.3em;
    block-size: 1.3em;
    flex: none;
    opacity: 0.95;
}

/* In the bar the control is its mark and chevron only. The bar is the tightest
   surface on the site, and the current language is one click away in the menu -
   so the name is hidden here rather than dropped, which keeps the button reading
   "Langue : Francais" to a screen reader. Scoped to the bar's copy by its parent:
   the menu's own rows keep their names, they are what the control opens, and the
   footer and the sheet both have the room to show the name too. */
.nav-right .lang-btn .lang-name {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Hiding the label also removes its line box, and a control that is only as tall
   as its marks would sit short beside the CTA and dip under the 24px minimum
   target. So the box keeps the height the label gave it: the line-height it would
   have had at this size, plus the same block padding on both sides. */
.nav-right .lang-btn {
    min-block-size: calc(1.65em + 2 * var(--btn-pad-sm-block));
}

.lang-chevron {
    inline-size: 1em;
    block-size: 1em;
    flex: none;
    opacity: 0.9;
    transition: transform var(--duration-200) var(--ease-out-expo);
}

/* The chevron turns over while the card is open - and that is now the only state
   there is, so it can never point down at an open menu. */
.lang-btn[aria-expanded="true"] .lang-chevron {
    transform: rotate(180deg);
}

/* The card grows out of the corner it is anchored to - the inline end, which is
   the left edge of the button once the page is right to left. */
.lang-menu {
    position: absolute;
    /* One block beat below the button - the same measure as the card's own
       padding, so the two gaps read as one. */
    inset-block-start: calc(100% + var(--lang-beat-block));
    inset-inline-end: 0;
    z-index: var(--z-dropdown);
    display: flex;
    flex-direction: column;
    /* 2px, the same tight row gap `.hslider-tabs` uses. */
    gap: 2px;
    /* No minimum width: the card hugs whatever the widest row needs and stays
       anchored to the button's inline end, so it never stretches to a size the
       languages did not ask for. */
    margin: 0;
    padding: var(--lang-beat-block);
    list-style: none;
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    opacity: 0;
    /* Hidden, not merely transparent: its links must stay out of the tab order. */
    visibility: hidden;
    transform: translateY(calc(-1 * var(--lang-beat-block))) scale(0.98);
    transform-origin: 100% 0;
    transition: opacity var(--duration-150) var(--ease-out-expo),
                transform var(--duration-150) var(--ease-out-expo),
                visibility 0s linear var(--duration-150),
                /* The card's surface follows the palette too, and it can be open
                   while the carousel moves: without this its own background snapped
                   while everything inside it faded. */
                background-color var(--duration-500) var(--ease-out-expo);
    pointer-events: none;
}

[dir="rtl"] .lang-menu {
    transform-origin: 0 0;
}

/* The open state is the button's own `aria-expanded`: one attribute, written by the
   JS and read here, so the button, the chevron and the card cannot disagree. */
.lang-btn[aria-expanded="true"] + .lang-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0) scale(1);
    transition-delay: 0s;
    pointer-events: auto;
}

.lang-menu a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6em;
    /* A compact dropdown, so the rows take the component's single size - no size
       step between the trigger and the menu - and Bootstrap's `0.25rem` block
       padding, which is what the trigger takes too: the two end up the same height.
       The inline padding derives from the component's beat, so the card and the
       button inset their content by the same measure, and the gap above is `em` so
       it scales with the type rather than beside it. */
    padding: 0.25rem calc(var(--lang-beat-inline) - var(--lang-beat-block));
    border-radius: var(--radius-sm);
    color: var(--fg);
    font-family: var(--font-heading);
    font-weight: 500;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--duration-150) var(--ease-out-expo),
                color var(--duration-150) var(--ease-out-expo);
}

.lang-menu a:hover,
.lang-menu a:focus-visible {
    background: var(--purple);
    color: #fff;
}

.lang-menu a.is-current {
    background: rgba(var(--purple-rgb), 0.08);
    color: var(--purple);
    font-weight: 600;
}

.lang-check {
    /* `em`, like the marks in the button, so it follows the one size declaration
       on `.lang-switcher` instead of being pinned to a size of its own. */
    inline-size: 1em;
    block-size: 1em;
    flex: none;
}

/* On a dark surface the two controls follow that surface's links: the ghost takes
   the muted light and the teal accent, and the CTA takes the accent the slide's
   other CTAs use. Scoped to `.on-dark` rather than to the bar, because the bar is
   not the only dark thing any more (the footer is too). */
.on-dark .lang-btn {
    color: var(--fg-dark-m);
}

.on-dark .lang-btn:hover {
    color: var(--teal-light);
    background: rgba(var(--teal-rgb), var(--opacity-hover));
}

.on-dark .lang-btn[aria-expanded="true"] {
    color: var(--teal-light);
    background: rgba(var(--teal-rgb), var(--opacity-focus));
}

.on-dark .cta {
    background: var(--teal);
}

.on-dark .cta:hover {
    background: var(--teal-deep);
}

.site-nav.on-dark .lang-menu {
    background: var(--bg-dark-elevated);
    border-color: rgba(255, 255, 255, 0.1);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55), var(--shadow-dark-1);
}

.site-nav.on-dark .lang-menu a {
    color: var(--fg-dark);
}

.site-nav.on-dark .lang-menu a:hover,
.site-nav.on-dark .lang-menu a:focus-visible {
    background: var(--teal);
    color: #fff;
}

.site-nav.on-dark .lang-menu a.is-current {
    background: rgba(var(--teal-rgb), 0.22);
    color: var(--teal-light);
}

/* The menu on any dark surface (the bar over a dark slide, the footer), which is
   the same treatment in both places. */
.on-dark .lang-menu {
    background: var(--bg-dark-elevated);
    border-color: rgba(255, 255, 255, 0.1);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55), var(--shadow-dark-1);
}

.on-dark .lang-menu a {
    color: var(--fg-dark);
}

.on-dark .lang-menu a:hover,
.on-dark .lang-menu a:focus-visible {
    background: var(--teal);
    color: #fff;
}

.on-dark .lang-menu a.is-current {
    background: rgba(var(--teal-rgb), 0.22);
    color: var(--teal-light);
}

/* === Hero === */
.slide-hero::before {
    content: '';
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 80% 54% at 50% 38%, rgba(var(--purple-rgb), 0.018), transparent 64%),
        linear-gradient(to top, rgba(var(--navy-rgb), 0.028) 0%, rgba(var(--navy-rgb), 0.0) 44%);
    pointer-events: none;
}

.hero-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.hero-content {
    position: relative;
    z-index: 1;
    max-inline-size: var(--content-width);
    margin-inline: auto;
    padding-inline: var(--space-inline);
    inline-size: 100%;
}

.hero-title {
    line-height: 1;
    overflow: visible;
}

.hero-name {
    display: inline-block;
    line-height: 0.9;
    transform: skewX(var(--slant));
}

.hero-name-inner {
    display: inline-block;
    animation: hero-slide var(--duration-800) var(--ease-out-expo) both;
}

/* Logo is already slanted visually - counter-skew so it renders upright */
.hero-name .hero-logo {
    display: inline-block;
    transform: skewX(var(--slant-back));
}

.hero-logo {
    display: block;
    inline-size: min(40vw, 320px);
    block-size: auto;
    max-inline-size: 85vw;
}

.hero-buttons {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: calc(clamp(0.75rem, 2vw, 1.5rem) / 2);
    margin-block-start: clamp(1.5rem, 4vh, 3.5rem);
}
.hero-buttons .cta {
    margin-block-start: 0;
}

.hero-tagline {
    margin-block-start: clamp(0.75rem, 2vh, 1.75rem);
    font-family: var(--font-body);
    /* The hero's one line of prose, under a wordmark: it carries the whole statement
       from its size, not from its weight. Setting it at 600 made it shout and read
       as a heading, so the light face is back and the type grows instead - a floor a
       quarter above the 1rem it started at, and a ceiling of 2rem. Still a fluid
       clamp, so it is the viewport that decides where in that range it sits. */
    font-size: clamp(1.25rem, 1.6vw + 0.7rem, 2rem);
    color: var(--fg);
    font-weight: 300;
    font-style: normal;
    max-inline-size: 36ch;
    margin-inline: auto;
    line-height: 1.4;
}

.hero-tagline .hero-tagline-text {
    display: block;
    animation: fade-up var(--duration-500) var(--ease-out-back) var(--duration-300) both;
}

/* === CTA === */
.cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-block-start: clamp(1.5rem, 3vh, 3rem);
    padding: var(--btn-pad-md-block) var(--btn-pad-md-inline);
    background: var(--purple);
    color: #fff;
    font-family: var(--font-heading);
    font-weight: 500;
    font-size: var(--btn-font-md);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    text-decoration: none;
    cursor: pointer;
    border: none;
    border-radius: var(--radius-btn);
    transition: background var(--duration-200) var(--ease-out-expo),
                box-shadow var(--duration-200) var(--ease-out-expo),
                transform var(--duration-150) var(--ease-out-expo);
}

/* The primary button's small variant: the same fill and radius a size down.
   The bar's Contact button is the chrome's only filled control now that the
   language control is a ghost, so this is what carries the eye - which is why it
   drops `.cta`'s tracking and its stacking margin: in the bar it sits beside the
   language control, not above a paragraph. */
.cta-sm {
    padding: var(--btn-pad-sm-block) var(--btn-pad-sm-inline);
    font-size: var(--btn-font-sm);
    letter-spacing: normal;
    margin-block-start: 0;
}

/* A filled mark inside a button, sized like the language control's mark: a touch
   larger than the label, because a solid glyph needs the room to read this small. */
.cta-icon {
    inline-size: 1.15em;
    block-size: 1.15em;
    flex: none;
    opacity: 0.95;
}

.cta > span {
    display: inline-block;
}

.cta:hover {
    background: var(--purple-deep);
    box-shadow: 0 8px 20px -8px rgba(var(--purple-rgb), 0.5);
    /* `translate` (not `transform`) so a tilted hero button keeps its skew. */
    translate: 0 -1px;
}

.cta:active {
    translate: 0 0;
}

.cta-ghost {
    background: transparent;
    color: var(--purple);
    box-shadow: inset 0 0 0 2px var(--purple);
}

.cta-ghost:hover {
    background: var(--purple);
    color: #fff;
    box-shadow: inset 0 0 0 2px var(--purple), 0 8px 20px -8px rgba(var(--purple-rgb), 0.5);
    translate: 0 -1px;
}

/* === Practice slide (the services slider) ===
   Two columns: a numbered index of the domains down the side, the active domain
   on the right. The horizontal slider mechanics are unchanged (see main.js) - the
   index is its tab row and the overview rows are its shortcuts. */
.practice {
    position: relative;
    z-index: 1;
    display: grid;
    /* One column: a row of tabs, then the stage. Side by side the stage got half of
       the md container and a domain read in 174px columns, which is the crowding the
       user pointed at; the rail sits above it at every width instead. */
    grid-template-columns: 1fr;
    /* The tabs take what they need and the stage takes the rest of the section -
       which is the only measure that cannot be wrong: a stage sized in `vh` has no
       way to know how much the bar, the slide's own padding and the tabs took, and
       on a short phone the remainder was negative, so a domain's panel ran off the
       bottom of the stage and was clipped. `minmax(0, 1fr)` is what lets the row be
       smaller than its content instead of pushing the grid past the section. */
    grid-template-rows: auto minmax(0, 1fr);
    align-content: stretch;
    row-gap: clamp(1.25rem, 3vh, 2rem);
    max-inline-size: var(--content-width);
    margin-inline: auto;
    padding-inline: var(--space-inline);
    inline-size: 100%;
    block-size: 100%;
}

/* The slide's texture: a grid of dots, faded at the edges so it never frames the
   content. */
.slide-practice::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: radial-gradient(rgba(var(--teal-rgb), 0.18) 1px, transparent 1px);
    background-size: 30px 30px;
    mask-image: radial-gradient(ellipse 70% 62% at 50% 45%, #000, transparent 76%);
    -webkit-mask-image: radial-gradient(ellipse 70% 62% at 50% 45%, #000, transparent 76%);
}

/* The same drifting pools the contact slide carries, in this slide's own colour:
   teal for the domains, with one quiet purple counter-pool so a dark full-height
   surface is not flat. Above the dots, below the content. */
.slide-practice::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        radial-gradient(ellipse 52% 46% at 16% 30%, rgba(var(--teal-rgb), 0.34), transparent 68%),
        radial-gradient(ellipse 44% 50% at 84% 72%, rgba(var(--purple-rgb), 0.26), transparent 70%);
    animation: pool-drift var(--duration-pool) ease-in-out infinite alternate;
}

.practice-side {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem 1.25rem;
    min-inline-size: 0;
}

/* The practice slide's own heading. This is the section's `h2`, over an `h3` for
   each service inside it - and it took the label token, which made the title of
   the section 12.8px on a desktop: an eyebrow's size where a title belongs, and
   smaller than the names it introduced. It is the section heading now, uppercase
   with the tracking drawn in, because a title at this size does not need the
   0.14em an eyebrow needs to be seen. */
.practice-eyebrow {
    font-family: var(--font-heading);
    font-size: var(--text-section);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--teal-light);
}

.hslider {
    position: relative;
    inline-size: 100%;
    /* No height of its own: it is the section's leftover, set by the row it sits
       in (see `.practice`). */
    min-block-size: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.hslider-track {
    display: flex;
    flex: 1;
    transition: transform var(--duration-700) var(--ease-slide);
}
.hslider-track.is-sliding {
    will-change: transform;
}

.hslider-slide {
    min-inline-size: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

/* Slide content. The body slides in from the inline end and fades; each variant
   below sets its own alignment. */
.hslider-body {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    inline-size: 100%;
    opacity: 0;
    transform: translateX(3rem);
    transition: opacity var(--duration-500) var(--ease-out-expo), transform var(--duration-500) var(--ease-out-back);
    transition-delay: 0.12s;
}

[dir="rtl"] .hslider-body {
    transform: translateX(-3rem);
}

.hslider-slide.active .hslider-body {
    opacity: 1;
    transform: translateX(0);
}

/* --- Overview: the whole catalogue as a grid of rows --- */
.hslider-summary {
    gap: clamp(1rem, 2.5vh, 1.75rem);
}

.hslider-summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: clamp(0.6rem, 1.2vw, 1rem);
}

/* One row of the overview: the number, the icon and the service. It is the
   slider's shortcut to that domain, so it is a button. */
.hslider-card {
    position: relative;
    display: flex;
    align-items: center;
    gap: clamp(0.6rem, 1.2vw, 0.9rem);
    padding: clamp(0.8rem, 2vh, 1.1rem) clamp(0.9rem, 2vw, 1.2rem);
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(var(--teal-rgb), 0.14);
    text-align: start;
    cursor: pointer;
    overflow: hidden;
    transition: background var(--duration-200) var(--ease-out-expo),
                border-color var(--duration-200) var(--ease-out-expo);
}

/* The accent the row grows along its inline start edge while it is under the
   pointer (or focused). */
.hslider-card::before {
    content: '';
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: 2px;
    background: var(--teal-light);
    transform: scaleY(0);
    transform-origin: 50% 100%;
    transition: transform var(--duration-300) var(--ease-out-expo);
}

.hslider-card:hover,
.hslider-card:focus-visible {
    background: rgba(var(--teal-rgb), 0.1);
    border-color: rgba(var(--teal-rgb), 0.4);
}

.hslider-card:hover::before,
.hslider-card:focus-visible::before,
.hslider-card:active::before {
    transform: scaleY(1);
}

.hslider-card:active {
    background: rgba(var(--teal-rgb), 0.16);
}

.hslider-card:focus-visible {
    outline: 2px solid var(--teal-light);
    outline-offset: 2px;
}

.hslider-card-num {
    align-self: flex-start;
    font-family: var(--font-heading);
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    color: var(--teal-light);
}

/* Icon beside the text, both in the row's own grid so the icon can span the
   title and the line under it without a wrapper of its own. */
.hslider-card-inner {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    column-gap: clamp(0.6rem, 1.2vw, 0.9rem);
    min-inline-size: 0;
}

.hslider-card-icon {
    grid-row: 1 / -1;
    display: flex;
    align-items: center;
    color: var(--teal-light);
}

.hslider-card-icon svg {
    inline-size: clamp(1.5rem, 2.4vw, 2rem);
    block-size: clamp(1.5rem, 2.4vw, 2rem);
}

.hslider-card-title {
    grid-column: 2;
    font-family: var(--font-heading);
    font-weight: 600;
    font-size: clamp(0.92rem, 0.6vw + 0.72rem, 1.12rem);
    color: var(--fg-dark);
}

/* The line of copy under it takes the label token, which is the scale's small end
   and now grows with the viewport like everything else. The title's own floor is a
   step above the summary's ceiling, so the two can never swap places. */
.hslider-card-summary {
    grid-column: 2;
    font-size: var(--text-label);
    font-weight: 300;
    line-height: 1.45;
    color: var(--fg-dark-m);
}

/* --- Domain detail: the pitch on the left, what it covers on the right --- */
.hslider-domain {
    align-items: flex-start;
}

/* Two columns: the icon, the title and the pitch on one side; what the service
   covers and the actions on the other. Every item is placed, so the order they
   appear in the markup never decides the layout. The panel is named by its icon,
   its title and the tab the reader pressed to get here: the slide's number used to
   be repeated in this column and said nothing the tab had not. */
.hslider-domain-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    /* Four rows, and the bullets start on the second: the icon owns the first, so
       the list begins level with the title instead of level with the glyph above
       it. The actions take the row under the bullets. */
    grid-template-rows: auto auto auto auto;
    align-items: start;
    column-gap: clamp(1.5rem, 4vw, 3.5rem);
    row-gap: clamp(1rem, 2.5vh, 1.75rem);
    inline-size: 100%;
}

.hslider-domain-head {
    grid-column: 1;
    grid-row: 1;
    display: flex;
    align-items: flex-start;
}

.hslider-domain-icon {
    display: flex;
    align-items: center;
    color: var(--teal-light);
}

.hslider-domain-icon svg {
    inline-size: clamp(2.25rem, 3.6vw, 3rem);
    block-size: clamp(2.25rem, 3.6vw, 3rem);
}

.hslider-domain-title {
    grid-column: 1;
    grid-row: 2;
    font-family: var(--font-heading);
    /* One section scale, shared with the contact heading. */
    font-size: var(--text-heading);
    font-weight: 600;
    /* Uppercase, like the tab that switches to it and the eyebrow above them. A
       display size needs less tracking than those small labels take, so this is a
       twentieth of their 0.14em rather than the same measure. */
    text-transform: uppercase;
    letter-spacing: 0.02em;
    line-height: 1.12;
    color: var(--fg-dark);
    text-align: start;
}

.hslider-domain-title > span {
    display: inline-block;
}

.hslider-domain-summary {
    grid-column: 1;
    grid-row: 3;
    font-size: var(--text-body);
    font-weight: 300;
    color: var(--fg-dark-m);
    max-inline-size: 32ch;
}

/* The bullets read as a list of deliverables: one hairline each, and every item is
   marked with the brand's own gesture - a chip sheared by --slant, the same angle
   the hero's name and buttons take, in the accent the service's number and its icon
   above the list already use. In the panel the chip hangs in the gap between the two
   columns, not in the item's own width: these labels are written to fill their
   column, and a mark that indents them wraps the longest one - which cost the panel
   more height than the slide has at 1024x600 and below, so the content ran past the
   bottom of the section. Hung in the gap, the text keeps the measure it had before
   the mark existed. The stacked layout has no gap to hang in, so there the chip
   takes a column of its own (see the tablet block). */
.hslider-bullets {
    grid-column: 2;
    grid-row: 2 / span 2;
    list-style: none;
    display: flex;
    flex-direction: column;
    inline-size: 100%;
}

.hslider-bullets li {
    /* Named because the chip is positioned from the padding box and has to sit on
       the first line's centre, which is this measure plus half the leftover of the
       line box. */
    --bullet-pad: clamp(0.45rem, 1.2vh, 0.7rem);
    position: relative;
    padding-block: var(--bullet-pad);
    font-family: var(--font-body);
    font-size: var(--text-body);
    font-weight: 300;
    color: var(--fg-dark);
    line-height: 1.5;
    text-align: start;
}

/* A half-em chip on the first line's centre. Every measure is em: the line box is
   1.5em (the item's line-height), so half of what is left over centres the chip in
   it, and the mark follows the item's size through the fluid scale and the phone
   step down to 0.85rem without a rule of its own. The offset keeps it inside the
   column gap at its narrowest (4vw from 48rem up, so 1.2em clears it) and leaves
   0.7em of air between the chip and the first letter. */
.hslider-bullets li::before {
    content: '';
    position: absolute;
    inset-inline-start: -1.2em;
    inset-block-start: calc(var(--bullet-pad) + (1.5em - 0.5em) / 2);
    inline-size: 0.5em;
    block-size: 0.5em;
    background: var(--teal-light);
    border-radius: 1px;
    transform: skewX(var(--slant));
}

/* Rules between the items and nowhere else: a border on every item drew a lid over
   the first line and, with the last-child rule, a floor under the last. */
.hslider-bullets li + li {
    border-block-start: 1px solid rgba(255, 255, 255, 0.08);
}

.hslider-domain-actions {
    grid-column: 2;
    grid-row: 4;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: clamp(0.85rem, 2vw, 1.5rem);
}

/* The domain's contact CTA. Teal, to match the dark slide (purple is the brand
   CTA on the light surfaces), and compact so it stays in scale with the slide. */
.hslider-domain-actions .cta {
    margin-block-start: 0;
    background: var(--teal);
    padding: 0.5rem 1.35rem;
    /* The system's small button size, not a fourth one measured by hand. */
    font-size: var(--btn-font-sm);
}

.hslider-domain-actions .cta:hover {
    background: var(--teal-deep);
}

/* Empty state shown before content is filled in (no seeded data). */
.hslider-summary .empty-state {
    font-family: var(--font-heading);
    font-size: var(--text-label);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--fg-dark-m);
    text-align: center;
    margin-block-start: 1.5rem;
}

/* The index: a row of numbered tabs above the stage, spanning it. */
.hslider-tabs {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px;
    inline-size: 100%;
}

.hslider-tab {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.35rem 0.7rem;
    background: none;
    border: none;
    border-block-end: 2px solid rgba(255, 255, 255, 0.12);
    color: var(--fg-dark-m);
    font-family: var(--font-heading);
    font-weight: 600;
    font-size: var(--text-label);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    text-align: start;
    cursor: pointer;
    transition: background var(--duration-200) var(--ease-out-expo),
                border-color var(--duration-200) var(--ease-out-expo),
                color var(--duration-200) var(--ease-out-expo);
}

.hslider-tab-num {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    opacity: 0.6;
}

.hslider-tab:hover {
    background: rgba(var(--teal-rgb), 0.08);
    color: var(--fg-dark);
}

.hslider-tab:active {
    translate: 0 1px;
}

.hslider-tab:focus-visible {
    outline: 2px solid var(--teal-light);
    outline-offset: 2px;
}

.hslider-tab.active {
    background: rgba(var(--teal-rgb), 0.16);
    border-block-end-color: var(--teal-light);
    color: #fff;
}

.hslider-tab-home {
    padding: 0.35rem 0.7rem;
}

/* An inline "go there" link used to live here: a label with an arrow character in a
   `::after`. The user asked for those links to go and for the arrow character never
   to appear on the site, so the rule is gone with them rather than left for the next
   person to reuse. */

/* === Scroll Hint ===
   A control, not decoration: `main.js` binds a click on it that advances the
   carousel, and its own hover already lightens it. It had no pointer cursor, which
   is what the user noticed - and it does not want one in the hidden state, which
   takes the pointer away with `pointer-events: none`. */
.scroll-hint {
    position: absolute;
    inset-block-end: clamp(1.5rem, 3vh, 2.5rem);
    inset-inline: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    opacity: 0.4;
    cursor: pointer;
    transition: opacity var(--duration-300);
    animation: fade-up var(--duration-800) var(--duration-300) var(--ease-out-expo) both;
}

.scroll-hint:hover {
    opacity: 0.7;
}

.scroll-hint-hidden {
    opacity: 0 !important;
    pointer-events: none !important;
}

.scroll-hint-arrow {
    color: var(--fg);
    animation: bounce-down 2s var(--ease-out-expo) infinite;
}

@keyframes bounce-down {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(6px); }
}

.contact-form {
    inline-size: min(100%, 30rem);
}

.form-body {
    display: flex;
    flex-direction: column;
    gap: clamp(1rem, 2vh, 1.5rem);
}

/* Name and company share a line where the card is wide enough for two, and stack
   on their own below that - so the form stays two rows shorter than it would be
   with every field full width, which matters on a slide that cannot scroll. */
.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: clamp(0.75rem, 1.5vw, 1.25rem);
}

/* === Fields ===
   The label sits above its control. The site's forms used to float it inside the
   input, which reads as decoration once the field is full and vanishes behind
   autofill; a label that stays put is also what the user asked for when they said
   they did not like the inputs. */
.form-field {
    display: grid;
    gap: 0.4rem;
}

.form-field label {
    font-family: var(--font-heading);
    font-size: var(--text-label);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--fg-subtle);
}

.form-field input,
.form-field textarea {
    display: block;
    inline-size: 100%;
    padding: 0.6rem 0.7rem;
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--fg);
    font-family: var(--font-body);
    font-size: var(--text-body);
    line-height: 1.4;
    outline: none;
    text-align: start;
    direction: inherit;
    transition: border-color var(--duration-200) var(--ease-out-expo),
                box-shadow var(--duration-200) var(--ease-out-expo);
}

.form-field textarea {
    resize: vertical;
    min-block-size: 5.5rem;
}

.form-field input:focus,
.form-field textarea:focus {
    border-color: var(--purple);
    box-shadow: 0 0 0 3px rgba(var(--purple-rgb), var(--opacity-focus));
}

/* Submit button */
.contact-form .submit-btn {
    align-self: center;
    margin-block-start: 0.25rem;
}

/* Form status */
.form-status {
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* The message holds its own space, not the empty box: with the confirmation moved
   into the dialog, an always-present 2rem band under the button was dead air - the
   last trace of the line the user called useless. */
.form-status > * {
    min-block-size: 2rem;
    display: inline-flex;
    align-items: center;
}

.form-status .success {
    display: inline-flex;
    padding: 0.3rem 1.25rem;
    background: var(--purple);
    color: #fff;
    font-family: var(--font-heading);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: var(--text-label);
    animation: badge-in var(--duration-300) var(--ease-out-back) both;
}

.form-status .error {
    color: var(--error);
    font-size: var(--text-label);
}

/* === Contact success dialog ===
   A successful send is the one moment the site has something to celebrate, so it
   gets a surface of its own instead of the inline chip it used. The element is a
   native <dialog>: Escape, the backdrop, the top layer and focus containment are
   the platform's, and the script only opens it, aims focus, and hands focus back
   to the button on close. */
.contact-dialog {
    inline-size: min(100% - 2rem, 26rem);
    /* The reset at the top of this file zeroes `margin` on everything, which also
       zeroes the `margin: auto` a modal dialog needs to be centred: without this the
       card sits in the top-left corner with its scrim around it, and a press in the
       corner is inside the box, so the scrim never closes it. */
    margin: auto;
    padding: clamp(1.75rem, 5vw, 2.5rem);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 60px -28px rgba(var(--navy-rgb), 0.45);
    color: var(--fg);
    text-align: center;
}

/* A flat scrim rather than a blurred one: the bar's overlay made the same choice,
   and a modal that dims the slide behind it needs nothing more. */
.contact-dialog::backdrop {
    background: rgba(var(--navy-rgb), 0.45);
}

.contact-dialog[open] {
    animation: badge-in var(--duration-300) var(--ease-out-back) both;
}

.contact-dialog-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 3.25rem;
    block-size: 3.25rem;
    margin-block-end: clamp(1rem, 3vh, 1.5rem);
    border-radius: var(--radius-full);
    background: rgba(var(--purple-rgb), 0.1);
    color: var(--purple);
}

.contact-dialog-mark svg {
    inline-size: 1.5rem;
    block-size: 1.5rem;
}

.contact-dialog-title {
    font-family: var(--font-heading);
    font-size: var(--text-subhead);
    font-weight: 600;
    line-height: 1.2;
    margin-block-end: 0.6rem;
}

.contact-dialog-text {
    font-size: var(--text-body);
    font-weight: 300;
    line-height: 1.6;
    color: var(--fg-muted);
    margin-block-end: clamp(1.25rem, 3vh, 1.75rem);
}

.contact-dialog .cta {
    margin-block-start: 0;
}

/* === Animations === */
@keyframes hero-slide {
    from {
        opacity: 0;
        transform: translateX(-6vw);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@keyframes fade-up {
    from { opacity: 0; transform: translateY(1rem); }
    to   { opacity: 1; transform: translateY(0); }
}

@keyframes badge-in {
    from { opacity: 0; transform: scale(0.9); }
    to   { opacity: 1; transform: scale(1); }
}

/* The colour pools on the closing and practice slides. A drift of about two
   percent over half a minute: transform only, so it stays on the compositor and
   never triggers layout, and small enough that it is noticed a beat late. */
@keyframes pool-drift {
    from { transform: translate3d(-1.2%, -0.8%, 0) scale(1); }
    to   { transform: translate3d(1.2%, 0.8%, 0) scale(1.05); }
}

/* The footer's dot field. A full turn takes three minutes: fast enough to be read
   as motion, slow enough that it never asks to be watched. */
@keyframes grid-turn {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

.hero-buttons {
    animation: fade-up var(--duration-500) var(--ease-out-back) var(--duration-500) both;
}

/* Scroll reveals */
.reveal {
    opacity: 0;
    transform: translateY(1.5rem);
    transition: opacity var(--duration-500) var(--ease-out-expo), transform var(--duration-500) var(--ease-out-back);
}

.reveal.visible {
    opacity: 1;
    transform: translateY(0);
}

/* === Reduced Motion ===
   Mute motion but keep it alive. One-shot entrance animations finish
   instantly (content just appears); the infinite decorative animations
   (coin-spin, bounce-down) run much slower instead of stopping in a frozen
   mid-motion pose. Functional transitions (carousel, slider, hover, focus)
   keep their durations so the UI remains usable.                        */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }

    /* One-shot entrance animations: land in their final state instantly. The menu's
       rows are one of these too, and they stagger - without zeroing the delay each
       row would still wait its turn before appearing. */
    .hero-name-inner,
    .hero-tagline .hero-tagline-text,
    .hero-buttons,
    .form-status .success,
    .nav-center.open .nav-link {
        animation-duration: 0.01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
    }
    .hero-name-inner { opacity: 1; transform: translateX(0); }
    .hero-tagline .hero-tagline-text { opacity: 1; transform: translateY(0); }
    .hero-buttons { opacity: 1; transform: translateY(0); }
    .reveal { opacity: 1; transform: translateY(0); transition: none; }
    .hslider-body { opacity: 1; transform: none; transition: none; }

    /* Infinite decorative animations: slow, not stop */
    .scroll-hint-arrow { animation-duration: 12s !important; }
    /* The pool drift is already near-still; stretched this far it reads as a
       frozen background rather than a paused one. */
    .slide-closing::before,
    .slide-practice::after,
    .slide-footer::after { animation-duration: 240s !important; }
    /* A turn in ten minutes: the field still reads as a field, not as a still. */
    .slide-footer::before { animation-duration: 600s !important; }
}

/* ============================================================
   RESPONSIVE - Mobile-first enhancements
   ============================================================ */

/* Mobile nav trigger. It carries its own name: a bare three-line glyph asks the
   visitor to already know what it means, and the bar has room for the word beside
   it. The glyph and the word are one row, centred, and they wear the same type
   the bar's button does - so the two controls read as one pair. */
.mobile-nav-trigger {
    display: none;
    grid-auto-flow: column;
    align-items: center;
    gap: 0.4rem;
    background: none;
    border: none;
    color: var(--fg);
    cursor: pointer;
    padding: 0.3rem 0.5rem;
    font-family: var(--font-heading);
    font-size: var(--btn-font-sm);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    line-height: 1;
    /* Comfortable tap area without growing the visible glyph */
    min-block-size: 2.75rem;
    border-radius: var(--radius-sm);
    transition: color var(--duration-200) var(--ease-out-expo), background var(--duration-200) var(--ease-out-expo);
}
.nav-trigger-icon {
    inline-size: 1.15rem;
    block-size: 1.15rem;
    flex: none;
}
.mobile-nav-trigger:hover { color: var(--purple); }
.mobile-nav-trigger:focus-visible { outline-offset: 2px; }

/* Overlay menu entry lifts in as it fades. */
@keyframes menu-item {
    from { opacity: 0; translate: 0 12px; }
    to { opacity: 1; translate: 0 0; }
}


/* ============================================================
   SUB-PAGES
   The services page and a service's own page. They are ordinary
   scrolling documents; the carousel belongs to the landing page.
   ============================================================ */
.page-sub {
    overflow: auto;
    block-size: auto;
    min-block-size: 100dvh;
    background: var(--bg);
    color: var(--fg);
}

/* The top bar is the site's own (templates/nav.html), so every page carries the
   same one. On a scrolling document it sticks instead of floating over a slide,
   and it takes a surface so the page never runs underneath it.
   The surface is opaque rather than blurred on purpose: a `backdrop-filter` here
   would make the bar a containing block, and the mobile menu's fixed overlay
   would be trapped inside the bar instead of covering the viewport. */
.page-sub .site-nav {
    position: sticky;
    background: var(--bg);
}

/* Only the dark-slide shading belongs to a slide; the light gradient is the same
   chrome on both, so a document's bar keeps it. */
.page-sub .site-nav::before {
    display: none;
}



/* The sub-pages' "what a service covers" checklist lived here (`.covers`, its rows and
their teal chip). The sub-pages became the slider's domain panels, whose bullets
carry that mark now (see `.hslider-bullets`), and no template has named this class
since - the sweep that found it is the one that caught it. */

/* The bar collapses below 56rem. With the language control showing a whole name,
six links and the switcher need this much room before they can sit side by side. */
@media (max-width: 56rem) {
    /* Grid handles the brand-left / controls-right split; keep the trigger group inline. */
    .nav-left { position: static; }
    .nav-right { display: flex; align-items: center; gap: 0.4rem; }

    /* The bar's language control moves into the overlay menu on phones. Scoped to
       the bar's own copy, which is the one that lives in `.nav-right`: the footer
       has the room for it and keeps it, and the sheet carries its own. */
    .nav-right .lang-switcher { display: none; }

    /* Show mobile trigger, hide desktop links */
    .mobile-nav-trigger { display: inline-grid; }
    .nav-center { display: none; }

    /* The bar's own geometry on touch screens, in one token. The hamburger is
       2.75rem tall - a finger's target, not a mouse's - and the bar pads it by
       half a rem on each side, so the bar stands exactly 3.75rem tall. Every
       slide reserves that much at the top of its section instead of guessing at
       it (see the phones block at the end of this file), which is what keeps a
       section's title clear of the bar. */
    :root { --bar-h: 3.75rem; }
    .nav-inner { padding-block: 0.5rem; }

    /* --- The menu sheet ---
       A panel with a surface of its own, sized by what is in it. It is opaque on
       purpose: the translucent version let the slide behind it ghost through, which
       is neither readable over the hero's wireframe nor over a dark slide, and a
       menu you can half-see the page through does not read as a surface at all.
       It is only as tall as its content - a menu of four rows should not paint an
       empty half-viewport on a tall screen - and it stops at the viewport and
       scrolls inside itself when the content needs more than that.
       The head carries the full lockup, the same render the hero shows, where the
       bar has its emblem: a menu has the room for the mark and the wordmark
       together. The close button sits where the hamburger does.
       The index below is centred, because the sheet is centred on the screen: four
       names read down the middle are the panel's own list, not a document's. The
       foot holds the way out and the language control on one line.
       `position: relative` on the bar makes it the containing block, `stretch`
       overrides the bar's `justify-self: center`, which would shrink the sheet to
       its text, and the max height is what keeps the whole thing on screen. */
    .nav-center.open {
        display: flex;
        position: fixed;
        inset-block-start: 0;
        inset-inline: 0;
        max-block-size: 100vh;
        max-block-size: 100dvh;
        grid-area: auto;
        justify-self: stretch;
        flex-direction: column;
        align-items: stretch;
        /* No gap: the spacing lives on the head instead, so the rows run straight
           into the foot and the foot reads as the menu's last section rather than as
           something that survived below it. That was the report - "I can see the
           contact button, language button below the menu" - and the gap between the
           last link and the pair was what said it. */
        gap: 0;
        padding: 0.75rem var(--space-inline) clamp(1rem, 3vh, 1.75rem);
        background: var(--bg);
        border-block-end: 1px solid var(--border);
        z-index: 200;
        overflow-y: auto;
        /* A flick past the end of a short sheet must not become a slide change:
           main.js already ignores the wheel, the keys and the touch while
           `menu-open` is on the body, and this keeps the sheet's own scroll from
           chaining out of it. */
        overscroll-behavior: contain;
        /* The sheet is a light surface whatever slide is behind it, so it states
           its own colours. Inheriting the bar's dark palette put near-white
           labels on a near-white panel: the menu opened on the dark slides as
           four blank rows. */
        color: var(--fg);
    }

    /* The scrim: it dims the page behind the menu, and the tap that lands on it
       closes the menu. It is its own element rather than a `::before` of the sheet,
       and that is the whole point: a box's own background paints *first* in its
       stacking context, then its negative-z children, so a scrim drawn from the
       sheet covered the sheet's own surface and left only the sheet's content
       crisp above it - a dimmed menu. As a sibling it takes a z-index below the
       sheet's (200) and nothing of the sheet is under it. It is `fixed`, so it
       cannot scroll with the sheet, and it carries `pointer-events: auto` because
       the bar turns pointer events off wholesale. */
    .nav-scrim {
        display: none;
    }

    /* 0.55, not 0.32. At 0.32 the page's own buttons stayed legible under the sheet
       and read as live controls belonging to the menu - the user's report was "I can
       see the contact button, language button below the menu", which is the hero's
       two CTAs showing through. The page behind an open menu has to read as inert,
       and the scrim is the only thing that says so. */
    .nav-center.open + .nav-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 150;
        background: rgba(17, 17, 17, 0.55);
        pointer-events: auto;
    }

    .nav-sheet-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 0.5rem;
        min-block-size: 2.75rem;
        /* The sheet's own spacing, moved here from the container's `gap` so the rows
           can meet the foot with nothing between them. */
        margin-block-end: clamp(1rem, 3vh, 2rem);
    }

    /* The full lockup, the same render the hero carries: a menu is the one surface
       with room for the mark and the wordmark together, and the emblem on its own
       reads as a favicon here. */
    .nav-sheet-mark {
        display: block;
        block-size: clamp(1.5rem, 6vw, 1.9rem);
        inline-size: auto;
    }

    .nav-center.open .nav-close {
        display: grid;
        place-items: center;
        min-inline-size: 2.75rem;
        min-block-size: 2.75rem;
        border: none;
        border-radius: var(--radius-sm);
        background: none;
        color: var(--fg);
        cursor: pointer;
    }
    .nav-center.open .nav-close:hover { color: var(--purple); }
    .nav-center.open .nav-close:focus-visible { outline: 2px solid var(--purple); outline-offset: 2px; }

    /* The index is the sheet's body: rows a finger can hit, on hairlines that stop
       at the last one. The names are centred, because the sheet is centred on the
       screen and a menu read down its middle is the panel's own list rather than a
       document's. */
    .nav-sheet-links {
        display: flex;
        flex-direction: column;
    }

    .nav-center.open .nav-link {
        display: flex;
        align-items: center;
        justify-content: center;
        inline-size: 100%;
        min-block-size: clamp(3.25rem, 7vh, 4rem);
        padding-block: 0.5rem;
        border-block-end: 1px solid var(--border);
        text-align: center;
        font-size: clamp(1.35rem, 6.5vw, 1.75rem);
        animation: menu-item var(--duration-400) var(--ease-out-expo) both;
    }
    .nav-center.open .nav-link:nth-of-type(1){ animation-delay: 0.03s; }
    .nav-center.open .nav-link:nth-of-type(2){ animation-delay: 0.08s; }
    .nav-center.open .nav-link:nth-of-type(3){ animation-delay: 0.13s; }
    .nav-center.open .nav-link:nth-of-type(4){ animation-delay: 0.18s; }
    .nav-center.open .nav-link:nth-of-type(5){ animation-delay: 0.23s; }
    .nav-center.open .nav-link:last-child { border-block-end: none; }

    /* Where the reader is: the row takes the tint the switcher's card gives the
       language in use. */
    .nav-center.open .nav-link.active {
        background: rgba(var(--purple-rgb), var(--opacity-hover));
    }

    /* The foot: the way out of the section, repeated, because the bar's own button
       is behind this sheet - a menu that hides the page's only call to action and
       offers nothing in its place is a dead end. It is the bar's button one size up,
       sized by its label, and it keeps the bar's envelope mark: this button is the
       only filled control on the sheet, so the mark is what says what it does.
       The language control stands beside it on the same line - it is the only other
       thing a reader changes here, and the two fit side by side down to 320px. */
    .nav-sheet-foot {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
        gap: clamp(0.75rem, 3vw, 1.25rem);
        /* The hairline the last row does not draw, and the beat under it: this is what
           makes the pair below the rows read as the menu's foot rather than as loose
           page chrome. */
        border-block-start: 1px solid var(--border);
        padding-block-start: clamp(0.75rem, 2vh, 1.25rem);
        /* One size for the pair below, declared once. It was two: the button took the
           phone's content-CTA size (measured 151x54) while the control kept the
           chrome's small variant (106x26), so the two controls standing on one line
           were visibly different sizes. Here they share a height and a type size, so
           a change to either cannot separate them again; 3rem is also the tap target
           the README asks for on a phone. Their widths stay their own, as they do in
           the bar, where a 52px control stands beside a 111px button and the two
           still read as a pair. */
        --sheet-btn-font: 1rem;
        --sheet-btn-block: 3rem;
    }

    .nav-center.open .nav-sheet-cta {
        display: inline-flex;
        /* The bar's copy sets this too: it is what holds the envelope off the label. */
        gap: 0.5em;
        margin-block-start: 0;
    }

    .nav-sheet-foot .lang-switcher,
    .nav-center.open .nav-sheet-cta {
        font-size: var(--sheet-btn-font);
    }

    .nav-sheet-foot .lang-btn,
    .nav-center.open .nav-sheet-cta {
        block-size: var(--sheet-btn-block);
    }

    /* The sheet is a light surface whatever slide is behind it, and `.on-dark`
       reaches everything inside the bar: the control carried here states the light
       palette, and its card opens *upward*, because the foot sits at the sheet's
       bottom edge and there is nothing under it to open into. */
    .nav-sheet-foot .lang-btn {
        color: var(--fg-muted);
    }
    .nav-sheet-foot .lang-btn:hover,
    .nav-sheet-foot .lang-btn[aria-expanded="true"] {
        color: var(--purple);
    }
    .nav-sheet-foot .lang-btn[aria-expanded="true"] {
        background: rgba(var(--purple-rgb), var(--opacity-focus));
    }
    .nav-sheet-foot .lang-menu {
        inset-block-start: auto;
        inset-block-end: calc(100% + var(--lang-beat-block));
        transform: translateY(var(--lang-beat-block)) scale(0.98);
        transform-origin: 100% 100%;
    }
    [dir="rtl"] .nav-sheet-foot .lang-menu {
        transform-origin: 0 100%;
    }
    /* The card follows the light palette the site uses everywhere outside the bar,
       including where the slide behind the sheet is a dark one. */
    .site-nav.on-dark .nav-sheet-foot .lang-menu {
        background: var(--bg-elevated);
        border-color: var(--border);
        box-shadow: var(--shadow-3);
    }
    .site-nav.on-dark .nav-sheet-foot .lang-menu a {
        color: var(--fg);
    }
    .site-nav.on-dark .nav-sheet-foot .lang-menu a:hover,
    .site-nav.on-dark .nav-sheet-foot .lang-menu a:focus-visible {
        background: var(--purple);
        color: #fff;
    }

    /* The sheet's own palette, ranked above the bar's dark one. The rules written
       for the bar (`.site-nav.on-dark .nav-link`) and for the slides (`.on-dark
       .cta`) also reach these elements, and they carried the dark slide's
       near-white labels and its teal button onto this panel - which is a light
       surface whatever is behind it, because the menu opens from a bar that spends
       half the site on a dark slide. So the sheet restates both, at a weight that
       wins. */
    .site-nav .nav-center.open .nav-link {
        color: var(--fg);
    }

    .site-nav .nav-center.open .nav-link:hover,
    .site-nav .nav-center.open .nav-link.active {
        color: var(--purple);
    }

    .site-nav .nav-center.open .nav-sheet-cta {
        background: var(--purple);
    }

    .site-nav .nav-center.open .nav-sheet-cta:hover {
        background: var(--purple-deep);
    }
}

/* Tablet & below */
@media (max-width: 48rem) {
    .hero-logo { inline-size: min(55vw, 280px); }

    /* The practice slide stacks: the index becomes a row of tabs above the
       stage, and a domain reads top to bottom. The rows themselves come from the
       base rule (`auto` and the leftover) - only the beat between them tightens,
       because centring the rows here would take the stage's leftover height away
       again on a tablet. */
    .practice {
        row-gap: clamp(0.9rem, 2vh, 1.5rem);
    }

    .practice-side {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.6rem 1.25rem;
    }

    .hslider-tabs {
        flex-direction: row;
        flex-wrap: wrap;
        gap: 2px;
        inline-size: auto;
    }

    .hslider-tab {
        padding: 0.35rem 0.7rem;
        border-inline-start: none;
        border-block-end: 2px solid rgba(255, 255, 255, 0.12);
    }

    .hslider-tab.active {
        border-inline-start-color: transparent;
        border-block-end-color: var(--teal-light);
    }

    .hslider-domain-row {
        grid-template-columns: 1fr;
        grid-template-rows: auto;
    }

    /* Stacked, the DOM order is the reading order again. */
    .hslider-domain-head,
    .hslider-domain-title,
    .hslider-domain-summary,
    .hslider-bullets,
    .hslider-domain-actions {
        grid-column: 1;
        grid-row: auto;
    }

    /* Stacked, there is no gap between columns for the bullet's chip to hang in and
       no column for it to overfill, so it goes back into the item: a column of its
       own, with the text hanging off it - which is where a label that wraps to two
       lines on a phone wants its marker anyway. */
    .hslider-bullets li {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        column-gap: 0.7em;
        align-items: start;
    }

    .hslider-bullets li::before {
        position: static;
        margin-block-start: calc((1.5em - 0.5em) / 2);
    }

    /* A service's name no longer takes a tablet size of its own: it is a panel
       heading at every width, from the token, and the section's own heading is a
       step above it wherever the two meet. The hand-written clamps here were
       written when the section had an eyebrow in the label size, and they put the
       panel above the section across the whole middle of the range. */

    /* The contact slide stacks: the panel above the form, both centred. The row
       centring of the base rule still applies - the two rows are the content. */
    .closing-body {
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        row-gap: clamp(1.5rem, 4vh, 2.5rem);
    }

    .closing-panel {
        align-items: center;
        text-align: center;
    }

    .closing-form { inline-size: min(100%, 28rem); }

    /* Touch-first: the links that stand on their own get a comfortable target
       without changing how they read. */
}

/* Phone */
@media (max-width: 32rem) {
    /* Gentler hero tilt on small screens. */
    :root { --slant: -8deg; --slant-back: 8deg; }


    .hero-logo { inline-size: min(70vw, 240px); }

    /* === The phone's section budget ===
       One viewport, one section. The slide's block padding is the bar plus a
       hair at the top - the bar is fixed and translucent, so a section's first
       line starts just under it - and a small hair at the bottom. What is left
       is the section's own budget, and the panels inside it flex into that
       budget instead of measuring themselves against the viewport, which is what
       they cannot know: 68vh of stage plus tabs plus two slide paddings is more
       than a phone has, and the overflow went up under the bar.
       `--space-section` stays what it is above this breakpoint, where a slide has
       room to breathe. */
    .slide {
        padding-block: calc(var(--bar-h) + 0.5rem) 0.5rem;
    }

    /* The hero's pair is the phone's main action, and a phone is not a desktop in
       a narrow column: the type steps up and the pill grows around the label (the
       desktop size is the floor of this scale, not its ceiling). The pair stacks;
       it does not stretch - a button is the size of what it says and no wider, and
       `inline-size: 100%` inside this column turned the two of them into panels. */
    .hero-buttons {
        flex-direction: column;
        align-items: center;
        gap: 0.6rem;
        margin-block-start: clamp(1.25rem, 4vh, 2.25rem);
    }

    .hero-buttons .cta {
        padding-block: 0.95rem;
        font-size: 1rem;
    }

    /* The phone's comfortable button, for the buttons that sit in content: the
       hero's pair, the slide's way out, a form's submit, the menu's. Its type is
       a step above the desktop's rather than a step below it - at arm's length
       the small size is the one that gets misread - and it carries its own
       padding for a thumb. */
    .cta {
        padding: 0.85rem 1.75rem;
        font-size: 1rem;
    }

    /* The chrome's small variant is not one of those: it is a control in the bar
       and in the dialog, sized by the design system's own tokens, so the phone
       takes it back at the same specificity. Without this the top bar's Contact
       button inherited the content padding and stood taller and wider on a phone
       than on the desktop it is a copy of. */
    .cta-sm {
        padding: var(--btn-pad-sm-block) var(--btn-pad-sm-inline);
        font-size: var(--btn-font-sm);
    }

    /* The stage is already the section's leftover (see `.practice`), so the phone
       only tightens the beat between the tabs and it. */
    .practice {
        row-gap: clamp(0.35rem, 1.2vh, 1rem);
    }

    /* The contact slide's whole rhythm in vh, so a shorter phone gets a shorter
       form instead of one that runs off the bottom of the section: the message
       box is the one field that can give, and the panel's copy tightens with it. */
    .closing-body {
        row-gap: clamp(0.75rem, 2vh, 1.5rem);
    }

    .closing-title { margin-block-end: clamp(0.35rem, 1vh, 0.6rem); }
    .closing-lead { margin-block-end: clamp(0.6rem, 1.6vh, 1rem); }
    .closing-social { margin-block-start: clamp(0.5rem, 1.4vh, 0.9rem); }

    .form-body { gap: clamp(0.6rem, 1.6vh, 1rem); }
    .form-field { gap: 0.25rem; }
    .contact-form .submit-btn { margin-block-start: 0; }

    /* The pitch and the number are carried by the service's own page; on a phone
       the domain slide keeps the title, what it covers, and the way out. */
    .hslider-domain-summary { display: none; }

    /* The overview keeps one row per service on a phone: two by two, each card is
       139px wide and its line of copy wraps into five, so the pair of them is
       taller than the four rows they replaced. What four rows need is a tighter
       box - and on the smallest phone it is what pays for the section's heading,
       which is now a heading and not an 11px eyebrow. */
    .hslider-card {
        padding: 0.4rem 0.7rem;
    }

    .hslider-card-icon svg {
        inline-size: 1.5rem;
        block-size: 1.5rem;
    }

    .hslider-card-title { font-size: 0.9rem; }
    .hslider-card-summary { font-size: 0.72rem; }

    .hslider-tabs {
        gap: 1px;
    }

    .hslider-tab {
        font-size: clamp(0.62rem, 2.2vw, 0.78rem);
        padding: 0.3rem 0.55rem;
    }

    /* The same for the phone: the name is a panel heading, from the token. */
    .hslider-bullets li {
        font-size: 0.85rem;
    }

    .contact-form {
        inline-size: 100%;
    }

    .form-field input,
    .form-field textarea {
        /* 16px prevents iOS auto-zoom when the field is focused */
        font-size: 1rem;
        padding: clamp(0.45rem, 1.2vh, 0.7rem) 0.7rem;
    }

    /* The message box is the only control on this slide that can lose height
       without costing the reader anything: three lines of a message are read in
       the field, and the rest scrolls inside it. */
    .form-field textarea {
        min-block-size: clamp(3rem, 10vh, 5.5rem);
    }

    /* Name and company stay on one line even on the narrowest phone. The fields
       are then 8rem wide, which is enough for a name and a company - and this
       slide cannot scroll, so the line they would have taken is worth more than
       the room the two fields lose. */
    .form-row {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    /* The sub-pages' button band stacked here. Both the band and this rule went with
       the sub-pages; the class had been left behind, naming nothing. */
}

/* === Selection & Focus === */
::selection {
    background: rgba(var(--purple-rgb), 0.15);
    color: var(--fg);
}

:focus-visible {
    outline: 2px solid var(--purple);
    outline-offset: 3px;
}

/* === Legal pages ===
   They are ordinary documents, so they take the same `.page-sub` chrome as the
   services pages - same top bar, same footer - and only the prose layout is their
   own. (There used to be a bespoke `.legal-header` bar here, which is exactly the
   second top bar this site must never have.) */

.legal-wrap {
    max-inline-size: 42rem;
    margin-inline: auto;
    padding: 3rem var(--space-inline) 4rem;
}
.legal-wrap h1 {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--purple);
    margin-block-end: 2rem;
}
.legal-wrap p {
    font-size: 0.95rem;
    color: var(--fg-muted);
    line-height: 1.7;
    margin-block-end: 1rem;
}
.legal-wrap a { color: var(--purple); text-decoration: none; }
.legal-wrap .legal-back { margin-block-start: 2.5rem; font-size: 0.85rem; }
.legal-wrap .legal-back a { display: inline-flex; align-items: center; gap: 0.35rem; min-block-size: 1.5rem; }
.legal-wrap .chevron { inline-size: 14px; block-size: 14px; flex: none; }
