/* ============================================================
   BEINC — Our Process Section
   Triangle-reveal entry that pins the previous section (industries)
   while a cream clip-path morphs over it, then a horizontal-scroll
   phase that translates a 5×2 grid of process steps across the
   viewport. Mobile (<768px) falls through to a vertical stack with
   per-element entrance animations instead.
   ============================================================ */

/* ============================================================
   Section-specific tokens — pulled out so they're easy to tweak.
   ============================================================ */
:root {
    --op-col-width: 425px;          /* fixed Figma column */
    --op-intro-width: 40vw;         /* intro left column */
    --op-label-size: 30px;          /* number-square label */
    --op-row-gap: 30px;             /* space between the 2 mid horizontal lines */
    --op-box-pad: 20px;             /* inner padding of main box content area */

    /* Reveal padding — used ONLY by the triangle clip-path during
       Phases A → D. Decoupled from --pad-x/--pad-y so the cream
       shape can grow with a tighter frame around it without
       affecting the section's layout. */
    --op-reveal-pad-x: 1vw;
    --op-reveal-pad-y: 4vh;
}


/* ============================================================
   Process section + reveal target
   ============================================================ */
.tsr-process {
    position: relative;
    /* No background here — the section is "transparent" during the
       triangle reveal so the prev section behind it shows through
       everywhere the cream clip hasn't reached yet. Cream is provided
       by .tsr-clip (the morphing shape) and by .tsr-process-first's
       content once the reveal completes.

       The section provides its own scroll budget for the horizontal
       phase: extra height = horizontal-translate distance. This is
       what lets .tsr-process-first stick at viewport top while the
       user scrolls past it; no ScrollTrigger pin needed on pin 2.

       --op-h-distance is set from JS once the viewport dimensions
       are known. Defaults to 0 here so the section is at least 100vh
       on first paint before the script runs. */
    height: calc(100vh + var(--op-h-distance, 0px));
    min-height: 100vh;

    /* Section-scoped override of the global guide colour. Both
       --guide-color AND --guide-border must be re-declared here.
       CSS custom-property substitution resolves nested var()
       references at the scope where the OUTER property is declared
       — so simply overriding --guide-color wouldn't reach the
       --guide-border composite (declared on :root). Re-declaring
       --guide-border in this scope makes its inner var(--guide-color)
       re-resolve to the local greyish.

       Affects every dashed border in the section (CSS borders via
       --guide-border) and every SVG stroke that inherits currentColor
       from .op-measure (whose color is also greyish). */
    --guide-color: var(--color-greyish);
    --guide-border: var(--guide-width) var(--guide-style) var(--color-greyish);
}

/* Default state: sticky at viewport top while inside .tsr-process's
   scroll range. This replaces ScrollTrigger pin for the horizontal
   scroll phase — sticky doesn't apply a transform on the parent, so
   descendants with position: fixed (specifically the .is-revealing
   override used during the triangle reveal) keep working correctly. */
.tsr-process-first {
    position: sticky;
    top: 0;
    width: 100%;
    height: 100vh;
    overflow: hidden;
}

/* Reveal phase override: while pin 1 is pinning the previous section,
   this class makes .tsr-process-first a viewport-fixed overlay on top
   of the pinned prev section. position: fixed wins over sticky as
   long as the class is present. */
.tsr-process-first.is-revealing {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    z-index: 5;
}

.tsr-clip {
    position: absolute;
    inset: 0;
    background: var(--color-white);
    clip-path: polygon(
        var(--pad-x) calc(100% - var(--pad-y)),
        var(--pad-x) calc(100% - var(--pad-y)),
        var(--pad-x) calc(100% - var(--pad-y)),
        var(--pad-x) calc(100% - var(--pad-y))
    );
    will-change: clip-path;
}

.tsr-content {
    position: absolute;
    inset: 0;
}

/* ── Side-fade vignettes ──────────────────────────────────
   Two cream → transparent gradients pinned to the left and right
   edges of the viewport. They sit INSIDE .tsr-clip (so they're
   masked by the triangle clip-path during the reveal) but OUTSIDE
   .op-stage (so they don't translate horizontally with the content).
   Width matches --pad-x so the fade strips cover exactly the global
   side-padding zone.

   As the horizontal scroll moves .op-stage leftward, content passes
   BENEATH these strips — content visibly dissolves as it enters from
   the right and as it exits on the left.

   Hidden on mobile — there's no horizontal scroll there. */
.tsr-side-fade {
    position: absolute;
    top: 0;
    bottom: 0;
    width: var(--pad-x);
    pointer-events: none;
    z-index: 4;
}
.tsr-side-fade--left {
    left: 0;
    background: linear-gradient(
        to right,
        var(--color-white) 0%,
        rgba(255, 251, 240, 0) 100%
    );
}
.tsr-side-fade--right {
    right: 0;
    background: linear-gradient(
        to left,
        var(--color-white) 0%,
        rgba(255, 251, 240, 0) 100%
    );
}

/* ── Bottom fade ──────────────────────────────────────────
   Same idea as the side fades, rotated 90°. Anchored to the
   viewport bottom inside .tsr-content. Height uses --pad-y
   (10vh) rather than --pad-x — the gradient needs vertical
   distance to read as a soft dissolve; --pad-x (≈ a few %
   of vw) was too thin and looked more like a hard cropped
   edge than a fade.

   IMPORTANT: stops short on the right by --pad-x rather than
   running edge-to-edge. The rightmost grid vline ends up at
   viewport-x = vw − pad-x at the end of horizontal scroll
   (where it lives at progress=1), and the design calls for
   that vline to stay visible all the way to the bottom. By
   capping the fade at right: var(--pad-x), the bottom-right
   pad-x-wide column stays unfaded, which is exactly the
   strip that vline lives in. The right SIDE fade still covers
   the same column (existing behaviour) — only the BOTTOM fade
   is exempted there. */
.tsr-bottom-fade {
    position: absolute;
    left: 0;
    right: var(--pad-x);
    bottom: 0;
    height: var(--pad-y);
    pointer-events: none;
    z-index: 4;
    background: linear-gradient(
        to top,
        var(--color-white) 0%,
        rgba(255, 251, 240, 0) 100%
    );
}


/* ============================================================
   OUR PROCESS — section content
   ============================================================
   Layout overview (desktop):

     ┌───────────────────────────────────────────────────────────┐
     │ ┊ pad ┊                                                    │
     │       ┌───────┬─────────┬─────────┬─────────┬─────────┬──┐│
     │ INTRO │ MAIN  │  empty  │  MAIN   │  empty  │  MAIN   │ vline-end
     │ (left)│  01   │ (diag)  │   03    │ (diag)  │   05    │
     │ ┊     │       │         │         │         │         │
     │ ────  │ ───── │ ─────── │ ─────── │ ─────── │ ─────── │  ← mid lines (2, 30px gap)
     │ ┊     │ empty │  MAIN   │  empty  │  MAIN   │  empty  │
     │       │ (vrt) │   02    │  (h)    │   04    │  (vrt)  │
     │       └───────┴─────────┴─────────┴─────────┴─────────┴──┘
     │ ┊ pad ┊
     └───────────────────────────────────────────────────────────┘

   The whole composition translates together as one block during
   the horizontal phase. .op-stage is the translating wrapper;
   .op-intro and .op-grid are static positions inside it, no
   per-layer parallax, no overlap. .tsr-process-first has
   overflow:hidden so off-screen content clips naturally. */

.op-stage {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    will-change: transform;
}


/* Mobile-only edge line — hidden on desktop. */
.op-mobile-edge-line { display: none; }


/* ── INTRO (left column) ─────────────────────────────────── */
.op-intro {
    position: absolute;
    top: var(--pad-y);
    left: var(--pad-x);
    bottom: var(--pad-y);
    width: var(--op-intro-width);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.op-intro-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-heading);
    font-weight: var(--font-weight-medium);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-tight);
    color: var(--color-black2);
}

.op-intro-eyebrow img {
    width: 14px;
    height: 14px;
    display: block;
    /* Hidden until the desktop entrance plays. JS flips visibility
       on init via gsap.set(). Mobile media query below resets to
       visible. */
    visibility: hidden;
    will-change: transform, opacity;
}
.op-intro-eyebrow span {
    visibility: hidden;
    will-change: transform, opacity;
}

.op-intro-bottom {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    max-width: 90%;
}

.op-intro-heading {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-medium);
    font-size: var(--text-2xl);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-tight);
    color: var(--color-black2);
}

.op-intro-body {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-body);
    line-height: var(--leading-relaxed);
    color: var(--color-black2);
    max-width: 36ch;
}

/* ── BOX LABEL + CONTENT initial states (desktop only) ──
   Hidden until JS GSAP applies the entrance state. The
   `.op-box .op-label` selector intentionally targets ONLY main
   boxes inside the desktop grid; mobile labels live under
   `.op-mobile-box .op-label` and are unaffected. Subhead + body
   parents start opacity:0 (not visibility) so we can split them
   into lines first, then make the parent visible while individual
   lines stay hidden via their own opacity. */
.op-box .op-label,
.op-content-title {
    visibility: hidden;
    will-change: transform, opacity;
}
.op-content-subhead,
.op-content-body {
    opacity: 0;
}
.op-text-line {
    display: block;
    will-change: transform, opacity;
}
/* Empty-box measurement numbers — also hidden until reveal. */
.op-measure-num {
    visibility: hidden;
}


/* ── INTRO ENTRANCE (desktop/tablet only) ────────────────
   Initial-state CSS for the heading "wipe up" reveal and the body
   "fade-up word stagger". `visibility: hidden` prevents a flash
   before JS runs; gsap.set() on init flips visibility to visible
   AND applies the offset/0 opacity start state so GSAP fully owns
   the transforms (avoids stacking with any CSS-defined transform).
   The mobile media query later resets these so the text is always
   visible at <768px (no scroll-driven reveal). */
.op-line-mask {
    display: block;
    overflow: hidden;
    line-height: inherit;
}
.op-line {
    display: block;
    visibility: hidden;
    will-change: transform;
}
.op-word {
    display: inline-block;
    visibility: hidden;
    opacity: 0;
    will-change: transform, opacity;
}


/* ── GRID (right side, 5×2) ──────────────────────────────── */
.op-grid {
    position: absolute;
    top: 0;
    left: calc(var(--pad-x) + var(--op-intro-width));
    width: calc(var(--op-col-width) * 5);
    height: 100%;
}

/* Vertical dashed lines: 6 total at column boundaries. Span FULL
   viewport height (top:0 bottom:0) — they ignore the section's
   top/bottom global padding by design. */
.op-vline {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    border-left: var(--guide-border);
    pointer-events: none;
}
.op-vline:nth-child(1) { left: 0; }
.op-vline:nth-child(2) { left: var(--op-col-width); }
.op-vline:nth-child(3) { left: calc(var(--op-col-width) * 2); }
.op-vline:nth-child(4) { left: calc(var(--op-col-width) * 3); }
.op-vline:nth-child(5) { left: calc(var(--op-col-width) * 4); }
.op-vline:nth-child(6) { left: calc(var(--op-col-width) * 5); }

/* The two middle horizontal dashed lines.
   Anchored to .op-cells (NOT .op-grid) so 50% means the row
   boundary, not the viewport center.

   Upper line:  sits on the last pixel of the top row — i.e.
                the row boundary's pixel. top = 50% - 1px so
                the 1px-tall line occupies that pixel.
   Lower line:  sits on the last pixel of the bottom-row labels.
                Bottom row starts at 50%, labels are 30px tall,
                so the label's last pixel is at 50% + 30px - 1px. */
.op-hline-mid {
    position: absolute;
    left: 0;
    width: 100%;
    height: 1px;
    border-top: var(--guide-border);
    pointer-events: none;
}
.op-hline-mid--upper { top: calc(50% - 1px); }
/* Lower line: 50% (row boundary) + label-size − 1px would be the
   last pixel of a label whose top sits at 50%. But bottom-row boxes
   are shifted UP by 1px (see .op-box--bottom and [data-empty^="b"]
   rules below) so their labels actually start at 50%-1px. We
   subtract another 1px (total -2px) so the lower line sits on the
   labels' new last pixel. */
.op-hline-mid--lower { top: calc(50% + var(--op-label-size) - 2px); }

/* Cells container: lives within the section's vertical padding so
   boxes never touch top/bottom screen edges. CSS Grid defines the
   5×2 box layout. */
.op-cells {
    position: absolute;
    top: var(--pad-y);
    left: 0;
    right: 0;
    bottom: var(--pad-y);
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    grid-template-rows: 1fr 1fr;
}

.op-box {
    position: relative;
}

/* Bottom-row boxes overlap the top row by 1 pixel.
   This makes the row-boundary pixel shared between:
     - the LAST pixel of the top row's boxes
     - the FIRST pixel of the bottom row's boxes
     - the upper mid dashed line
   Consistent visual logic with the rest of the dashed grid where
   1px lines sit on shared cell edges. */
.op-box--bottom,
.op-box--empty[data-empty^="b"] {
    margin-top: -1px;
}


/* ── MAIN BOX — number label ─────────────────────────────── */
.op-label {
    position: absolute;
    top: 0;
    left: 0;
    width: var(--op-label-size);
    height: var(--op-label-size);
    background: var(--color-black2);
    color: var(--color-white);
    font-family: var(--font-heading);
    font-weight: var(--font-weight-medium);
    font-size: 16px;
    letter-spacing: var(--tracking-tight);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;       /* digit can be wider than label, never grow it */
    z-index: 3;             /* on top of internal dashed lines */
}

.op-label > span {
    display: block;
    line-height: 1;
}


/* ── MAIN BOX — internal dashed lines ────────────────────── */
/* Top-row main box has THREE internal lines:
      ─── top edge to right of label
      ─── below-label edge to right of label
        | left edge of content area (below label)
   Bottom-row main box has ONE internal line:
        | left edge of content area (below label) */
.op-mainline {
    position: absolute;
    pointer-events: none;
}

/* All three internal lines originate from the bottom-right INSIDE
   pixel of the number label (overlap the label edge by 1px) rather
   than from the next pixel past it. Without the -1px offset the
   dashed lines feel detached from the label; with it they read as
   if drawn from the corner of the label. */

/* Horizontal lines (top + below-label) — only on top-row main */
.op-mainline--top {
    top: 0;
    left: calc(var(--op-label-size) - 1px);
    right: 0;
    height: 1px;
    border-top: var(--guide-border);
}
.op-mainline--below-label {
    top: calc(var(--op-label-size) - 1px);
    left: calc(var(--op-label-size) - 1px);
    right: 0;
    height: 1px;
    border-top: var(--guide-border);
}
/* Vertical line (left edge of content area) — both rows */
.op-mainline--vleft {
    top: calc(var(--op-label-size) - 1px);
    left: calc(var(--op-label-size) - 1px);
    bottom: 0;
    width: 1px;
    border-left: var(--guide-border);
}
/* Horizontal line at the bottom of the box — bottom-row main only.
   Sits on the box's last pixel (bottom: 0, line is 1px tall via
   border-top). Spans full box width. */
.op-mainline--bottom {
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    border-top: var(--guide-border);
}


/* ── MAIN BOX — content (title + subhead + body) ─────────── */
.op-content {
    position: absolute;
    top: var(--op-label-size);
    left: var(--op-label-size);
    right: 0;
    bottom: 0;
    padding: var(--op-box-pad);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    color: var(--color-black2);
}

.op-content-title {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-medium);
    font-size: var(--text-xl);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-tight);
}

.op-content-bottom {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.op-content-subhead {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-medium);
    font-size: var(--text-sm);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-snug);
}

.op-content-body {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-body);
    line-height: var(--leading-relaxed);
}


/* ── EMPTY BOX — measure lines ───────────────────────────── */
/* Each empty box has its own measure decoration (a 1px dashed line
   with chevrons at both ends, plus a number label that sits offset
   from the line). The line and number are absolutely positioned
   within the box. Variants below for each position. */

.op-measure {
    position: absolute;
    pointer-events: none;
    color: var(--color-greyish);
    font-family: var(--font-body);
    font-size: 12px;
    letter-spacing: var(--tracking-body);
    line-height: 1;
}

/* Horizontal measure: a horizontal dashed line between two
   outward-pointing chevrons, with the number above.

   Anchored between the LOWER mid horizontal dashed line and the
   viewport bottom — NOT centered within the empty box. That range is:
     top:    label-size − 1px (where the lower mid line sits in
             box-local coords; bottom-row empties start at the row
             boundary so this matches the line)
     bottom: −padY (extend past the box bottom to reach viewport
             bottom; box bottom = vh − padY)
   The flex container fills that range; align-items: center puts the
   line + chevrons on its vertical centerline. */
.op-measure--horizontal {
    top: calc(var(--op-label-size) - 1px);
    bottom: calc(-1 * var(--pad-y));
    left: 0;
    right: 0;
    display: flex;
    align-items: center;
}
.op-measure--horizontal .op-measure-line {
    flex: 1 1 auto;
    height: 1px;
    border-top: var(--guide-border);
}
.op-measure--horizontal .op-chevron {
    flex: 0 0 auto;
    display: block;
}
/* Number sits 6px above the centerline (where the line is). */
.op-measure--horizontal .op-measure-num {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, calc(-100% - 6px));
    white-space: nowrap;
}

/* Vertical measure: a vertical dashed line with chevrons at top +
   bottom, with number to the left of midpoint.

   Width must be NON-ZERO so the reveal `clip-path: inset(...)` has a
   real reference box — a 0-width clip region clips the line, chevrons
   AND the number to nothing even at the "fully revealed" end state.
   Width is also wide enough to FULLY CONTAIN the number text (which
   sits to the left of the column midline), otherwise clip-path on
   the parent still clips the number even if it positions correctly
   visually. 100px gives ~50px of room either side of the midline,
   plenty for a 3–4 digit pixel measurement.
   `align-items: center` keeps the line + chevrons on the column
   midline regardless of parent width. */
.op-measure--vertical {
    top: 0;
    left: 50%;
    bottom: 0;
    width: 100px;
    display: flex;
    flex-direction: column;
    align-items: center;
    transform: translateX(-50%);
}
.op-measure--vertical .op-measure-line {
    flex: 1 1 auto;
    width: 1px;
    border-left: var(--guide-border);
}
.op-measure--vertical .op-chevron {
    flex: 0 0 auto;
    display: block;
}
/* Number sits 8px to the left of the line. `right: 50%` anchors the
   number's right edge to the parent's horizontal centre (which is
   also the line's position), then translateX(-8px) pulls it 8px left
   so there's a clean gap. Width-independent — survives parent-width
   tweaks. */
.op-measure--vertical .op-measure-num {
    position: absolute;
    top: 50%;
    right: 50%;
    transform: translate(-8px, -50%);
    white-space: nowrap;
}
/* Vertical-extended: starts at the lower mid horizontal dashed line
   and extends to the bottom of the viewport (ignores bottom global
   padding). Used by the bottom-row col 3 empty box.

   The empty box's local top corresponds to the row-boundary pixel
   (where the upper mid line sits). The lower mid line sits 29px
   below that (label height − 1px overlap). So the measure starts
   at top: 29px.

   The empty box bottom = viewport bottom minus padY. To reach
   viewport bottom we extend past the box by padY. We push bottom
   to a NEGATIVE value equal to padY so the flex container reaches
   all the way down. */
.op-measure--vertical-extended {
    top: calc(var(--op-label-size) - 1px);
    bottom: calc(-1 * var(--pad-y));
}

/* Diagonal measure: an SVG line drawn corner-to-corner of the
   measure container, with separately-positioned chevrons at each
   endpoint. Used by top-row col 2 (BL → TR) and top-row col 4
   (BR → TL).

   The container extends ABOVE the empty box up to viewport top
   (top: −padY, since the empty box starts at padY) so the line
   reaches the column gridline at the very top of the screen, past
   the section's top padding — which is what the design calls for.

   Container shifted by 1px on the left + 1px from bottom so its
   corners land on the exact intersection pixels:
     - LEFT edge sits 1px right of the box's left edge → on top of
       the next pixel after the column gridline (the vline's
       last-right pixel for t2, the vline's first-left pixel
       equivalent for t4 by mirror).
     - BOTTOM edge sits 1px above the box's bottom edge → on top
       of the upper-mid horizontal line's top pixel (which is the
       row-boundary pixel).
   These shifts make the diagonal endpoints land on the clean
   gridline intersections rather than on the box corners. */
.op-measure--diagonal {
    position: absolute;
    top: calc(-1 * var(--pad-y));
    left: 0;
    right: 0;
    bottom: 2px;
}
/* Per-variant horizontal offset so the diagonal's bottom corner
   lands on the gridline intersection pixel:
     t2 line goes BL → TR: bottom corner is at LEFT  → shift left
     t4 line goes BR → TL: bottom corner is at RIGHT → shift right */
[data-empty="t2"] .op-measure--diagonal { left: 2px; }
[data-empty="t4"] .op-measure--diagonal { right: 2px; }
/* Diagonal SVG fills the measure container 1:1 in pixel space.
   Line + chevrons are drawn together by JS at runtime, with the
   SVG's viewBox set to the container's pixel dimensions — that way
   the chevrons can be drawn in SVG units without any non-uniform
   stretching, and the line endpoints land precisely where they
   should without CSS-translate gymnastics. */
.op-measure--diagonal svg.op-measure-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    color: var(--color-greyish);
}
/* Number sits near the diagonal midpoint, offset to the side
   OPPOSITE the rising direction (per the reference screenshot).

   For BOTH t2 and t4 the number sits in the UPPER-side region of
   the empty container — t2 to the upper-left of the midpoint, t4
   to the upper-right. Visually places the number clearly inside
   the half-plane not crossed by the line.

   Typography inherits from .op-measure (12px greyish body). */
.op-measure--diagonal .op-measure-num {
    position: absolute;
    pointer-events: none;
}
/* Number positioned near the diagonal midpoint, offset just to the
   side opposite the rising direction. The diagonal midpoint sits at
   (50%, 50%) of the measure container. We translate the number's
   anchor (its top-left corner) so the number's RIGHT/LEFT edge
   sits ~14px (≈ 1 line-height) away from the midpoint, at roughly
   the same vertical level. */
[data-empty="t2"] .op-measure--diagonal .op-measure-num {
    top: 50%;
    left: 50%;
    transform: translate(calc(-100% - 14px), calc(-100% - 6px));
}
[data-empty="t4"] .op-measure--diagonal .op-measure-num {
    top: 50%;
    right: 50%;
    transform: translate(calc(100% + 14px), calc(-100% - 6px));
}

/* Chevron arrowheads — small CSS triangles built with rotated
   dashes. Rendered as 8x8 SVG inside .op-chevron. */
.op-chevron {
    color: var(--color-greyish);
}


/* ============================================================
   MOBILE (<768px): vertical stack, no pin, no horizontal scroll
   ============================================================ */
@media (max-width: 767px) {
    /* ── MOBILE ENTRANCE INITIAL STATES ─────────────────
       All animated elements start hidden so the JS-driven
       IntersectionObserver entrance can reveal them on viewport
       entry. Failure mode if JS doesn't run: the page renders
       without these elements visible. */
    .op-mobile-edge-line,
    .op-mobile-gap-line,
    .op-mobile-gap-measure {
        clip-path: inset(0% 0% 100% 0%);   /* top→bottom reveal */
        will-change: clip-path;
    }
    .op-mobile-gap-h-measure {
        clip-path: inset(0% 100% 0% 0%);   /* left→right reveal */
        will-change: clip-path;
    }
    .op-mobile-gap-num,
    .op-mobile-gap-h-num,
    .op-mobile-gap-diag,
    .op-mobile-gap-diag-num {
        opacity: 0;
        will-change: opacity;
    }

    /* Section root: drop the desktop's fixed height + min-height so it
       grows naturally to wrap the mobile vertical stack. Without this,
       the section stays locked at 100vh while the stack (5 boxes + 4
       gaps + intro) is several thousand pixels tall — the footer ends
       up overlapping the section and the rest of the stack bleeds past
       it. */
    .tsr-process {
        height: auto;
        min-height: auto;
    }
    .tsr-process-first {
        position: relative;
        height: auto;
        min-height: auto;
    }
    .tsr-clip {
        position: relative;
        inset: auto;
        width: 100%;
        height: auto;
        /* !important so this beats the inline clip-path that the
           desktop JS sets during the triangle reveal — without it,
           the inline value wins and hides the mobile content if a
           user resizes from desktop. */
        clip-path: none !important;
    }
    .tsr-content {
        position: relative;
        inset: auto;
        overflow: visible;
    }
    /* Side fades and bottom fade disabled on mobile — no horizontal
       scroll, no fade story. */
    .tsr-side-fade,
    .tsr-bottom-fade {
        display: none;
    }
    .op-stage {
        position: relative;
        width: 100%;
        height: auto;
        padding: var(--pad-y) var(--pad-x);
        display: flex;
        flex-direction: column;
        /* Mobile: tighter inter-section gap so the first box sits
           closer below the intro text chunk. */
        gap: var(--space-7);
    }

    /* Intro stacks at the top, normal flow. Mobile uses --space-10
       (120px) between the eyebrow and the heading/body chunk —
       significantly more breathing room than desktop, where the
       entire layout is tighter. */
    .op-intro {
        position: relative;
        top: auto;
        left: auto;
        bottom: auto;
        width: 100%;
        background: transparent;
        gap: var(--space-10);
        will-change: auto;
        transform: none !important;
    }
    .op-intro-bottom {
        max-width: 100%;
        /* Heading + body inset by an additional --pad-x so their
           right edge sits at 2 × --pad-x from the viewport (the
           eyebrow above keeps the standard inset). */
        padding-right: var(--pad-x);
    }

    /* Mobile entrance animations: elements start HIDDEN and are
       revealed by JS GSAP tweens triggered on viewport entry. */
    .op-intro-heading,
    .op-intro-body {
        opacity: 0;
        will-change: transform, opacity;
    }

    /* Mobile-only vertical dashed line aligned with the right global
       padding column. Sits inside .op-stage (which has position:
       relative and var(--pad-x) horizontal padding), so right:
       var(--pad-x) lands it on the same column as the right border
       of every .op-mobile-box. Height is measured in JS so the line
       ends exactly on the top of the first mobile box. */
    .op-mobile-edge-line {
        display: block;
        position: absolute;
        top: 0;
        right: var(--pad-x);
        width: 1px;
        height: 0;       /* set by JS — measured at runtime */
        border-left: var(--guide-border);
        pointer-events: none;
    }

    /* Grid converts to a vertical stack of main boxes only */
    .op-grid {
        position: relative;
        top: auto;
        left: auto;
        width: 100%;
        height: auto;
        will-change: auto;
        transform: none !important;
        display: flex;
        flex-direction: column;
        gap: var(--space-9);
    }
    /* Hide grid-level lines/cells on mobile — replaced by per-box
       dashed frames */
    .op-vline,
    .op-hline-mid,
    .op-cells {
        display: none;
    }

    /* Mobile main-box list (separate copies rendered just for mobile).
       No flex `gap` — all inter-box spacing lives inside the
       .op-mobile-gap children, so the dashed gap-line fills the full
       distance between adjacent boxes with no dead space. */
    .op-mobile-stack {
        display: flex;
        flex-direction: column;
        gap: 0;
    }
    .op-mobile-box {
        position: relative;
        /* 20px inner padding everywhere except top + left, which
           include label-size offset so the text content sits clear
           of both internal dashed lines (the horizontal below-label
           line and the vertical vleft line). The -1px offset on the
           label-size component cancels the box's 1px outer border so
           the visible gap between each dashed line and the text is
           exactly 20px. */
        padding: var(--op-box-pad);
        padding-top: calc(var(--op-label-size) + var(--op-box-pad) - 1px);
        padding-left: calc(var(--op-label-size) + var(--op-box-pad) - 1px);
        /* Transparent border preserves layout (children's absolute
           positioning anchors stay the same) but the actual visible
           dashed perimeter is drawn by 4 child .op-mobile-box-edge
           spans so each side can be animated independently with
           clip-path. */
        border: 1px solid transparent;
    }
    /* The 4 outer-border edges. Each is positioned at -1px so its
       1px-thick dashed line lands exactly where the original CSS
       border was painted. Hidden by default via clip-path:
       inset(100%); JS GSAP tweens reveal them on viewport entry. */
    .op-mobile-box-edge {
        position: absolute;
        pointer-events: none;
        clip-path: inset(100%);
        will-change: clip-path;
    }
    .op-mobile-box-edge--top {
        top: -1px;
        left: -1px;
        right: -1px;
        height: 1px;
        border-top: var(--guide-border);
    }
    .op-mobile-box-edge--right {
        top: -1px;
        right: -1px;
        bottom: -1px;
        width: 1px;
        border-right: var(--guide-border);
    }
    .op-mobile-box-edge--bottom {
        bottom: -1px;
        left: -1px;
        right: -1px;
        height: 1px;
        border-top: var(--guide-border);
    }
    .op-mobile-box-edge--left {
        top: -1px;
        left: -1px;
        bottom: -1px;
        width: 1px;
        border-left: var(--guide-border);
    }
    /* Internal mobile-box dashed lines start hidden too. */
    .op-mobile-box .op-mainline--below-label,
    .op-mobile-box .op-mainline--vleft {
        clip-path: inset(100%);
        will-change: clip-path;
    }
    /* Mobile-box content elements start hidden. JS sets the GSAP
       from-state and tweens them in on viewport entry. */
    .op-mobile-box .op-label {
        visibility: hidden;
        will-change: transform, opacity;
    }
    .op-mobile-box-title,
    .op-mobile-box-subhead,
    .op-mobile-box-body {
        opacity: 0;
        will-change: transform, opacity;
    }
    /* Label inset 1px on top and left so its outer edges sit ON TOP
       of the box's outer dashed border — the box's border line at
       the top-left is hidden behind the label, leaving the label's
       bottom and right edges as the visible "corner" of the dashed
       grid in that area. */
    .op-mobile-box .op-label {
        top: -1px;
        left: -1px;
    }
    /* Two internal dashed lines emanating from the label's
       bottom-right inside corner, mirroring the desktop top-row
       main box pattern (--mainline--below-label and --vleft). No
       top horizontal line — the box's outer border already provides
       the top edge. */
    .op-mobile-box .op-mainline--below-label,
    .op-mobile-box .op-mainline--vleft {
        position: absolute;
        pointer-events: none;
    }
    /* The -2px offset (vs desktop's -1px) accounts for the box's
       1px outer border: absolutely-positioned children anchor
       against the inside-border edge, so an extra 1px is subtracted
       to land the line on the label's last pixel row/column from
       the outer box. */
    .op-mobile-box .op-mainline--below-label {
        top: calc(var(--op-label-size) - 2px);
        left: calc(var(--op-label-size) - 2px);
        right: 0;
        height: 1px;
        border-top: var(--guide-border);
    }
    .op-mobile-box .op-mainline--vleft {
        top: calc(var(--op-label-size) - 2px);
        left: calc(var(--op-label-size) - 2px);
        bottom: 0;
        width: 1px;
        border-left: var(--guide-border);
    }
    .op-mobile-box-title {
        font-family: var(--font-heading);
        font-weight: var(--font-weight-medium);
        font-size: var(--text-xl);
        letter-spacing: var(--tracking-tight);
        line-height: var(--leading-tight);
        color: var(--color-black2);
        margin-bottom: var(--space-5);
    }
    .op-mobile-box-subhead {
        font-family: var(--font-heading);
        font-weight: var(--font-weight-medium);
        font-size: var(--text-sm);
        letter-spacing: var(--tracking-tight);
        line-height: var(--leading-snug);
        color: var(--color-black2);
        margin-bottom: var(--space-2);
    }
    .op-mobile-box-body {
        font-family: var(--font-body);
        font-size: var(--text-xs);
        letter-spacing: var(--tracking-body);
        line-height: var(--leading-relaxed);
        color: var(--color-black2);
    }
    /* Inter-box measurement (between adjacent main boxes). The gap
       container's full height IS the visual distance between the
       boxes, so the measure number reports it directly via
       offsetHeight. */
    .op-mobile-gap {
        position: relative;
        /* Was 60px (--space-9). Now absorbs the previous flex-gap on
           either side (2 × --space-7 = 72px) to keep total spacing
           roughly the same: 60 + 72 = 132px. */
        height: calc(var(--space-9) + var(--space-7) * 2);
        display: flex;
        align-items: center;
        justify-content: flex-start;
    }
    /* Dashed line in the gap — sits on the outer-left x-column
       (left: 0 of the gap container = .op-stage's content-left edge
       = global --pad-x from viewport). That's the same column as
       each box's outer-left dashed border, so the line visually
       extends the boxes' OUTER left border seamlessly through the
       inter-box gap. */
    .op-mobile-gap-line {
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        width: 1px;
        border-left: var(--guide-border);
    }
    .op-mobile-gap-num {
        color: var(--color-greyish);
        font-family: var(--font-body);
        /* Mobile measure numbers run 1px smaller than --text-xs. */
        font-size: calc(var(--text-xs) - 1px);
        /* Sit just to the right of the gap-line (which is at left: 0
           of the gap container). 8px breathing room. */
        margin-left: var(--space-2);
    }

    /* Hide the .op-mobile-gap-line on every gap that is NOT the
       first, second, third, or fourth. The line is used on:
          - gap 1 (at outer-left, x=0)
          - gap 2 (repositioned at 70%)
          - gap 3 (repositioned at 30%)
          - gap 4 (at outer-right, mirror of gap 1) */
    .op-mobile-gap:not(.op-mobile-gap--first):not(.op-mobile-gap--second):not(.op-mobile-gap--third):not(.op-mobile-gap--fourth) .op-mobile-gap-line {
        display: none;
    }
    /* Hide the basic .op-mobile-gap-num except on the gaps that have
       a measure line beside it (1 + 4). */
    .op-mobile-gap:not(.op-mobile-gap--first):not(.op-mobile-gap--fourth) .op-mobile-gap-num {
        display: none;
    }

    /* New measure line for the first gap only — vertical dashed line
       positioned at 85% from the gap container's left edge (close to
       but not flush with the right-padding column). Outward-pointing
       chevrons at both endpoints; uses a flex column so the chevron
       SVGs stay 12×6px while the dashed line (flex:1) stretches to
       fill the gap height with zero SVG distortion. */
    .op-mobile-gap-measure {
        position: absolute;
        /* Wrapper is 12px wide with the dashed line at its center.
           Subtracting 6px from 85% places the LINE (not the wrapper
           edge) on the 85% column. */
        left: calc(85% - 6px);
        top: 0;
        bottom: 0;
        width: 12px;
        display: flex;
        flex-direction: column;
        align-items: center;
        /* Chevrons inherit currentColor — keep the grey from globals
           consistent across all mobile measure lines. */
        color: var(--color-greyish);
        pointer-events: none;
    }
    .op-mobile-gap-measure-line {
        flex: 1;
        width: 1px;
        border-left: var(--guide-border);
    }
    .op-mobile-gap-chevron {
        flex: none;
        overflow: visible;
    }

    /* On the first gap: number is repositioned to sit to the LEFT of
       the new measure line, vertically centered. The margin-left
       from the base rule above is overridden. */
    .op-mobile-gap--first .op-mobile-gap-num {
        position: absolute;
        /* Right edge of number sits exactly 4px to the left of the
           measure line. Line is 1px wide centered at 85% of
           container, so its left edge is at 15% from container-right;
           add 4px to put the number's right edge 4px further left. */
        right: calc(15% + 4.5px);
        top: 50%;
        transform: translateY(-50%);
        margin-left: 0;
        white-space: nowrap;
    }

    /* ─── SECOND GAP (between box 02 Define and box 03 Develop) ─── */
    /* The outer-left dashed line is repurposed: position it at 70%
       (same column as the gap-1 measure line is roughly, but at
       exactly 70% per spec). It is a plain dashed line here — no
       chevrons, no number. */
    .op-mobile-gap--second .op-mobile-gap-line {
        left: 70%;
    }

    /* ─── THIRD GAP (between box 03 Develop and box 04 Deliver) ─── */
    /* Plain vertical dashed line repositioned at 30% of the gap
       container's width. No chevrons, no measure number. */
    .op-mobile-gap--third .op-mobile-gap-line {
        left: 30%;
    }

    /* ─── FOURTH GAP (between box 04 Deliver and box 05 Evolve) ─── */
    /* Mirror of gap 1: the outer dashed line sits on the
       OUTER-RIGHT padding column (so it visually continues the
       boxes' right dashed border through the gap). */
    .op-mobile-gap--fourth .op-mobile-gap-line {
        left: auto;
        right: 0;
    }
    /* Vertical measure line at 15% (mirror of 85%) — places the
       dashed line on the 15% column (since wrapper is 12px wide,
       subtracting 6px lands the line center on exactly 15%). */
    .op-mobile-gap--fourth .op-mobile-gap-measure {
        left: calc(15% - 6px);
    }
    /* Number on the RIGHT side of the measure line, 4px gap. Line is
       1px wide centered at 15% from container-left, so its right
       edge is at 15% + 0.5px; add 4px to put the number's left edge
       4px further right. */
    .op-mobile-gap--fourth .op-mobile-gap-num {
        position: absolute;
        left: calc(15% + 4.5px);
        right: auto;
        top: 50%;
        transform: translateY(-50%);
        margin-left: 0;
        white-space: nowrap;
    }
    /* Diagonal measure SVG fills the gap container; viewBox is set
       in pixel-space by JS so chevrons render at exact 4/8 pixel
       sizes regardless of container dimensions. */
    .op-mobile-gap-diag {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        overflow: visible;
        color: var(--color-greyish);
        pointer-events: none;
    }
    .op-mobile-gap-diag-num {
        position: absolute;
        color: var(--color-greyish);
        font-family: var(--font-body);
        /* Mobile measure numbers run 1px smaller than --text-xs. */
        font-size: calc(var(--text-xs) - 1px);
        white-space: nowrap;
        pointer-events: none;
        /* Top/left set inline by JS to land 4px perpendicular to the
           diagonal's midpoint, on the upper-right side. */
    }

    /* Horizontal measure line — only on the second gap. Vertically
       centered, spans from the absolute viewport left edge (negative
       --pad-x relative to the gap container, which itself sits inside
       the .op-stage's --pad-x padding) to exactly the vertical line
       at 70%. Outward chevrons at both endpoints. Uses flex row:
       chevron → dashed line (flex:1) → chevron. */
    .op-mobile-gap-h-measure {
        position: absolute;
        left: calc(0px - var(--pad-x));
        right: 30%;        /* ends exactly at the 70% vertical */
        top: 50%;
        height: 12px;
        transform: translateY(-50%);
        display: flex;
        flex-direction: row;
        align-items: center;
        color: var(--color-greyish);
        pointer-events: none;
    }
    .op-mobile-gap-h-line {
        flex: 1;
        height: 1px;
        border-top: var(--guide-border);
    }
    .op-mobile-gap-h-chevron {
        flex: none;
        overflow: visible;
    }
    /* Measure number — centered horizontally on the line. Line now
       spans from container-x = −pad-x to 70%, so midpoint (in
       container-local coords) = 35% − pad-x/2. Sits just below the
       line. */
    .op-mobile-gap-h-num {
        position: absolute;
        left: calc(35% - var(--pad-x) / 2);
        top: calc(50% + var(--space-2));
        transform: translateX(-50%);
        color: var(--color-greyish);
        font-family: var(--font-body);
        font-size: calc(var(--text-xs) - 1px);
        white-space: nowrap;
    }
}

/* Hide mobile-only markup on desktop+tablet */
@media (min-width: 768px) {
    .op-mobile-stack {
        display: none;
    }
}
/* Hide desktop-only markup on mobile */
@media (max-width: 767px) {
    .op-grid .op-cells {
        display: none;
    }
}
