/**
 * assets/css/app.css, everything the first paint does not need.
 *
 * Loaded non-blocking by the layout. Tokens and the reset live in
 * critical.css; this file is components only, and it keeps to the same rule:
 * logical properties on the inline axis, so Arabic and English share it.
 */

/* ═══ Buttons ═══════════════════════════════════════════════════════════ */
.btn {
    --btn-bg: transparent;
    --btn-fg: var(--ink);
    --btn-border: var(--line-strong);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    padding: 0.7rem 1.4rem;
    font-size: var(--step--1);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    background: var(--btn-bg);
    color: var(--btn-fg);
    border: 1px solid var(--btn-border);
    border-radius: var(--radius-pill);
    transition: transform 140ms var(--ease),
                background var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease),
                opacity var(--dur) var(--ease);
}
.btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.btn:active:not(:disabled) { transform: translateY(0); }
.btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }

.btn--primary {
    --btn-bg: var(--brand);
    --btn-fg: var(--brand-ink);
    --btn-border: transparent;
}

/* ═══ UI kit: toasts, modals, confirmations ═════════════════════════════
 * One surface and one set of motions for everything the site says. A toast,
 * a modal and a confirmation are the same glass pill at different sizes, so
 * a new feature inherits the look instead of inventing one.
 *
 * The stack sits below the navbar, centred on the inline axis, and each pill
 * is a flex row, which is what makes the whole kit direction-neutral.
 */
.ui-stack {
    position: fixed;
    inset-block-start: calc(var(--header-h) + var(--space-xs));
    inset-inline: 0;
    z-index: 9000;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
    padding-inline: var(--space-md);
    pointer-events: none;
}

/* ── The pill ──────────────────────────────────────────────────────────── */
.ui-pill {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    max-inline-size: min(34rem, 100%);
    padding: 0.4rem 0.4rem 0.4rem 0.85rem;
    padding-inline: 0.85rem 0.4rem;
    color: var(--ink);
    background: var(--glass-bg);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-edge);
    border-radius: var(--radius-pill);
    box-shadow:
        inset 0 1px 0 0 var(--glass-specular),
        inset 0 -1px 0 0 var(--glass-shade),
        var(--glass-shadow);
    pointer-events: auto;

    /* Entrance matches the navbar's language: drops in from above. */
    opacity: 0;
    transform: translateY(-14px) scale(0.97);
    transition: opacity 240ms var(--ease), transform 320ms var(--ease);
}
.ui-pill.is-in  { opacity: 1; transform: none; }
.ui-pill.is-out { opacity: 0; transform: translateY(-10px) scale(0.98); }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .ui-pill { background: var(--surface-raised); }
}

.ui-pill__icon { inline-size: 18px; block-size: 18px; flex-shrink: 0; }
.ui-pill--success .ui-pill__icon { color: var(--success); }
.ui-pill--error   .ui-pill__icon { color: var(--danger); }
.ui-pill--warning .ui-pill__icon { color: #b45309; }
.ui-pill--info    .ui-pill__icon { color: var(--brand); }

.ui-pill__text {
    flex: 1;
    min-inline-size: 0;
    font-size: var(--step--1);
    font-weight: 600;
    line-height: 1.45;
}

.ui-pill__btn {
    flex-shrink: 0;
    padding: 0.42rem 0.9rem;
    font-family: inherit;
    font-size: var(--step--1);
    font-weight: 700;
    color: var(--ink-muted);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.ui-pill__btn:hover { color: var(--ink); background: var(--hover-bg); }
.ui-pill__btn.is-primary {
    color: var(--brand-ink);
    background: var(--brand);
}
.ui-pill__btn.is-primary:hover { filter: brightness(1.1); background: var(--brand); }

.ui-pill__close {
    flex-shrink: 0;
    display: grid;
    place-items: center;
    inline-size: 26px;
    block-size: 26px;
    color: var(--ink-muted);
    background: transparent;
    border: 0;
    border-radius: 50%;
    cursor: pointer;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.ui-pill__close:hover { background: var(--hover-bg); color: var(--ink); }
.ui-pill__close svg { inline-size: 13px; block-size: 13px; }

/* ── Countdown ring ────────────────────────────────────────────────────── */
.ui-ring {
    position: relative;
    display: grid;
    place-items: center;
    inline-size: 28px;
    block-size: 28px;
    flex-shrink: 0;
}
.ui-ring svg {
    position: absolute;
    inset: 0;
    transform: rotate(-90deg);     /* start at twelve o'clock, run clockwise */
}
.ui-ring__track,
.ui-ring__fill { fill: none; stroke-width: 2.5; }
.ui-ring__track { stroke: var(--hover-bg); }
.ui-ring__fill {
    stroke: var(--brand);
    stroke-linecap: round;
    transition: stroke-dashoffset 1s linear;
}
.ui-ring__count {
    position: relative;
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
    line-height: 1;
}

/* Narrow screens: the ring and the action carry the meaning, the sentence goes. */
@media (max-width: 420px) {
    .ui-pill { inline-size: 100%; }
    .ui-pill--update .ui-pill__text { display: none; }
}

/* ═══ Modal ═════════════════════════════════════════════════════════════ */
.ui-modal {
    position: fixed;
    inset: 0;
    z-index: 9500;
    display: grid;
    place-items: center;
    padding: var(--space-md);
}

.ui-modal__scrim {
    position: absolute;
    inset: 0;
    background: rgba(11, 11, 12, 0.45);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    opacity: 0;
    transition: opacity 240ms var(--ease);
}
.ui-modal.is-in  .ui-modal__scrim { opacity: 1; }
.ui-modal.is-out .ui-modal__scrim { opacity: 0; }

.ui-modal__panel {
    position: relative;
    inline-size: min(26rem, 100%);
    max-block-size: calc(100svh - 2 * var(--space-xl));
    overflow-y: auto;
    padding: var(--space-xl) var(--space-lg) var(--space-lg);
    text-align: center;
    background: var(--surface-raised);
    border: 1px solid var(--glass-edge);
    border-radius: var(--radius-lg);
    box-shadow:
        inset 0 1px 0 0 var(--glass-specular),
        var(--shadow-lg);

    /* Same easing as the pills, scaled up: it reads as one family. */
    opacity: 0;
    transform: translateY(10px) scale(0.96);
    transition: opacity 240ms var(--ease), transform 320ms var(--ease);
}
.ui-modal.is-in  .ui-modal__panel { opacity: 1; transform: none; }
.ui-modal.is-out .ui-modal__panel { opacity: 0; transform: translateY(6px) scale(0.98); }
.ui-modal__panel:focus { outline: none; }

.ui-modal__panel .ui-pill__close {
    position: absolute;
    inset-block-start: var(--space-sm);
    inset-inline-end: var(--space-sm);
}

.ui-modal__title {
    font-size: var(--step-1);
    font-weight: 700;
    margin-block-end: var(--space-xs);
}
.ui-modal--danger .ui-modal__title { color: var(--danger); }

.ui-modal__body {
    font-size: var(--step--1);
    line-height: 1.65;
    color: var(--ink-muted);
}

.ui-modal__actions {
    display: flex;
    gap: var(--space-xs);
    justify-content: center;
    flex-wrap: wrap;
    margin-block-start: var(--space-lg);
}

.ui-modal__btn {
    flex: 1 1 8rem;
    padding: 0.7rem 1.2rem;
    font-family: inherit;
    font-size: var(--step--1);
    font-weight: 700;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: background var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                filter var(--dur) var(--ease);
}
.ui-modal__btn:hover { background: var(--hover-bg); }
.ui-modal__btn.is-primary {
    color: var(--brand-ink);
    background: var(--brand);
    border-color: transparent;
}
.ui-modal__btn.is-danger {
    color: #fff;
    background: var(--danger);
    border-color: transparent;
}
.ui-modal__btn.is-primary:hover,
.ui-modal__btn.is-danger:hover { filter: brightness(1.1); }

@media (prefers-reduced-motion: reduce) {
    .ui-pill, .ui-modal__panel, .ui-modal__scrim { transition: opacity 120ms linear; transform: none; }
    .ui-pill.is-in, .ui-modal.is-in .ui-modal__panel { transform: none; }
    .ui-ring__fill { transition: none; }
}

/* ═══ Mobile menu ═══════════════════════════════════════════════════════ */
.mobile-menu { position: fixed; inset: 0; z-index: 500; }

.mobile-menu__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(11, 11, 12, 0.45);
    backdrop-filter: blur(3px);
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
}
.mobile-menu.is-open .mobile-menu__backdrop { opacity: 1; }

.mobile-menu__panel {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    inline-size: min(22rem, 86vw);
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    padding: var(--space-xl) var(--space-lg);
    background: var(--paper);
    box-shadow: var(--shadow-lg);
    /* Slides in from the END edge, right in LTR, left in RTL. */
    transform: translateX(calc(var(--panel-shift, 1) * 100%));
    transition: transform 320ms var(--ease);
}
:root[dir="rtl"] .mobile-menu__panel { --panel-shift: -1; }
.mobile-menu.is-open .mobile-menu__panel { transform: translateX(0); }

.mobile-menu__close {
    align-self: flex-end;
    display: grid;
    place-items: center;
    inline-size: 36px;
    block-size: 36px;
    background: var(--paper-alt);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.mobile-menu__nav { display: grid; gap: var(--space-xs); }
.mobile-menu__link {
    padding: var(--space-sm) 0;
    font-size: var(--step-1);
    font-weight: 600;
    color: var(--ink-soft);
    border-block-end: 1px solid var(--line);
    transition: color var(--dur) var(--ease), padding-inline-start var(--dur) var(--ease);
}
.mobile-menu__link:hover,
.mobile-menu__link.is-active { color: var(--brand); padding-inline-start: var(--space-xs); }

.mobile-menu__foot {
    margin-block-start: auto;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

body.is-locked {
    position: fixed;
    inline-size: 100%;
    overflow: hidden;
}

/* ═══ Footer ════════════════════════════════════════════════════════════
 * The phone problem: three columns of links stacked vertically become several
 * screens of scrolling nobody reads. Each column is a <details> here, so a
 * phone gets a few collapsed rows; from 768px up the CSS opens them all and
 * hides the disclosure arrow, giving an ordinary multi-column footer.
 *
 * Native elements, so it works with JavaScript off and assistive technology
 * already knows what a disclosure is.
 */
.site-footer {
    position: relative;
    isolation: isolate;
    /* Generous on a desktop, tight on a phone. Breathing room that costs
       nothing on a wide screen is a third of a viewport on a small one. */
    margin-block-start: var(--space-2xl);
    padding-block: var(--space-xl) var(--space-md);
    background: var(--paper-alt);
    border-block-start: 1px solid var(--line);
    /* Reaches past the bottom of the page so a phone's overscroll bounce
       shows footer rather than canvas. Costs nothing: it is below the fold by
       definition. */
    margin-block-end: -1px;
    padding-block-end: calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
    overflow: hidden;
}

/* A wash of brand colour bleeding up from the bottom edge. */
.site-footer__glow {
    position: absolute;
    inset-block-end: -60%;
    /* Centred with physical `left`, deliberately. Under inset-inline-start
       this resolves to `right` in Arabic, which put the wash a full width off
       the side of the page, and the hero had no light in it at all. The
       centre of the screen is the same place in both directions. */
    left: 50%;
    z-index: -1;
    inline-size: min(120%, 900px);
    aspect-ratio: 2 / 1;
    transform: translateX(-50%);
    /* Was --brand-soft, which is a 9% tint meant for filling small controls.
       Spread across nine hundred pixels it came to nothing at all. This is
       the same light as the hero's, rising from below instead of falling
       from above, so the page opens and closes on the same colour. */
    background: radial-gradient(closest-side,
        color-mix(in srgb, var(--brand) 24%, transparent) 0%,
        color-mix(in srgb, var(--brand) 12%, transparent) 40%,
        color-mix(in srgb, var(--brand) 4%,  transparent) 64%,
        transparent 82%);
    opacity: 0.95;
    pointer-events: none;
}

.site-footer__inner {
    inline-size: 100%;
    max-inline-size: var(--container);
    margin-inline: auto;
    padding-inline: var(--gutter);
    display: grid;
    gap: var(--space-md);
}

/* ── Brand block ───────────────────────────────────────────────────────── */
.site-footer__brand { display: grid; gap: var(--space-sm); justify-items: start; }

.brand--plain {
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    font-size: var(--step-0);
    color: var(--ink);
}
.brand--plain::before { display: none; }
.brand--plain { justify-self: start; }
.brand--plain .brand__logo { block-size: 34px; }

.site-footer__tagline {
    font-size: var(--step--1);
    color: var(--ink-muted);
    max-inline-size: 34ch;
}

.site-footer__social { display: flex; gap: var(--space-xs); list-style: none; padding: 0; }
.social-dot {
    display: grid;
    place-items: center;
    inline-size: 34px;
    block-size: 34px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-pill);
    color: var(--ink-muted);
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.social-dot:hover { color: var(--ink); border-color: var(--ink-muted); }

/* ── Columns ───────────────────────────────────────────────────────────── */
.site-footer__cols { display: grid; gap: 0; }

.footer-col { border-block-start: 1px solid var(--line); }

.footer-col__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    /* Keeps the row at a ~46px touch target, the floor for a tap target, without the padding a desktop row could afford. */
    padding-block: 0.78rem;
    font-size: var(--step--1);
    font-weight: 700;
    color: var(--ink);
    cursor: pointer;
    list-style: none;                     /* Firefox */
    user-select: none;
}
.footer-col__head::-webkit-details-marker { display: none; }   /* Safari/Chrome */

/* The disclosure arrow, drawn rather than relying on the UA marker. */
/* Physical borders on purpose. A disclosure chevron means "down to expand" in
   both directions, it is a direction of motion, not a direction of reading.
   With logical borders it flipped to point sideways in Arabic. */
.footer-col__head::after {
    content: '';
    inline-size: 8px;
    block-size: 8px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    opacity: 0.45;
    transition: transform var(--dur) var(--ease);
}
.footer-col[open] > .footer-col__head::after {
    transform: rotate(-135deg) translateY(-2px);
}

/* Collapse is stated explicitly rather than left to the browser. How a closed
   <details> hides its content has changed across engine versions, some use a
   display rule on the children, newer Chrome uses ::details-content with
   content-visibility, and depending on that left a phone with every column
   expanded on one of them. Saying display:none outright is the same in all of
   them. */
.footer-col__body {
    gap: var(--space-xs);
    padding-block-end: var(--space-md);
}
.footer-col > .footer-col__body       { display: none; }
.footer-col[open] > .footer-col__body { display: grid; }
.footer-col__body a {
    font-size: var(--step--1);
    color: var(--ink-muted);
    transition: color var(--dur) var(--ease);
}
.footer-col__body a:hover { color: var(--ink); }
.footer-col__body a[aria-current="true"] { color: var(--ink); font-weight: 600; }

/* ── From tablet up: open everything, drop the accordion affordance ────── */
@media (min-width: 768px) {
    .site-footer {
        margin-block-start: var(--space-3xl);
        padding-block: var(--space-2xl) var(--space-lg);
    }
    .site-footer__inner {
        grid-template-columns: minmax(0, 1.2fr) minmax(0, 2fr);
        align-items: start;
        gap: var(--space-xl);
    }
    .site-footer__bottom { padding-block-start: var(--space-lg); }
    .site-footer__cols {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: var(--space-lg);
    }

    .footer-col { border-block-start: 0; }

    .footer-col__head {
        padding-block: 0 var(--space-sm);
        cursor: default;
        pointer-events: none;             /* not a control any more */
        font-size: 11px;
        letter-spacing: 0.09em;
        text-transform: uppercase;
        color: var(--ink-faint);
    }
    :root[lang="ar"] .footer-col__head { letter-spacing: 0; text-transform: none; }
    .footer-col__head::after { display: none; }

    /* Opening the columns here takes TWO rules, because engines collapse a
       closed <details> in two different ways.
       
       Older engines hide the non-summary children with a display rule, which
       the first line overrides.
       
       Chrome 131+ instead puts the content in a ::details-content pseudo with
       content-visibility: hidden. That still gives the child a layout box, getBoundingClientRect reports a perfectly normal, visible-looking
       element, but skips PAINTING it, and no amount of `display` on the
       child reaches a pseudo-element. Without the second line the desktop
       footer rendered its headings and silently dropped every link under
       them. */
    .footer-col > .footer-col__body { display: grid !important; }
    .footer-col::details-content {
        content-visibility: visible;
        block-size: auto;
    }

    .site-footer__bottom { grid-column: 1 / -1; }
}

/* ── Bottom bar ────────────────────────────────────────────────────────── */
.site-footer__bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xs);
    padding-block-start: var(--space-md);
    border-block-start: 1px solid var(--line);
}
.site-footer__copy { font-size: var(--step--1); color: var(--ink-faint); }

.footer-top {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: 0.42rem 0.9rem;
    font-family: inherit;
    font-size: var(--step--1);
    font-weight: 600;
    color: var(--ink-muted);
    background: transparent;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.footer-top:hover { color: var(--ink); border-color: var(--ink-muted); }
.footer-top svg { inline-size: 13px; block-size: 13px; }

/* ═══ Error pages ═══════════════════════════════════════════════════════ */
.section--error { min-block-size: 60svh; display: grid; place-items: center; }
.error-page { text-align: center; display: grid; gap: var(--space-md); justify-items: center; }
.error-page__code {
    font-size: var(--step-4);
    font-weight: 800;
    color: var(--line-strong);
    line-height: 1;
}
.error-page__title { font-size: var(--step-2); }
.error-page__text { color: var(--ink-muted); max-inline-size: 42ch; }

/* ═══ Utilities ═════════════════════════════════════════════════════════ */
.is-navigating { cursor: progress; }

/* Long pages stay cheap to paint: sections below the fold skip layout until
   they are close to the viewport. */
.section { content-visibility: auto; contain-intrinsic-size: auto 500px; }

@media print {
    .site-header, .site-footer, .ui-stack, .ui-modal, .mobile-menu, .route-progress { display: none !important; }
    body { color: #000; background: #fff; }
}

/* ═══ The stage ═════════════════════════════════════════════════════════
 * One lit object, type above it, space around it.
 *
 * It follows the theme like everything else: a lit room in light mode, a dark
 * room in dark mode. Sizing comes from the type scale and the space scale
 * already defined in critical.css, not from bespoke clamps, so the hero sits
 * in the same rhythm as the rest of the site.
 */
.stage {
    position: relative;
    isolation: isolate;
    display: grid;
    place-items: center;
    /* Tall enough to feel like a stage, not so tall that the page below it is
       a rumour. Capped in px so a large monitor does not stretch it. */
    min-block-size: min(72svh, 620px);
    margin-block-start: calc(var(--header-h) * -1 - var(--space-sm));
    /* Clear of the bar, with room to breathe. The headline used to start
       almost against the navbar. */
    padding-block: calc(var(--header-h) + var(--space-2xl)) var(--space-lg);
    overflow: hidden;
    /* No panel of its own. The page carries one continuous field of colour
       behind everything (see the aurora below), and an opaque background here
       would cut it off in a straight line at the bottom of the hero. */
    background: transparent;
    color: var(--stage-ink);
}

/* Light: a lit room. */
:root {
    --stage-bg:     var(--paper-alt);
    --stage-ink:    var(--ink);
    --stage-dim:    var(--ink-muted);
    --stage-faint:  var(--ink-faint);
    --tile-face:    linear-gradient(150deg, #ffffff, #e9e9ee);
    --tile-edge:    rgba(11, 11, 12, 0.10);
    --tile-shadow:  0 10px 24px rgba(11, 11, 12, 0.12);
    --tile-inner:   rgba(255, 255, 255, 0.9);
    /* The brand wash behind the object. Carried high on a light page: it is
       the one piece of colour up there and at a third of its strength it was
       invisible against the paper. */
    --stage-glow:   0.95;
}

/* The hero's own light, held below the top of the screen.
 *
 * It used to start a fifth of its own height ABOVE the stage, which pushed
 * brand colour into the first strip of the page, and that strip is the one a
 * phone's status bar has to match. At this height its contribution up there
 * is nothing measurable, and it still does its actual job, which is putting
 * light behind the headline and the tiles. */
.stage__light {
    position: absolute;
    inset-block-start: 10%;
    /* Centred with physical `left`, deliberately. Under inset-inline-start
       this resolves to `right` in Arabic, which put the wash a full width off
       the side of the page, and the hero had no light in it at all. The
       centre of the screen is the same place in both directions. */
    left: 50%;
    z-index: -2;
    inline-size: min(150%, 1100px);
    aspect-ratio: 1.3 / 1;
    transform: translateX(-50%);
    pointer-events: none;
    opacity: var(--stage-glow);
    background: radial-gradient(closest-side,
        color-mix(in srgb, var(--brand) 26%, transparent) 0%,
        color-mix(in srgb, var(--brand) 14%, transparent) 38%,
        color-mix(in srgb, var(--brand) 5%,  transparent) 62%,
        transparent 80%);
}

.stage__grain {
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0.14;
    mix-blend-mode: soft-light;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
}

.stage__inner {
    display: grid;
    justify-items: center;
    text-align: center;
    gap: var(--space-sm);
}

/* ── Type: the site's own scale ────────────────────────────────────────── */
/* A stray `:root[lang="ar"]` used to sit on the line above this selector,
   which quietly turned the whole block into a descendant rule and meant
   English never received any of it: no size, no weight, no colour, no width
   cap and, most visibly, no text-wrap: balance. The English headline was
   falling back to a default h1 and breaking wherever it ran out of room,
   which is how "click." ended up alone on a line of its own. */
.stage__title {
    font-size: clamp(1.85rem, 1.3rem + 2.2vw, 3rem);
    font-weight: 800;
    line-height: 1.06;
    letter-spacing: -0.04em;
    /* balance splits the lines evenly instead of filling the first and
       leaving the remainder stranded. The cap is what gives it something to
       balance: without one there is only ever a single line to fill. */
    text-wrap: balance;
    max-inline-size: 20ch;
    color: var(--stage-ink);
}
:root[lang="ar"] .stage__title { letter-spacing: 0; line-height: 1.24; max-inline-size: 18ch; }

.stage__word {
    display: inline-block;
    animation: stage-word 820ms var(--ease) backwards;
    animation-delay: calc(160ms + var(--i) * 80ms);
}
/* Entrances move and blur, and never touch opacity.
 *
 * An animation that starts at opacity 0 leaves the element invisible for as
 * long as the animation has not run, and "has not run" turns out to have many
 * causes. It has blanked this hero twice. Animating transform and filter only
 * means the worst a stalled entrance can do is leave something slightly offset
 * or soft, which nobody would file a bug about. */
@keyframes stage-word {
    from { transform: translateY(0.36em); filter: blur(6px); }
    to   { transform: none; filter: none; }
}

.stage__lede {
    font-size: var(--lede);
    line-height: 1.6;
    color: var(--stage-dim);
    max-inline-size: 46ch;
    text-wrap: balance;
    animation: stage-rise 820ms var(--ease) 460ms backwards;
}
@keyframes stage-rise {
    from { transform: translateY(16px); filter: blur(4px); }
    to   { transform: none; filter: none; }
}

/* ── The object ────────────────────────────────────────────────────────── */
.stage__object {
    position: relative;
    display: grid;
    /* Pinned to LTR on purpose, and the one place in this stylesheet that is.
       Under direction: rtl a grid lays its columns out right to left, so tile
       one rendered top-RIGHT in Arabic while both the launch vectors below
       (--lx) and the scatter offsets in hero.js place it top-LEFT. Every tile
       then crossed the composition to reach a position on the wrong side.
       These nine tiles carry icons and no text, so there is no reading order
       to respect: one layout serves both languages, and the animation
       matches it in both. */
    direction: ltr;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(10px, 1.4vw, 18px);
    inline-size: clamp(210px, 26vw, 340px);
    aspect-ratio: 1;
    /* Clearance for the scattered state, so tiles never land on the type. */
    margin-block: var(--space-2xl) var(--space-xl);
    cursor: pointer;
    animation: stage-rise 820ms var(--ease) 580ms backwards;
}

.tile {
    display: grid;
    place-items: center;
    aspect-ratio: 1;
    border-radius: clamp(14px, 2vw, 24px);
    background: var(--tile-face);
    border: 1px solid var(--tile-edge);
    box-shadow: var(--tile-shadow), inset 0 1px 0 0 var(--tile-inner);
    transition: transform 720ms cubic-bezier(0.28, 1.18, 0.36, 1);
    /* Promoted for the whole of the hero's life: these move often enough that
       paying the layer cost once beats re-promoting on every transition. */
    will-change: transform;
    transform: translate3d(0, 0, 0);
}

.tile--brand {
    background: linear-gradient(150deg,
        color-mix(in srgb, var(--brand) 88%, white), var(--brand));
    border-color: color-mix(in srgb, var(--brand) 55%, white);
    box-shadow: var(--tile-shadow),
                0 0 30px color-mix(in srgb, var(--brand) 45%, transparent),
                inset 0 1px 0 0 rgba(255, 255, 255, 0.35);
}

.stage__hint {
    font-size: var(--step--1);
    font-weight: 600;
    color: var(--stage-faint);
    transition: opacity var(--dur) var(--ease);
    animation: stage-rise 820ms var(--ease) 680ms backwards;
}
.stage__hint.is-quiet { opacity: 0; }

/* ── Actions and family ────────────────────────────────────────────────── */
.stage__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-xs);
    animation: stage-rise 820ms var(--ease) 760ms backwards;
}

/* The quiet half of the pair on the stage. It takes its colour from the
   stage rather than the page so it stays correct if the stage is ever given
   a surface of its own again. */
.btn--stage {
    color: var(--stage-ink);
    background: color-mix(in srgb, var(--stage-ink) 8%, transparent);
    border-color: color-mix(in srgb, var(--stage-ink) 18%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
}
.btn--stage:hover { background: color-mix(in srgb, var(--stage-ink) 14%, transparent); }

.stage__family {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-md);
    list-style: none;
    padding: 0;
    margin-block-start: var(--space-sm);
    animation: stage-rise 820ms var(--ease) 840ms backwards;
}
.stage__family li { display: inline-flex; align-items: baseline; gap: var(--space-2xs); }
.stage__member-name  { font-size: var(--step--1); font-weight: 700; color: var(--stage-dim); }
.stage__member-state { font-size: var(--step--1); font-weight: 500; color: var(--stage-faint); }

@media (max-width: 560px) {
    .stage {
        min-block-size: auto;
        padding-block: calc(var(--header-h) + var(--space-xl)) var(--space-xl);
    }
    .stage__family { gap: var(--space-sm); }
    .stage__object { margin-block: var(--space-md) var(--space-lg); }
}

@media (prefers-reduced-motion: reduce) {
    .stage__word, .stage__lede, .stage__object,
    .stage__hint, .stage__actions, .stage__family { animation: none; }
    .tile { transition: none; }
}

/* Scroll-driven recede: removed.
 *
 * It faded the type and the object out as you scrolled off the stage, using
 * animation-timeline: scroll(root) with fill: both and an end state of
 * opacity 0. When the page is not tall enough to scroll, that timeline has no
 * range to resolve against and the animation pins at its END, so the whole
 * hero rendered invisible. The home page is exactly the page most likely to
 * be short.
 *
 * This is the second time an animation whose end state is invisible has taken
 * the hero out, and I could not reproduce or verify the effect in a headless
 * browser either way. An effect that cannot be verified and whose failure mode
 * is a blank hero is not worth the depth it adds.
 *
 * If it comes back: animate transform only, never opacity, so the worst case
 * is content in the wrong position rather than no content at all.
 */

/* The arrival, entirely in CSS.
 *
 * Each tile flies in from its own direction and lands on the grid. There is no
 * JavaScript in this at all, which is the point: an earlier version had the
 * script remove a class to release the tiles, so anything that stopped
 * hero.js loading left them parked off-screen and the object simply was not
 * there. A stylesheet that fails to load takes the whole page with it anyway,
 * so depending on one is not adding a way to break.
 *
 * One keyframe, nine directions: the launch vector is a custom property per
 * tile and the animation reads it.
 */
@keyframes tile-arrive {
    from {
        transform: translate(var(--lx), var(--ly)) rotate(var(--lr)) scale(0.32);
    }
    to {
        transform: none;
    }
}

@media (prefers-reduced-motion: no-preference) {
    .tile {
        animation: tile-arrive 1550ms cubic-bezier(0.17, 1, 0.21, 1) backwards;
        animation-delay: calc(var(--n, 0) * 62ms);
    }
}

.tile:nth-child(1) { --lx: -64vw; --ly: -54vh; --lr: -48deg; }
.tile:nth-child(2) { --lx:   4vw; --ly: -78vh; --lr:  40deg; }
.tile:nth-child(3) { --lx:  64vw; --ly: -54vh; --lr:  52deg; }
.tile:nth-child(4) { --lx: -78vw; --ly:   6vh; --lr: -36deg; }
.tile:nth-child(5) { --lx:   8vw; --ly: -86vh; --lr:  28deg; }
.tile:nth-child(6) { --lx:  78vw; --ly:   6vh; --lr:  36deg; }
.tile:nth-child(7) { --lx: -64vw; --ly:  58vh; --lr:  44deg; }
.tile:nth-child(8) { --lx:  -4vw; --ly:  80vh; --lr: -40deg; }
.tile:nth-child(9) { --lx:  64vw; --ly:  58vh; --lr: -52deg; }

/* ── Arrival ────────────────────────────────────────────────────────────
 * The bloom that fires where the tiles land: a ring of brand light pushing
 * outward once, then gone. Purely transient, created and removed by the
 * script, so it can never be left over the content.
 */
.stage__bloom {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    z-index: -1;
    inline-size: 100%;
    aspect-ratio: 1;
    translate: -50% -50%;
    border-radius: 50%;
    pointer-events: none;
    background: radial-gradient(closest-side,
        color-mix(in srgb, var(--brand) 42%, transparent) 0%,
        color-mix(in srgb, var(--brand) 16%, transparent) 48%,
        transparent 72%);
    animation: bloom-out 1100ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes bloom-out {
    from { scale: 0.25; opacity: 0; }
    35%  { opacity: 1; }
    to   { scale: 3.4; opacity: 0; }
}

/* The moment of landing: the brand tile flares, then settles. */
.stage__object.is-landed .tile--brand {
    animation: tile-flare 900ms var(--ease);
}
@keyframes tile-flare {
    0%   { box-shadow: var(--tile-shadow), 0 0 0 color-mix(in srgb, var(--brand) 60%, transparent); }
    30%  { box-shadow: var(--tile-shadow), 0 0 62px color-mix(in srgb, var(--brand) 85%, transparent); }
    100% { box-shadow: var(--tile-shadow), 0 0 30px color-mix(in srgb, var(--brand) 45%, transparent); }
}

/* A sheen running over the assembled grid, the way light crosses a surface as
   it settles.
 *
 * It used to be one band drawn over the whole object. The object is a grid
 * with gaps, so the band lit the empty space between the tiles just as
 * brightly as the tiles themselves, and a rectangle of light hanging in a gap
 * reads exactly like a pane of glass nobody can see. Hard stops at 38% and
 * 62% of the width gave it a cut edge on top of that.
 *
 * Now each tile carries its own highlight and clips it to its own corners, so
 * light only ever appears where there is something to catch it. The gaps stay
 * dark, which is what makes it read as nine separate surfaces. The delays run
 * along the diagonal, so the highlight crosses the grid as one wave rather
 * than flashing on all nine at once. */
.stage__object.is-landed .tile::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    border-radius: inherit;
    /* A wide, gentle falloff. The old one went from nothing to full white
       inside 12% of the width, which is a hard edge however briefly it is on
       screen. This one takes 35% to arrive and 35% to leave. */
    background: linear-gradient(115deg,
        transparent 0%,
        color-mix(in srgb, white 6%,  transparent) 28%,
        color-mix(in srgb, white 34%, transparent) 50%,
        color-mix(in srgb, white 6%,  transparent) 72%,
        transparent 100%);
    animation: sheen 1100ms cubic-bezier(0.33, 0, 0.3, 1) backwards;
    animation-delay: calc(var(--sheen, 0) * 90ms);
}
/* The tile has to clip it, or the highlight spills back into the gaps the
   whole change was meant to clear. */
.stage__object.is-landed .tile { overflow: hidden; }

@keyframes sheen {
    from { transform: translateX(-160%); opacity: 0; }
    18%  { opacity: 1; }
    82%  { opacity: 1; }
    to   { transform: translateX(160%); opacity: 0; }
}

/* Diagonal order, so the wave enters at one corner and leaves at the other. */
.tile:nth-child(1) { --sheen: 0; }
.tile:nth-child(2) { --sheen: 1; }
.tile:nth-child(3) { --sheen: 2; }
.tile:nth-child(4) { --sheen: 1; }
.tile:nth-child(5) { --sheen: 2; }
.tile:nth-child(6) { --sheen: 3; }
.tile:nth-child(7) { --sheen: 2; }
.tile:nth-child(8) { --sheen: 3; }
.tile:nth-child(9) { --sheen: 4; }

/* The scroll recede is gone.
 *
 * It lifted the whole hero as you scrolled and set it back down on the way
 * up, which on top of the arrival read as the tiles leaving through the top
 * and coming back from it. The arrival is the moment worth having; a second
 * movement of the same object competing with it is one too many.
 *
 * What comes back below is blur and only blur. Nothing moves, so the hero
 * cannot appear to leave and return; it just goes soft as you scroll past it
 * and sharpens as you come back.
 */

/* ── Focus pull on scroll ───────────────────────────────────────────────
 * The hero defocuses as it leaves and sharpens as it returns.
 *
 * Filter only. No transform, so nothing shifts position and the arrival is the
 * only movement the object ever makes. No opacity either: blur softens
 * content, it never removes it, so even a timeline that resolves wrongly
 * leaves a readable page.
 *
 * On view() with an exit range, so a page too short to scroll never begins
 * exiting and the animation rests at 0%, which is no blur at all.
 */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        .stage__inner {
            animation: stage-defocus linear both;
            animation-timeline: view();
            animation-range: exit 0% exit 70%;
        }
    }
}
@keyframes stage-defocus {
    from { filter: blur(0); }
    to   { filter: blur(10px); }
}

/* ── What the tiles carry ───────────────────────────────────────────────
 * Eight things the products do, and the brand symbol in the middle. Stroke
 * icons on currentColor, so they take the theme with everything else.
 */
/* The tiles grew, and the icons grew with them into heavy black glyphs that
   fought the logo for attention. They are marks ON the tiles, not the subject
   of them: smaller, lighter, and thinner in the stroke. */
.tile__icon {
    inline-size: 34%;
    block-size: 34%;
    color: var(--ink-faint);
    opacity: 0.8;
    /* Keeps the stroke the same visual weight whatever size the tile is, so a
       phone and a desktop do not get different looking icons. */
    vector-effect: non-scaling-stroke;
    transition: opacity var(--dur) var(--ease), color var(--dur) var(--ease);
}
.stage__object:hover .tile__icon { opacity: 0.95; }

.tile__symbol {
    inline-size: 42%;
    block-size: auto;
}

/* The centre tile is brand blue, so its icon needs the light side of the
   palette rather than the page's text colour. */
.tile--brand .tile__icon { color: var(--brand-ink); opacity: 0.9; }

/* ═══ Preloader ═════════════════════════════════════════════════════════
 * The same surface and the same motion vocabulary as the hero: the page
 * colour underneath, the wordmark, a shimmer crossing it, and a fill that
 * tracks real progress rather than a fixed timer.
 *
 * It is only ever rendered when JavaScript is available, and the layout's
 * inline script tears it down on a hard timeout whatever else happens, so
 * nothing here can leave the page behind a cover.
 */
.preloader {
    /* Hidden unless the document says it is loading, and only JavaScript ever
       says that. So this is a div nobody sees when scripting is off, rather
       than a cover waiting for something to take it away. */
    display: none;
    position: fixed;
    inset: 0;
    z-index: 10000;
    place-items: center;
    background: var(--paper-alt);
    /* Leaves by scaling up and fading, like a lens pulling focus past it. */
    transition: opacity 620ms var(--ease), transform 900ms var(--ease), visibility 0s linear 900ms;
}
.preloader.is-done {
    opacity: 0;
    transform: scale(1.06);
    visibility: hidden;
    pointer-events: none;
}

/* A wash of brand light, the same one the stage uses. */
.preloader::before {
    content: '';
    position: absolute;
    inset-block-start: -20%;
    /* Centred with physical `left`, deliberately. Under inset-inline-start
       this resolves to `right` in Arabic, which put the wash a full width off
       the side of the page, and the hero had no light in it at all. The
       centre of the screen is the same place in both directions. */
    left: 50%;
    inline-size: min(140%, 1000px);
    aspect-ratio: 1.3 / 1;
    transform: translateX(-50%);
    pointer-events: none;
    background: radial-gradient(closest-side,
        color-mix(in srgb, var(--brand) 26%, transparent) 0%,
        transparent 70%);
    opacity: var(--stage-glow, 0.35);
}

.preloader__stage {
    position: relative;
    display: grid;
    justify-items: center;
    gap: var(--space-lg);
}

.preloader__mark {
    position: relative;
    display: block;
    overflow: hidden;
    animation: preloader-breathe 2.6s ease-in-out infinite;
}
@keyframes preloader-breathe {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.035); }
}

.preloader__logo { display: block; block-size: 46px; inline-size: auto; }

/* The shimmer.
 *
 * It used to be a band swept across the mark's box. The box is a rectangle
 * and the wordmark inside it is not, so the light appeared on empty space
 * either side of the letters, which reads exactly like a pane of glass nobody
 * can see. The stops made it worse: nothing to full colour inside a tenth of
 * the width is a cut edge however fast it goes past.
 *
 * The band is now masked by the wordmark itself, so light only lands where
 * there is ink to catch it, and the falloff is spread over two thirds of the
 * width instead of a tenth. A browser that ignores the mask gets the soft
 * band over the box, which is the old behaviour minus the hard edge. */
.preloader__shimmer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(115deg,
        transparent 0%,
        color-mix(in srgb, var(--brand) 6%,  transparent) 30%,
        color-mix(in srgb, var(--brand) 42%, transparent) 50%,
        color-mix(in srgb, var(--brand) 6%,  transparent) 70%,
        transparent 100%);
    -webkit-mask-image: url('/assets/brand/click-wordmark-dark-text.svg');
            mask-image: url('/assets/brand/click-wordmark-dark-text.svg');
    -webkit-mask-size: contain;
            mask-size: contain;
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    animation: preloader-sweep 2.2s ease-in-out infinite;
}
@keyframes preloader-sweep {
    from { transform: translateX(-130%); }
    to   { transform: translateX(130%); }
}
:root[dir="rtl"] .preloader__shimmer { animation-name: preloader-sweep-rtl; }
@keyframes preloader-sweep-rtl {
    from { transform: translateX(130%); }
    to   { transform: translateX(-130%); }
}

.preloader__track {
    inline-size: 148px;
    block-size: 2px;
    border-radius: 99px;
    background: var(--hover-bg);
    overflow: hidden;
}
.preloader__fill {
    display: block;
    block-size: 100%;
    inline-size: 0%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--brand), var(--accent));
    transition: inline-size 320ms var(--ease);
}

/* The hero waits its turn. Paused from the first frame, so the arrival starts
   when the cover lifts instead of playing behind it. */
html.is-loading .preloader { display: grid; }

/* The hero waits its turn. Paused from the first frame, so the arrival starts
   when the cover lifts instead of playing behind it. */
html.is-loading .tile { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
    .preloader__mark, .preloader__shimmer { animation: none; }
    .preloader { transition: opacity 200ms linear, visibility 0s linear 200ms; }
    .preloader.is-done { transform: none; }
}

/* ═══ Partners ══════════════════════════════════════════════════════════
 * A label, then a row of marks that change in place.
 *
 * Nothing slides sideways. The whole line turns over at once: the marks on
 * screen leave together and the next set arrives together, so the strip reads
 * as one row of names being replaced rather than as individual logos taking
 * turns.
 *
 * Each slot is a window holding the whole pool, showing one. The script moves
 * a class between them and does nothing else, so the row is already complete
 * before any script runs and stays complete if none ever does.
 */
.partners {
    /* Tight at both ends. The hero already finishes with its own clearance and
       the section below opens with its own, so a full band here and another
       one there stacked up into a gap you had to scroll through. */
    padding-block: var(--space-md) var(--space-lg);
}

.partners__inner {
    display: flex;
    align-items: center;
    gap: clamp(var(--space-lg), 4vw, var(--space-3xl));
}

/* The label sits on the reading-start side, in line with the row, the way a
   caption sits beside a list rather than on top of it. */
.partners__label {
    flex: 0 0 auto;
    margin: 0;
    font-size: var(--step--1);
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--ink-faint);
}
/* Arabic has no case, and letter-spacing breaks the joins between letters, so
   the treatment that makes the Latin label look considered makes the Arabic
   one look broken. */
:root[lang="ar"] .partners__label {
    text-transform: none;
    letter-spacing: 0;
    font-size: var(--step-0);
    font-weight: 700;
    color: var(--ink-muted);
}

.partners__row {
    flex: 1 1 auto;
    display: grid;
    grid-template-columns: repeat(var(--slots, 5), 1fr);
    align-items: center;
    gap: clamp(var(--space-sm), 2vw, var(--space-xl));
    margin: 0;
    padding: 0;
    list-style: none;
    min-inline-size: 0;
}

/* The window. Marks leave through the top of it and arrive through the
   bottom, so the overflow has to be hidden or they are visible travelling. */
.partners__slot {
    position: relative;
    display: grid;
    place-items: center;
    block-size: 60px;
    overflow: hidden;
}

.partners__logo {
    grid-area: 1 / 1;
    max-inline-size: 100%;
    /* Each mark carries its own height, and the whole set scales together on
       a narrow screen so three still fit where two used to. */
    block-size: calc(var(--logo-h, 39px) * var(--logo-scale, 1));
    inline-size: auto;
    object-fit: contain;
    /* Waiting off stage, below the window. */
    opacity: 0;
    transform: translateY(78%);
    filter: blur(5px);
    pointer-events: none;
    transition: opacity 520ms var(--ease),
                transform 520ms var(--ease),
                filter 520ms var(--ease);
    /* No stagger. Every mark in the row starts and finishes at the same
       instant, because the whole row changing together is the point.
       A fifty millisecond delay per slot spread the change over about three
       quarters of a second once the transition itself was counted, and at
       that length the eye stops reading it as one line turning over and
       starts reading it as five logos taking turns, which is exactly what it
       was meant to stop doing. */
    transition-delay: 0ms;
}
.partners__logo.is-current {
    opacity: 0.72;
    transform: none;
    filter: none;
}
/* On its way out, through the top. Without this the outgoing mark would drop
   back down to where it waits, and the two would cross in the window. */
.partners__logo.is-leaving {
    opacity: 0;
    transform: translateY(-78%);
    filter: blur(5px);
}
.partners__slot:hover .partners__logo.is-current { opacity: 1; }

.partners__note {
    margin-block-start: var(--space-lg);
    text-align: center;
    font-size: var(--step--1);
    color: var(--ink-muted);
}

/* ── Narrower ───────────────────────────────────────────────────────────
 * Slots come off the end rather than shrinking: four logos with room to read
 * beats six squeezed. The markup always ships five; these hide the surplus.
 */

/* Slots come off the end rather than shrinking: four marks with room to read
   beats six squeezed. The markup always ships five; these hide the surplus. */
@media (max-width: 1100px) { .partners__row { --slots: 4; } .partners__slot:nth-child(n + 5) { display: none; } }
@media (max-width: 860px)  { .partners__row { --slots: 3; } .partners__slot:nth-child(n + 4) { display: none; } }

@media (max-width: 680px) {
    /* The label moves above the row: side by side, it takes a third of a
       phone and leaves the marks fighting for the rest. */
    .partners__inner {
        display: grid;
        justify-items: center;
        gap: var(--space-lg);
    }
    .partners__row {
        /* Smaller marks, not fewer of them. The column count comes from how
           many partners there are, so it is the markup's business, not a
           breakpoint's. */
        --logo-scale: 0.82;
        inline-size: 100%;
        gap: var(--space-sm);
    }
    .partners__slot { block-size: 48px; }
    .partners__label { letter-spacing: 0.12em; }
}
@media (max-width: 420px) {
    .partners__row { --slots: 2; --logo-scale: 0.86; }
    .partners__slot:nth-child(n + 3) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    /* The change still happens, it just happens without travel or stagger.
       Someone who asked for less motion should not get a row that never
       changes; they should get the change without the movement. */
    .partners__logo {
        transition: opacity 200ms linear;
        transition-delay: 0ms;
        transform: none;
        filter: none;
    }
    .partners__logo.is-leaving { transform: none; filter: none; }
}

/* ═══ Page sections ═════════════════════════════════════════════════════
 * Everything below the partner strip: what Click is, the three products, and
 * one ask at the end.
 *
 * The page is built out of panels. A section is a big soft-cornered card on
 * the paper rather than a band of full-width text, which is what keeps a long
 * one-page site from reading as a document. Each product panel carries its
 * own colour at low strength, so you can tell the three apart from across the
 * room without any of them shouting.
 *
 * None of these use the .section class, which sets content-visibility: auto.
 * That skips layout for what is off screen, and these sections are measured
 * by scroll-linked animations that want to know their real size.
 */

/* ── Shared furniture ──────────────────────────────────────────────────── */
.eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0 0 var(--space-md);
    font-size: var(--step--1);
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-p, var(--brand));
}
/* Arabic has no case, and letter-spacing breaks the joins between letters. */
:root[lang="ar"] .eyebrow {
    text-transform: none;
    letter-spacing: 0;
    font-size: var(--step-0);
}
/* A short rule before the word, the way a printed standfirst is marked. */
.eyebrow::before {
    content: "";
    inline-size: 22px;
    block-size: 2px;
    border-radius: 2px;
    background: currentColor;
    opacity: 0.55;
}

.section__head {
    max-inline-size: 44rem;
    margin-inline: auto;
    text-align: center;
    margin-block-end: var(--space-2xl);
}
.section__head .eyebrow { justify-content: center; }

.section__title {
    margin: 0 0 var(--space-md);
    font-size: clamp(1.65rem, 1.2rem + 1.9vw, 2.5rem);
    font-weight: 800;
    line-height: 1.06;
    letter-spacing: -0.035em;
    text-wrap: balance;
    color: var(--ink);
}
:root[lang="ar"] .section__title { letter-spacing: 0; line-height: 1.3; }

.section__lede {
    margin: 0;
    font-size: var(--lede);
    line-height: 1.62;
    color: var(--ink-muted);
    text-wrap: pretty;
}

.btn--lg { padding: 0.9rem 1.7rem; font-size: var(--step-0); }

/* ── Entrances ─────────────────────────────────────────────────────────
 * Scroll-linked, transform and filter only. Nothing here decides whether an
 * element can be seen: before its range starts a block sits slightly low and
 * slightly soft, and that is the whole failure mode. A browser without view()
 * timelines gets the finished state at once, which is the right answer rather
 * than a fallback.
 */
@keyframes rise {
    from { transform: translateY(30px); filter: blur(8px); }
    to   { transform: none; filter: none; }
}
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        .section__head,
        .journey__step,
        .journey__pull,
        .cta__inner {
            animation: rise linear both;
            animation-timeline: view();
            animation-range: entry 4% entry 42%;
        }
        /* The stagger is a delay in scroll distance rather than in time, so
           the group arrives as a sequence however fast you scroll. */
        .journey__step { animation-range: entry calc(3% + var(--n) * 5%) entry calc(45% + var(--n) * 5%); }
    }
}

/* ── The idea ─────────────────────────────────────────────────────────
 * Four steps in one block, divided by hairlines. Not four cards.
 *
 * A card draws a frame around each step, and four frames in a row read as a
 * set of tiles rather than as one idea in four parts. A rule between two
 * columns says the same thing with nothing drawn around it, and leaves the
 * type to carry the section.
 */
.journey { position: relative; isolation: isolate; padding-block: var(--space-xl) var(--space-2xl); }

.journey__steps {
    display: grid;
    grid-template-columns: 1fr 1fr;
    margin: 0;
    padding: 0;
    list-style: none;
    /* The rules are the component. Only the internal ones are drawn: a border
       round the outside would be a card again. */
    border-block-start: 1px solid var(--line);
}

.journey__step {
    position: relative;
    display: grid;
    gap: var(--space-sm);
    justify-items: start;
    align-content: start;
    padding: clamp(1.5rem, 3.2vw, 3.2rem);
    border-inline-end: 1px solid var(--line);
    border-block-end: 1px solid var(--line);
}
.journey__step:nth-child(2n) { border-inline-end: 0; }

/* A quiet wash that follows the pointer into a quadrant, so the block
   responds without anything moving. */
.journey__step::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    background: radial-gradient(90% 90% at 50% 0%, var(--brand-soft), transparent 70%);
    transition: opacity 420ms var(--ease);
}
.journey__step:hover::before { opacity: 1; }

.journey__mark {
    display: grid;
    place-items: center;
    inline-size: 44px;
    block-size: 44px;
    border-radius: 13px;
    color: var(--brand);
    background: var(--brand-soft);
}
.journey__mark svg { inline-size: 21px; block-size: 21px; }

/* The step number, set as a quiet counter rather than a badge competing with
   the icon. Logical inset, so it changes sides with the language. */
.journey__num {
    position: absolute;
    inset-block-start: clamp(1.5rem, 3.2vw, 3.2rem);
    inset-inline-end: clamp(1.5rem, 3.2vw, 3.2rem);
    font-size: var(--step-0);
    font-weight: 700;
    line-height: 1;
    color: var(--line-strong);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.04em;
}

.journey__step-title {
    margin: var(--space-xs) 0 0;
    font-size: clamp(1.1rem, 0.95rem + 0.75vw, 1.4rem);
    font-weight: 800;
    line-height: 1.14;
    letter-spacing: -0.025em;
    text-wrap: balance;
    color: var(--ink);
}
:root[lang="ar"] .journey__step-title { letter-spacing: 0; line-height: 1.38; }

.journey__step-body {
    margin: 0;
    font-size: var(--step-0);
    line-height: 1.62;
    color: var(--ink-muted);
    max-inline-size: 34ch;
}

.journey__pull {
    max-inline-size: 36rem;
    margin: var(--space-3xl) auto 0;
    text-align: center;
    font-size: clamp(1.2rem, 0.95rem + 1.05vw, 1.7rem);
    font-weight: 700;
    line-height: 1.28;
    letter-spacing: -0.025em;
    text-wrap: balance;
    color: var(--ink);
}
:root[lang="ar"] .journey__pull { letter-spacing: 0; line-height: 1.5; }

/* ── The ask at the end ────────────────────────────────────────────────
 * On the paper, with the brand light behind it. The coloured slab that was
 * here briefly won the page: it pulled the eye straight past everything above
 * it, which is the wrong job for the last thing you read.
 */
.cta {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding-block: var(--space-3xl);
    text-align: center;
}
.cta__glow {
    position: absolute;
    inset-block-start: 50%;
    /* Physical `left`: inset-inline-start resolves to `right` in Arabic and
       puts a centred wash a full width off the side of the page. The centre
       of the screen is the same place in both directions. */
    left: 50%;
    z-index: -1;
    inline-size: min(130%, 1000px);
    aspect-ratio: 1.8 / 1;
    transform: translate(-50%, -50%);
    pointer-events: none;
    background: radial-gradient(closest-side,
        color-mix(in srgb, var(--brand) 24%, transparent) 0%,
        color-mix(in srgb, var(--brand) 12%, transparent) 40%,
        color-mix(in srgb, var(--brand) 4%,  transparent) 64%,
        transparent 82%);
}
@media (prefers-reduced-motion: no-preference) {
    .cta__glow { animation: cta-drift 16s ease-in-out infinite alternate; }
}
@keyframes cta-drift {
    from { transform: translate(-50%, -50%) scale(1); }
    to   { transform: translate(-50%, -50%) scale(1.14); }
}

.cta__inner { max-inline-size: 40rem; }
.cta .eyebrow { justify-content: center; }
.cta__title {
    margin: 0 0 var(--space-md);
    font-size: clamp(1.65rem, 1.2rem + 1.9vw, 2.5rem);
    font-weight: 800;
    line-height: 1.06;
    letter-spacing: -0.035em;
    text-wrap: balance;
    color: var(--ink);
}
:root[lang="ar"] .cta__title { letter-spacing: 0; line-height: 1.3; }
.cta__lede {
    margin: 0 0 var(--space-xl);
    font-size: var(--lede);
    line-height: 1.62;
    color: var(--ink-muted);
    text-wrap: pretty;
}
.cta__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-sm); }
.cta__note { margin: var(--space-lg) 0 0; font-size: var(--step--1); color: var(--ink-faint); }

/* ── Narrower ──────────────────────────────────────────────────────────── */
@media (max-width: 980px) {
    /* No parallax once the panel sits under the text rather than beside it:
       a block drifting while you read the thing above it is just noise. */
}

@media (max-width: 680px) {
    /* The quadrants become a column. Two of them on a phone are 180 pixels
       wide, which is narrower than the sentences inside them. With one per
       row the vertical rule has nothing to divide, so it goes. */
    .journey__steps { grid-template-columns: 1fr; }
    .journey__step { border-inline-end: 0; }
}

@media (max-width: 600px) {
    .journey { padding-block: var(--space-lg) var(--space-lg); }
    .cta     { padding-block: var(--space-2xl); }
    .section__head { margin-block-end: var(--space-xl); }
    .journey__pull { margin-block-start: var(--space-2xl); }
    .case__row { grid-template-columns: 6.5rem 1fr; }
    .journey__num { inset-block-start: var(--space-md); inset-inline-end: var(--space-md); }
    .journey__step { padding: var(--space-md); }
}

/* ── The colour under everything ───────────────────────────────────────
 * One field, not one per section.
 *
 * Each section used to lay its own wash behind itself. However soft the
 * gradient inside it, the element was still a rectangle, and where the colour
 * was still strong at that rectangle's edge you got a straight horizontal cut
 * across the page. Three sections meant three cuts.
 *
 * This is a single fixed layer behind the whole document, so there is no
 * boundary anywhere to show. It drifts and swells as you scroll, which is
 * what makes the colour under the page change as you move down it, and being
 * fixed it cannot extend the scrollable area however far it is transformed.
 *
 * The colours are the logo's: the blue and the lime off the mark, with the
 * brand purple as the third. Blurred hard, so they read as light rather than
 * as shapes.
 */
.aurora {
    position: fixed;
    /* Oversized on every side so the blur never reveals its own edge. */
    inset: -25%;
    z-index: -1;
    pointer-events: none;
    opacity: 0.85;
    /* Enough blur to turn the gradients into light, not so much that a
       full-screen fixed layer is being re-filtered on every frame. The
       radials are already soft; this only takes the last edge off. */
    filter: blur(34px);
    /* No mask. The colour runs to the very top edge of the page.
       Masking it made the first slice of the page the flat paper colour, to
       match the status bar the browser paints with theme-color. That fixed
       the hard line but produced a different mismatch: a pale band sitting
       above a tinted hero. The status bar is matched from the other
       direction now, by setting theme-color to the colour that is actually
       rendered at the top of the page. */
    background:
        radial-gradient(34% 38% at 16% 20%,
            color-mix(in srgb, var(--brand) 62%, transparent), transparent 66%),
        radial-gradient(30% 34% at 84% 32%,
            color-mix(in srgb, var(--accent) 78%, transparent), transparent 68%),
        radial-gradient(36% 40% at 66% 80%,
            color-mix(in srgb, var(--purple) 58%, transparent), transparent 68%),
        radial-gradient(28% 32% at 26% 90%,
            color-mix(in srgb, var(--sky) 85%, transparent), transparent 66%);
    will-change: transform;
}

@keyframes aurora-drift {
    from { transform: translate3d(-7%, -5%, 0) scale(1); }
    to   { transform: translate3d(7%, 7%, 0) scale(1.3); }
}
@supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
        /* Tied to the document's scroll, so the colour is a function of where
           you are on the page rather than of how long you have been looking
           at it. On a page too short to scroll the timeline resolves at the
           start and the field simply holds still, which is a perfectly good
           background. */
        .aurora {
            animation: aurora-drift linear both;
            animation-timeline: scroll(root);
        }
    }
}
/* Without scroll timelines it still breathes, slowly, so the page is never
   completely static. */
@supports not (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
        .aurora { animation: aurora-drift 30s ease-in-out infinite alternate; }
    }
}

/* ── The strip behind the status bar ───────────────────────────────────
 * A constant wash across the top of the screen, and the one part of the
 * background that does not move.
 *
 * On a phone the browser paints the status bar, the notch and the clock with
 * theme-color, which is a single value that cannot animate. The colour field
 * behind the page drifts as you scroll, so the page's own top edge measured
 * anywhere from a lavender at rest to bare paper further down: no single
 * theme-color could match it, and the notch always looked like it belonged to
 * a different page than the hero under it.
 *
 * This pins the top of the viewport to one colour whatever the field beneath
 * is doing, and theme-color in the layout is set to that same value. It fades
 * out over a third of the screen, so it reads as the sky at the top of the
 * page rather than as a band with an edge.
 */
:root { --sky-top: #dfe0f0; }

.skyline {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 34vh;
    z-index: -1;
    pointer-events: none;
    /* Extends into the safe area, so on a phone that draws the page under the
       status bar there is colour up there rather than bare canvas. */
    margin-block-start: calc(env(safe-area-inset-top, 0px) * -1);
    padding-block-start: env(safe-area-inset-top, 0px);
    background-clip: border-box;
    background: linear-gradient(to bottom,
        var(--sky-top) 0%,
        color-mix(in srgb, var(--sky-top) 82%, transparent) 26%,
        color-mix(in srgb, var(--sky-top) 38%, transparent) 60%,
        transparent 100%);
}

/* ── The band under the status bar ─────────────────────────────────────
 * Solid across the very top of the screen, in front of the page rather than
 * behind it.
 *
 * Once you scroll, a phone makes the status bar translucent and whatever
 * happens to be under it shows through: a pair of buttons, half a heading,
 * the top of a logo. The navbar floats as a pill, so it does not cover that
 * strip either, and content kept sliding up past it into the clock.
 *
 * This sits above the page and below the navbar, holds the same colour as
 * theme-color for its first stretch, then fades out. At rest it is
 * indistinguishable from the page it covers; scrolled, it is what the status
 * bar has behind it and what the pill floats on.
 */
.topband {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    /* Deep enough to clear the floating pill, plus whatever the phone
       reserves for the notch. */
    block-size: calc(env(safe-area-inset-top, 0px) + var(--header-h) + var(--space-md));
    /* Under the navbar (100) and the drawer, over everything else. */
    z-index: 90;
    pointer-events: none;
    background: linear-gradient(to bottom,
        var(--sky-top) 0%,
        var(--sky-top) 46%,
        color-mix(in srgb, var(--sky-top) 62%, transparent) 72%,
        transparent 100%);
}
