
/* -------------------- SLIDE CONTAINERS -------------------- */
.slides-container {
    position: relative;
    width: 100vw;
    max-width: 100vw;
}

.slide {
    width: 100vw;
    position: relative;
    max-width: 100vw;
    overflow: hidden;
    height: 100vh;
}

/* -------------------- SLIDE 1 GRIDS -------------------- */
.slide-1-grid-desktop {
    display: grid;
    grid-template-columns: 5% 10% 5% 60% 5% 10% 5%;
    grid-template-rows: max(5%, 36px) 10% 30% 10% 30% 10% max(5%, 36px);
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

.slide-1-grid-mobile {
    display: grid;
    grid-template-columns: 5% 10% 5% 60% 5% 10% 5%;
    /* Rows 3 and 5 trimmed 30% -> 27% so the auto marquee row (row 4) goes from 12% to
       18% of the slide — exactly 1.5x thicker, and proportional at every screen height.
       Rows 3-5 still total 72%, so .s1m-b1v-img (which spans 3/6) is unaffected. */
    grid-template-rows: max(2%, 10px) max(12%, 20px) 27% auto 27% max(12%, 20px) max(2%, 10px);
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

.slide-1-grid-mobile .grid-cell { position: relative; z-index: 1; }

.grid-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    max-width: 100%;
    overflow: hidden;
}

.s1d-2-2-3 { grid-column: 2 / span 2; grid-row: 2; }
.s1d-2-5-6 { grid-column: 5 / span 2; grid-row: 2; }
.s1d-4-1-7 { grid-column: 1 / span 7; grid-row: 4; }

.s1m-2-4   { grid-column: 4; grid-row: 2; }
.s1m-4-1-7 { grid-column: 1 / span 7; grid-row: 4; }
.s1m-6-4   { grid-column: 4; grid-row: 6; }

/* -------------------- SLIDE 2 GRIDS -------------------- */
.big-screen-layout {
    display: grid;
    grid-template-columns: 5% 30% 5% 55% 5%;
    grid-template-rows: 15% 25% 25% 20% 15%;
    width: 100%;
    height: 100%;
    max-width: 100vw;
    box-sizing: border-box;
}

.small-screen-layout {
    display: grid;
    grid-template-columns: 5% 5% 80% 5% 5%;
    /* 1 gutter, 2 carousel, 3 gap, 4 headline, 5 body + arrows, 6 marketplace icons,
       7 gutter. Must total 100 — every track is a share of the slide height, so a set
       that does not add up silently rescales all of them.

       Portrait only by construction: .small-screen-layout is display:none in landscape,
       which runs .big-screen-layout and its own row set entirely separately. */
    grid-template-rows: 5% 50% 5% 12% 15% 8% 5%;
    width: 100vw;
    max-width: 100vw;
    height: 100%;
    box-sizing: border-box;
}

.bs-1-1 { grid-area: 1 / 1 / 2 / 2; }
.bs-1-2 { grid-area: 1 / 2 / 2 / 3; }
.bs-1-3 { grid-area: 1 / 3 / 2 / 4; }
.bs-1-4 { grid-area: 1 / 4 / 2 / 5; }
.bs-1-5 { grid-area: 1 / 5 / 2 / 6; }

.bs-2-1 { grid-area: 2 / 1 / 3 / 2; }
.bs-2-2 { grid-area: 2 / 2 / 3 / 3; }
.bs-2-3 { grid-area: 2 / 3 / 3 / 4; }
.bs-2-4 { grid-area: 2 / 4 / 3 / 5; }
.bs-2-5 { grid-area: 2 / 5 / 3 / 6; }

.bs-3-1 { grid-area: 3 / 1 / 4 / 2; }
.bs-3-2 { grid-area: 3 / 2 / 4 / 3; }
.bs-3-3 { grid-area: 3 / 3 / 4 / 4; }
.bs-3-4 { grid-area: 3 / 4 / 4 / 5; }
.bs-3-5 { grid-area: 3 / 5 / 4 / 6; }

.bs-4-1 { grid-area: 4 / 1 / 5 / 2; }
.bs-4-2 { grid-area: 4 / 2 / 5 / 3; }
.bs-4-3 { grid-area: 4 / 3 / 5 / 4; }
.bs-4-4 { grid-area: 4 / 4 / 5 / 5; }
.bs-4-5 { grid-area: 4 / 5 / 5 / 6; }

.bs-5-1 { grid-area: 5 / 1 / 6 / 2; }
.bs-5-2 { grid-area: 5 / 2 / 6 / 3; }
.bs-5-3 { grid-area: 5 / 3 / 6 / 4; }
.bs-5-4 { grid-area: 5 / 4 / 6 / 5; }
.bs-5-5 { grid-area: 5 / 5 / 6 / 6; }

.bs-carousel-combined { grid-area: 2 / 4 / 5 / 5; }
/* Column 2 only, with the row left-aligned inside it — see .bs-icons-aligned .icon-buttons
   below. Narrow landscape widths override both; that block is near the icon sizing tiers. */
.bs-icons-aligned     { grid-area: 4 / 2 / 5 / 3; overflow: visible; z-index: 1; }

.ss-1-1 { grid-area: 1 / 1 / 2 / 2; }
.ss-1-2 { grid-area: 1 / 2 / 2 / 3; }
.ss-1-3 { grid-area: 1 / 3 / 2 / 4; }
.ss-1-4 { grid-area: 1 / 4 / 2 / 5; }
.ss-1-5 { grid-area: 1 / 5 / 2 / 6; }

.ss-2-1 { grid-area: 2 / 1 / 3 / 2; }
.ss-2-2 { grid-area: 2 / 2 / 3 / 3; }
.ss-2-3 { grid-area: 2 / 3 / 3 / 4; }
.ss-2-4 { grid-area: 2 / 4 / 3 / 5; }
.ss-2-5 { grid-area: 2 / 5 / 3 / 6; }

.ss-3-1 { grid-area: 3 / 1 / 4 / 2; }
.ss-3-2 { grid-area: 3 / 2 / 3 / 3; }
.ss-3-3 { grid-area: 3 / 3 / 3 / 4; }
.ss-3-4 { grid-area: 3 / 4 / 3 / 5; }
.ss-3-5 { grid-area: 3 / 5 / 3 / 6; }

.ss-4-1 { grid-area: 4 / 1 / 5 / 2; }
.ss-4-2 { grid-area: 4 / 2 / 5 / 3; }
.ss-4-3 { grid-area: 4 / 3 / 5 / 4; }
.ss-4-4 { grid-area: 4 / 4 / 5 / 5; }
.ss-4-5 { grid-area: 4 / 5 / 5 / 6; }

.ss-5-1 { grid-area: 5 / 1 / 6 / 2; }
.ss-5-2 { grid-area: 5 / 2 / 6 / 3; }
.ss-5-3 { grid-area: 5 / 3 / 6 / 4; }
.ss-5-4 { grid-area: 5 / 4 / 6 / 5; }
.ss-5-5 { grid-area: 5 / 5 / 6 / 6; }

.ss-6-1 { grid-area: 6 / 1 / 7 / 2; }
.ss-6-2 { grid-area: 6 / 2 / 7 / 3; }
.ss-6-3 { grid-area: 6 / 3 / 7 / 4; }
.ss-6-4 { grid-area: 6 / 4 / 7 / 5; }
.ss-6-5 { grid-area: 6 / 5 / 7 / 6; }

.ss-7-1 { grid-area: 7 / 1 / 8 / 2; }
.ss-7-2 { grid-area: 7 / 2 / 8 / 3; }
.ss-7-3 { grid-area: 7 / 3 / 8 / 4; }
.ss-7-4 { grid-area: 7 / 4 / 8 / 5; }
.ss-7-5 { grid-area: 7 / 5 / 8 / 6; }

.ss-carousel      { grid-area: 2 / 3 / 3 / 4; }
.ss-icons-aligned { grid-area: 6 / 3 / 7 / 4; overflow: visible; z-index: 1; }
.ss-arrow-left    { grid-area: 5 / 2 / 6 / 3; }
.ss-arrow-right   { grid-area: 5 / 4 / 6 / 5; }

/* -------------------- SLIDE 3 GRID -------------------- */
#slide-3 {
    display: grid;
    grid-template-columns: 10% 80% 10%;
    grid-template-rows: 10% 20% 0% 45% 10% 10% 5%;
    width: 100vw;
    max-width: 100vw;
}

.s3-1-1 { grid-area: 1 / 1 / 2 / 2; }
.s3-2-2 { grid-area: 2 / 2 / 3 / 3; }
.s3-2-3 { grid-area: 6 / 2 / 7 / 3; }
.s3-3-1 { grid-area: 3 / 1 / 4 / 2; }
.s3-4-2 { grid-area: 4 / 2 / 5 / 3; }
.s3-5-1 { grid-area: 6 / 1 / 7 / 2; }
.s3-6-1 { grid-area: 7 / 1 / 8 / 2; }
.s3-1-3 { grid-area: 1 / 3 / 2 / 4; }
.s3-3-3 { grid-area: 3 / 3 / 4 / 4; }
.s3-5-3 { grid-area: 6 / 3 / 7 / 4; }
.s3-6-3 { grid-area: 7 / 3 / 8 / 4; }

/* Base: desktop-first */
.slide-1-grid-desktop { display: grid; }
.slide-1-grid-mobile  { display: none; }
.big-screen-layout    { display: grid; }
.small-screen-layout  { display: none; }

/* Slide 1 grids — orientation based */
@media screen and (orientation: portrait) {
    .slide-1-grid-desktop { display: none; }
    .slide-1-grid-mobile  { display: grid; }
}

@media screen and (orientation: landscape) {
    .slide-1-grid-desktop { display: grid; }
    .slide-1-grid-mobile  { display: none; }
}

@media screen and (orientation: portrait) {
    #slide-3 { grid-template-rows: 6% 10% max(4%, 10px) 60% max(4%, 10px) 10% 6%; }
}

@media screen and (orientation: landscape) {
    #slide-3 { grid-template-rows: 6% 9% max(2%, 10px) 66% max(2%, 10px) 9% 6%; }
}


/* Slide 2 layouts — orientation based */
@media screen and (orientation: portrait) {
    .big-screen-layout   { display: none; }
    .small-screen-layout { display: grid; }
}

@media screen and (orientation: landscape) {
    .big-screen-layout   { display: grid; }
    .small-screen-layout { display: none; }
}

/* -------------------- FONTS -------------------- */
@font-face {
    font-family: 'BebasNeue';
    src: url('BebasNeue-Regular.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Manrope';
    src: url('Manrope-Regular.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* Slide 2's body copy. OpenType, so format() is 'opentype' — same as Nautilus and
   InconsolataLGC below. Regular cut, so 400 here matches .carousel-body's own
   font-weight and nothing is synthesised. */
@font-face {
    font-family: 'OtfitsGrotesk';
    src: url('otfits-grotesk.regular.otf') format('opentype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'EmbolismSpark';
    src: url('embolism-spark.regular.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
}

@font-face {
    font-family: 'InconsolataLGC';
    src: url('inconsolata-lgc.bold.otf') format('opentype');
    font-weight: bold;
    font-style: normal;
}

@font-face {
    font-family: 'Nautilus';
    src: url('Nautilus.otf') format('opentype');
    font-weight: normal;
    font-style: normal;
}

@font-face {
    font-family: 'Oswald';
    src: url('the-free-font.regular.ttf') format('truetype');
    font-weight: 100;
    font-style: normal;
}


/* -------------------- GLOBAL RESET -------------------- */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
}

button, a, input, label { outline: none; }
button:focus-visible, a:focus-visible { outline: none; }

/* -------------------- AMPERSAND -------------------- */
.ampersand {
    font-family: 'InconsolataLGC', monospace;
    font-weight: bold;
    font-size: 1.0em;
}


/* -------------------- PRELOADER -------------------- */
body.preloader-active {
    overflow: hidden;
    height: 100vh;
    position: fixed;
    width: 100%;
}

.preloader {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: #0b0a0a;
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9999;
    opacity: 1;
    transition: opacity 0.8s ease-out;
}

.preloader.fade-out {
    opacity: 0;
    pointer-events: none;
}

.preloader-content {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Anchor for .preloader-text, which is positioned against THIS box rather than the
       viewport — so its placement beside the logo is pure CSS and survives any resize,
       font swap or breakpoint change without JS recalculating anything.
       THIS BOX DOES NOT MOVE. The slide lives on .preloader-logo-col below, so that only
       the logo travels; the text is anchored here and therefore stays put and can fade in
       on the spot. Putting the transform back here would drag the text along with it. */
    position: relative;
}

.preloader-logo-col {
    display: flex;
    align-items: center;
    /* The slide. Kept on the logo's own column rather than on the content box so the text
       does not ride along — see the note above. The class is still applied to
       .preloader-content by preloader.js; only the element that reacts to it changed, so
       no JS was touched. */
    transition: transform 2.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.preloader-content.slide-left .preloader-logo-col {
    transform: translateX(calc(-1 * var(--slide-offset, 0px)));
}

.preloader-center {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
}

.preloader-bar-track {
    width: 80px;
    height: 2px;
    overflow: hidden;
    opacity: 0;
    transition: opacity 0.4s ease;
    background: rgba(255,255,255,0.2);
}

.preloader-bar-track.visible {
    opacity: 1;
}

.preloader-bar {
    height: 100%;
    width: 0%;
    background: #ffffff;
    border-radius: 1px;
}

.preloader-logo {
    object-fit: contain;
    opacity: 1;
    will-change: transform;
    transform: scale(0.35) translateZ(0);
    -webkit-transform: scale(0.35) translateZ(0);
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    image-rendering: -webkit-optimize-contrast;
}

/* Bloom only — the slide now lives on .preloader-content. Splitting them means no
   pause/resume of a half-finished keyframe, and the bar can take as long as it needs. */
.preloader-logo.logo-animate {
    animation: logoBloom 2.25s ease-in-out forwards;
}

@keyframes logoBloom {
    from { transform: scale(0.35) translateZ(0); }
    to   { transform: scale(1.0)  translateZ(0); }
}

.preloader-text {
    font-family: 'BebasNeue', sans-serif;
    color: #eae7df;
    text-transform: uppercase;
    font-weight: 400;
    white-space: nowrap;
    opacity: 0;
    /* Absolute against .preloader-content, pinned just past the logo's right edge and
       vertically centred on it. Previously this was set from JS in viewport pixels,
       which is what drifted when the font swapped or the window changed. Out of flow,
       so the content box stays logo-width and the logo alone reads centred during bloom.

       The margin subtracts --slide-offset so the text sits at its FINAL position from the
       start and never moves — it only fades. The logo slides left by that same offset, so
       once it lands the gap between the two is exactly --pl-gap and the pair is centred,
       identical to before. Both halves read the same variable, so the composition cannot
       drift: shorten the text and the logo travels less, in step.
       Before --slide-offset is measured it resolves to 0, which puts the text at its
       pre-slide spot — never seen, because .text-visible is only added afterwards and the
       text is opacity 0 until then. */
    position: absolute;
    left: 100%;
    margin-left: calc(var(--pl-gap, 12px) - var(--slide-offset, 0px));
    top: 50%;
    transform: translateY(-50%);
}

.preloader-text.text-visible {
    animation: textFadeIn 0.8s ease-out forwards;
}

@keyframes textFadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* --pl-gap drives both the flex gap and the text's margin-left, so the spacing beside
   the logo stays correct at every breakpoint without JS reading it back. */
@media screen and (min-width: 1025px) {
    .preloader-logo    { width: auto; height: 136px; }
    .preloader-text    { font-size: 45px; letter-spacing: 3px; line-height: 1; }
    .preloader-content { --pl-gap: 15px; gap: var(--pl-gap); }
}
@media screen and (min-width: 701px) and (max-width: 1024px) {
    .preloader-logo    { width: auto; height: 120px; }
    .preloader-text    { font-size: 40px; letter-spacing: 2px; line-height: 1; }
    .preloader-content { --pl-gap: 11px; gap: var(--pl-gap); }
}
@media screen and (max-width: 700px) {
    .preloader-logo    { width: auto; height: 90px; }
    .preloader-text    { font-size: 30px; letter-spacing: 1.5px; line-height: 1; }
    .preloader-content { --pl-gap: 9px; gap: var(--pl-gap); }
}


/* -------------------- SLIDE 1 LOGO TARGET -------------------- */
.logo-target {
    width: 100%; height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

.logo-target-content {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.logo-target-logo { object-fit: contain; }

.logo-target-text {
    font-family: 'BebasNeue', sans-serif;
    color: #eae7df;
    text-transform: uppercase;
    font-weight: 400;
    white-space: nowrap;
}

@media screen and (min-width: 1025px) {
    .logo-target-logo    { width: auto; height: 96px; }
    .logo-target-text    { font-size: 32px; letter-spacing: 2px; line-height: 1; }
    .logo-target-content { gap: 11px; }
}
@media screen and (min-width: 701px) and (max-width: 1024px) {
    .logo-target-logo    { width: auto; height: 88px; }
    .logo-target-text    { font-size: 29px; letter-spacing: 1.8px; line-height: 1; }
    .logo-target-content { gap: 9px; }
}
@media screen and (max-width: 700px) {
    .logo-target-logo    { width: auto; height: 66px; }
    .logo-target-text    { font-size: 22px; letter-spacing: 1.4px; line-height: 1; }
    .logo-target-content { gap: 6px; }
}

/* The -6px nudge existed to optically centre the logo+text pair. The mobile logo has no
   text any more, so it centres on its own. Desktop keeps its text and its nudge. */
#logo-target-mobile .logo-target-content { transform: none; }

/* ---- Corner token (vertical only) ----
   One value drives BOTH the logo's inset and its size, so the mark and its margin scale
   as a single unit instead of drifting apart as independent numbers.

   max(5vw, 2vh) = the larger of the two corner margins (col 1 is 5% of width, row 1 is 2%
   of height), applied to both axes so the gap above and to the left is always equal.

   The clamp is what makes deriving the logo size from this safe: unbounded, the 5vw term
   reaches 41px+ on a tablet and would drag the logo to ~129px. The floor also holds the
   phone size exactly where it is today.

   Defined on .slide-1-grid-mobile, which is display:none in landscape — the desktop logo
   never sees any of this. */
.slide-1-grid-mobile {
    --corner: clamp(21px, max(5vw, 2vh), 27px);
}

#logo-target-mobile {
    position: absolute;
    top: var(--corner);
    left: var(--corner);
    width: auto;
    height: auto;
    z-index: 2;
}

/* The rule: the logo is 3.14x its corner margin. Height only — width stays auto so the
   file's own aspect ratio decides it. Higher specificity than the .logo-target-logo
   breakpoint tiers, so it replaces them here without touching the desktop logo. */
#logo-target-mobile .logo-target-logo {
    height: calc(var(--corner) * 2.50);
    width: auto;
}

.slide-1-grid-desktop .s1d-2-2-3,
.slide-1-grid-desktop .s1d-2-5-6,
.slide-1-grid-mobile .s1m-2-2,
.slide-1-grid-mobile .s1m-2-4,
.slide-1-grid-mobile .s1m-6-4 { overflow: visible; }

/* Vertical only (this cell exists only in .slide-1-grid-mobile).
   Optical centring for BUY NOW. The picture's visible bottom sits exactly on the row-5
   boundary, so the space under it is rows 6+7 — but the button is centred in row 6 alone,
   which reads ~9px high. The correction is the gap between those two centres, which works
   out to exactly half of row 7 at any screen size. Done as a transform rather than by
   spanning the cell into row 7, which would collide with the row-7 cells and reshuffle
   the grid. */
.slide-1-grid-mobile .s1m-6-4 .interested-container {
    transform: translateY(max(1vh, 5px));
}


/* -------------------- INTERESTED BUTTON -------------------- */
.interested-container {
    width: 100%; height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

.interested-btn {
    background: transparent;
    border: none;
    color: #eae7df;
    font-family: 'Oswald', sans-serif;
    cursor: pointer;
    white-space: nowrap;
    font-weight: normal;
    padding: 8px 16px;
}

@media screen and (min-width: 1025px) {
    .interested-btn { font-size: 29px; letter-spacing: 1.9px; }
}
@media screen and (min-width: 701px) and (max-width: 1024px) {
    .interested-btn { font-size: 26px; letter-spacing: 1.3px; }
}
@media screen and (max-width: 700px) {
    .interested-btn { font-size: 19px; letter-spacing: 1px; }
}


/* ==================== RIPPLE LAYER ====================
   Drawn over slide 2's flat field. The #272727 resting fill below is one half of a pair
   with the #171717 in .slide2-bg — see the note there, and move both or neither. */
.ripple-layer {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    /* Not a free number. ripple.js inserts these with bg.after(), so they are positioned
       SIBLINGS of .slide2-bg under #slide-2 — which means anything meant to sit above the
       texture has to be positioned too, since a positioned z-index:0 element paints over
       in-flow content whatever its DOM order. Both arrow sets clear it deliberately, at
       z-index: 10: .carousel-arrows for the big layout, .ss-arrow-left/.ss-arrow-right for
       the small one. Raise this above 10 and both sets disappear under the specks. */
    z-index: 0;
    overflow: visible;
}

.ripple-layer[data-group="0"] { --ripple-color: #64a0ff; }
.ripple-layer[data-group="1"] { --ripple-color: #64dc96; }
.ripple-layer[data-group="2"] { --ripple-color: #ff6478; }

.ripple-speck { fill: #272727; }

/* Specks overlapping the carousel text / icon rows — kept in the DOM so the holes can
   follow the carousel as it flips, but not rendered. */
.rs-hidden { display: none; }

@keyframes speck-ripple {
    0%   { fill: #272727; animation-timing-function: ease-out; }
    35%  { fill: color-mix(in srgb, var(--ripple-color) calc(var(--ripple-intensity, 1) * 100%), #272727); animation-timing-function: ease-in; }
    100% { fill: #272727; }
}

.ripple-speck--animate {
    animation: speck-ripple;
}

.rs-colored, .rs-static {
    --rs-scale: 0.72;
    transform: scale(var(--rs-scale));
    transform-box: fill-box;
    transform-origin: center;
}

/* Portrait added alongside the existing size rules — must stay in step with isMobile()
   in ripple.js, which picks the tile spacing. If one includes portrait and the other
   doesn't, you get small shapes on wide spacing (or the reverse). */
@media (orientation: portrait), (max-width: 699px), (max-height: 499px) {
    .rs-colored, .rs-static { --rs-scale: 0.524; }
}

@keyframes speck-swell {
    0%   { transform: scale(var(--rs-scale)); animation-timing-function: ease-out; }
    35%  { transform: scale(calc(var(--rs-scale) * (1 + 0.18 * var(--ripple-intensity, 1)))); animation-timing-function: ease-in; }
    100% { transform: scale(var(--rs-scale)); }
}

.ripple-speck--animate .rs-colored,
.ripple-speck--animate .rs-static {
    animation-name: speck-swell;
    animation-duration: inherit;
    animation-delay: inherit;
    animation-fill-mode: inherit;
}

/* ==================== MARQUEE ==================== */
.s1d-4-1-7,
.s1m-4-1-7 {
    background-color: rgba(11, 10, 10, 0.85);
    color: #eae7df;
    font-family: 'BebasNeue', sans-serif;
    text-transform: uppercase;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: center;
    transition: opacity 0.6s;
}

.marquee-container {
    width: 100%;
    overflow: hidden;
    white-space: nowrap;
    position: relative;
}

.marquee-track {
    display: inline-flex;
    white-space: nowrap;
    animation: marquee linear infinite;
    will-change: transform;
    transform: translateZ(0);
    -webkit-transform: translateZ(0);
    -webkit-animation: marquee linear infinite;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
}

.marquee-content { flex-shrink: 0; font-size: inherit; letter-spacing: inherit; }

/* Vertical only: marquee text 1.5x. Scaling with em rather than restating a size means
   this rides on top of all ~12 existing font-size breakpoints instead of duplicating
   them. letter-spacing is re-expressed in em (the original ratio is ~font/16 at every
   breakpoint) so it scales with the text instead of staying at its old pixel value.
   .s1m- prefix keeps this off the desktop marquee, which shares .marquee-content.
   Scroll speed self-corrects: marquee.js derives duration from track scrollWidth. */
.s1m-4-1-7 .marquee-content {
    font-size: 1.03em;
    letter-spacing: 0.0625em;
}

.s1d-4-1-7.fade-out,
.s1m-4-1-7.fade-out {
    opacity: 0;
    pointer-events: none;
}

.s1d-4-1-7::before, .s1d-4-1-7::after,
.s1m-4-1-7::before, .s1m-4-1-7::after {
    content: '';
    position: absolute;
    top: 0; height: 100%; width: 100px;
    z-index: 2; pointer-events: none;
}
.s1d-4-1-7::before, .s1m-4-1-7::before {
    left: 0;
    background: linear-gradient(to right, rgba(11,10,10,0.85) 0%, transparent 100%);
}
.s1d-4-1-7::after, .s1m-4-1-7::after {
    right: 0;
    background: linear-gradient(to left, rgba(11,10,10,0.85) 0%, transparent 100%);
}

@media screen and (min-width: 1025px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 57.6px; letter-spacing: 3.6px; }
}
@media screen and (min-width: 701px) and (max-width: 1024px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 41px; letter-spacing: 2.5px; }
}
@media screen and (max-width: 700px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 35px; letter-spacing: 2.2px; }
}

@keyframes marquee {
    0%   { transform: translateX(0); }
    100% { transform: translateX(-50%); }
}


/* ==================== CAROUSEL ==================== */

.carousel-container {
    position: relative;
    width: 100%; height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    perspective: 1200px;
    background: transparent;
}

.carousel-card {
    width: 100%; height: 100%;
    position: relative;
    transform-style: preserve-3d;
    transition: transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    background: transparent;
    transform: rotateY(0.01deg);
}

.carousel-card.flip-right { transform: rotateY(180deg); }
.carousel-card.flip-left  { transform: rotateY(-180deg); }

.carousel-face {
    position: absolute;
    width: 100%; height: 100%;
    backface-visibility: hidden;
    border-radius: 16px;
    overflow: hidden;
    background: transparent;
    transform-style: preserve-3d;
    -webkit-transform-style: preserve-3d;
}

.carousel-face.front { transform: rotateY(0deg); }
.carousel-face.back  { transform: rotateY(180deg); }

.carousel-image {
    width: 100%; height: 100%;
    object-fit: cover;
    border-radius: 16px;
    transform: translate3d(0, 0, 0.1px);
    -webkit-transform: translate3d(0, 0, 0.1px);
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
}

.carousel-arrows {
    position: absolute;
    top: 50%; left: 0; right: 0;
    transform: translateY(-50%);
    display: flex;
    justify-content: space-between;
    padding: 0 20px;
    pointer-events: none;
    z-index: 10;
    max-width: 100%;
    box-sizing: border-box;
}

.carousel-arrow {
    pointer-events: auto;
    border: none;
    background: transparent !important;
    color: white;
    font-size: 32px;
    width: 50px; height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: transform 0.3s ease, opacity 0.3s ease;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
    user-select: none;
    border-radius: 50%;
}
.carousel-arrow:hover { transform: scale(1.1); }
.carousel-arrow:focus,
.carousel-arrow:focus-visible { outline: none; border: none; }

.big-arrows .carousel-arrow { background: none !important; font-size: 32px; padding: 10px; z-index: 20; }

@media screen and (orientation: landscape) {
    .big-arrows { padding-left: 20px !important; padding-right: 20px !important; }
    .big-arrows .carousel-arrow { font-size: 56px !important; padding: 15px !important; }
}

.small-arrow { border-radius: 0 !important; background: transparent !important; width: 100%; height: 100%; font-size: 28px; }
.small-arrow:hover              { background: transparent !important; border-radius: 0 !important; }
/* position+z-index are load-bearing, not cosmetic. COUPLED to .ripple-layer's z-index: 0
   in the RIPPLE LAYER block above — ripple.js inserts those layers with bg.after(), so
   they are POSITIONED SIBLINGS of .slide2-bg inside #slide-2, not children of it. CSS
   paints positioned z-index:0 elements above in-flow non-positioned ones, so while these
   cells and their buttons stayed static, a lit speck painted over the arrow glyph.
   The big layout never had this: .carousel-arrows already clears the layer with z-index: 10.
   10 here is that same number on purpose — the two arrow sets sit at one height. Move one,
   move the other. Text and icons solve the same overlap the other way, via the holes
   computeZones() punches in ripple.js; arrows are not in that selector list and must not
   be, or the texture would gain two dead patches at the screen edges. */
.ss-arrow-left, .ss-arrow-right { border-radius: 0 !important; background: transparent !important;
                                  position: relative; z-index: 10; }

.icon-buttons {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 20px;
    width: 100%; height: 100%;
    opacity: 1; visibility: visible;
    transition: all 0.3s ease;
    max-width: 100%;
    box-sizing: border-box;
}
.icon-buttons.hidden { opacity: 0; visibility: hidden; }

.icon-button {
    max-width: 22%; max-height: 22%;
    width: auto; height: auto;
    object-fit: contain;
    transition: all 0.3s ease;
    border-radius: 4px;
    background: #ffffff;
    padding: 4px;
    box-sizing: border-box;
}
.icon-button:hover { transform: scale(1.1); cursor: pointer; }

.icon-link { cursor: pointer; }

#slide-2 .icon-button {
    box-sizing: border-box;
    transition: transform 0.3s ease;
}
#slide-2 .icon-button:hover,
#slide-2 .icon-button:focus,
#slide-2 .icon-button:focus-visible,
#slide-2 .icon-button:active {
    box-shadow: none !important;
}

/* ---- Icon row entrance / exit ----
   A plain fade, all three together — no stagger, no movement. Opacity stays on the
   container so they can't drift apart. visibility flips instantly on the way in and
   waits out the fade on the way out, so it never cuts the animation short while still
   blocking clicks once hidden. */
#slide-2 .icon-buttons {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.4s ease, visibility 0s;
}
#slide-2 .icon-buttons.hidden {
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.4s ease, visibility 0s 0.4s;
}

/* Left-aligned in the cell, not centred — this is what lines the icons up with the copy
   above them rather than with the middle of column 2.
   The !important that used to be on justify-content here has been dropped: this selector
   is (0,2,0) against the base .icon-buttons rule's (0,1,0), so it already wins on
   specificity alone. Removing it is what lets the narrow-landscape override below take
   over with plain source order instead of needing an !important of its own. */
.bs-icons-aligned .icon-buttons { justify-content: flex-start; padding: 10px; }

/* Below 1480 the icons stop hugging the left of column 2 and instead centre across
   columns 1 and 2 together. Column 1 is the empty 5% gutter, so the pair gives them 35%
   of the viewport to sit in rather than 30% — the row gets breathing room exactly where
   the layout is tightest, and re-centring keeps it from drifting into that gutter.
   Above 1480 there is enough width that the original left alignment still reads correctly
   against the copy, so it is left alone.
   Neither declaration needs !important: both match the base rules' specificity and simply
   come later in the file. */
@media screen and (orientation: landscape) and (max-width: 1479.98px) {
    .bs-icons-aligned              { grid-area: 4 / 1 / 5 / 3; }
    .bs-icons-aligned .icon-buttons { justify-content: center; }
}

@media screen and (orientation: landscape) and (max-width: 1400px) {
    .bs-icons-aligned .icon-button { max-width: min(100px, 12vw); max-height: min(100px, 12vh); }
}
@media screen and (orientation: landscape) and (max-width: 1200px) {
    .bs-icons-aligned .icon-button { max-width: min(80px, 10vw); max-height: min(80px, 10vh); }
}
/* One more step at the narrow end, continuing the 100 -> 80 ladder rather than shrinking
   the tiers above it. Must stay AFTER the 1200 rule: same specificity, so source order is
   what lets it win below 1000.
   The px value is the one that actually bites — the icons are 4.42:1, so max-width always
   binds before max-height, and 8vw only takes over below 750px wide. */
@media screen and (orientation: landscape) and (max-width: 1000px) {
    .bs-icons-aligned .icon-button { max-width: min(60px, 8vw); max-height: min(60px, 8vh); }
}
@media screen and (orientation: portrait) {
    .ss-icons-aligned .icon-button { max-width: 100px; max-height: 100px; }
}
@media screen and (max-width: 440px) {
    .ss-icons-aligned .icon-button { max-width: 60px; max-height: 60px; }
}

.text-content {
    position: absolute;
    width: 100%;
    opacity: 0;
    transform: translateX(-20px);
    transition: all 0.6s ease;
    pointer-events: none;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    color: #fff;
}
.text-content.active {
    opacity: 1;
    transform: translateX(0);
    pointer-events: auto;
}

@media screen and (orientation: landscape) {
    .bs-2-2 { position: relative; overflow: hidden; background: transparent !important; }
    .bs-3-2 { position: relative; overflow: visible; background: transparent !important; }
    .bs-2-2 .text-content { bottom: 0; left: 0; right: 0; height: auto; text-align: left; justify-content: flex-start; }
    /* right is the indent knob for the body copy — it pulls the wrap point in from the
       right edge, so the copy breaks earlier while its left edge stays flush with the
       headline in .bs-2-2 above.
       width:auto is load-bearing — the base .text-content rule sets width:100%, which
       over-constrains an element that also has left and right, so the browser drops
       `right` and the indent does nothing at all. Releasing the width lets left/right
       define the box. */
    .bs-3-2 .text-content { top: 0; left: 0; right: 60px; width: auto; height: auto; text-align: left; justify-content: flex-start; }
}
@media screen and (orientation: portrait) {
    .ss-4-3, .ss-5-3 { position: relative; align-items: left; overflow: hidden; background: transparent !important; }
    .ss-4-3 .text-content { top: 0; height: 100%; align-items: center; justify-content: flex-end; text-align: center; }
    .ss-5-3 .text-content { top: 0; height: 100%; align-items: center; justify-content: center; text-align: center; }
    /* No slide-in in portrait — text is centered so translateX causes it to bleed outside the bg box */
    .ss-4-3 .text-content,
    .ss-5-3 .text-content { transform: none; transition: opacity 0.6s ease; }
    .ss-4-3 .text-content.active,
    .ss-5-3 .text-content.active { transform: none; }
}

.carousel-headline {
    font-family: 'BebasNeue', sans-serif;
    font-size: 32px;
    margin: 0 0 25px 0;
    font-weight: normal;
    letter-spacing: 1px;
    line-height: 1.2;
    color: #eae7df;
    text-transform: uppercase;
}

.carousel-body {
    font-family: 'Manrope', sans-serif;
    font-size: 18px;
    line-height: 1.6;
    margin: 0;
    opacity: 0.9;
    max-width: 100%;
    color: rgba(255, 255, 255, 0.8);
    font-weight: 400;
}


.bs-carousel-combined,
.ss-carousel,
.carousel-container { overflow: visible !important; }

@media screen and (orientation: landscape) {
    .carousel-headline { font-size: 40px; }
    .carousel-body     { font-size: 22px; }
}
@media screen and (orientation: portrait) and (min-width: 701px) {
    .carousel-headline { font-size: 29px; margin-bottom: 0; }
    .carousel-body     { font-size: 19px; }
}
@media screen and (orientation: portrait) and (max-width: 700px) {
    .carousel-headline { font-size: 22px; margin-bottom: 0; }
    .carousel-body     { font-size: 15px; }
}

/* ---- Slide 2 only — small viewport height ---- */
/* Scoped to #slide-2 so slide 1 / marquee are never touched */
/* overflow: visible on text cells so headline is never clipped top/bottom */
@media screen and (max-height: 700px) and (orientation: landscape) {
    #slide-2 .bs-2-2, #slide-2 .bs-3-2 { overflow: visible; }
    #slide-2 .carousel-headline { font-size: 30px; margin-bottom: 0px; }
    #slide-2 .carousel-body     { font-size: 17px; }
}
@media screen and (max-height: 600px) and (orientation: landscape) {
    #slide-2 .carousel-headline { font-size: 26px; margin-bottom: 0px; }
    #slide-2 .carousel-body     { font-size: 15px; }
}
@media screen and (max-height: 500px) and (orientation: landscape) {
    #slide-2 .carousel-headline { font-size: 22px; margin-bottom: 0px; }
    #slide-2 .carousel-body     { font-size: 13px; }
}

/* ---- Slide 2 big-screen — shrink row 2, grow row 3 by same amount ---- */
@media screen and (orientation: landscape) and (max-height: 700px) {
    .big-screen-layout { grid-template-rows: 15% 18% 32% 20% 15%; }
}
@media screen and (orientation: landscape) and (max-height: 600px) {
    .big-screen-layout { grid-template-rows: 15% 14% 36% 20% 15%; }
}
@media screen and (orientation: landscape) and (max-height: 500px) {
    .big-screen-layout { grid-template-rows: 15% 10% 40% 20% 15%; }
}
@media screen and (max-height: 700px) and (orientation: portrait) {
    #slide-2 .ss-4-3, #slide-2 .ss-5-3 { overflow: visible; }
    #slide-2 .carousel-body     { font-size: 13px; }
}

/* ---- Marquee only — small viewport height ---- */
/* Uses marquee-specific classes only, no slide 2 classes touched */
@media screen and (max-height: 700px) and (min-width: 1025px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 38px; letter-spacing: 2.4px; }
}
@media screen and (max-height: 600px) and (min-width: 1025px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 32px; letter-spacing: 2.0px; }
}
@media screen and (max-height: 500px) and (min-width: 1025px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 27px; letter-spacing: 1.7px; }
}
@media screen and (max-height: 400px) and (min-width: 1025px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 21px; letter-spacing: 1.3px; }
}
@media screen and (max-height: 700px) and (min-width: 701px) and (max-width: 1024px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 30px; letter-spacing: 1.9px; }
}
@media screen and (max-height: 600px) and (min-width: 701px) and (max-width: 1024px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 25px; letter-spacing: 1.6px; }
}
@media screen and (max-height: 500px) and (min-width: 701px) and (max-width: 1024px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 21px; letter-spacing: 1.3px; }
}
@media screen and (max-height: 400px) and (min-width: 701px) and (max-width: 1024px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 16px; letter-spacing: 1.0px; }
}
@media screen and (max-height: 700px) and (max-width: 700px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 26px; letter-spacing: 1.6px; }
}
@media screen and (max-height: 600px) and (max-width: 700px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 21px; letter-spacing: 1.3px; }
}
@media screen and (max-height: 500px) and (max-width: 700px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 18px; letter-spacing: 1.1px; }
}
@media screen and (max-height: 400px) and (max-width: 700px) {
    .s1d-4-1-7, .s1m-4-1-7 { font-size: 14px; letter-spacing: 0.9px; }
}

:root {
    --shadow-blue:  rgba(100, 150, 255, 0.15);
    --shadow-green: rgba(100, 220, 150, 0.15);
    --shadow-red:   rgba(255, 100, 120, 0.15);
    --text-blue:    #64a0ff;
    --text-green:   #64dc96;
    --text-red:     #ff6478;
}

.highlight-blue  { color: var(--text-blue); }
.highlight-green { color: var(--text-green); }
.highlight-red   { color: var(--text-red); }

/* .carousel-face[data-index="0"] { box-shadow: 0 25px 60px var(--shadow-blue); } */
/* .carousel-face[data-index="1"] { box-shadow: 0 25px 60px var(--shadow-green); } */
/* .carousel-face[data-index="2"] { box-shadow: 0 25px 60px var(--shadow-red); } */


/* ==================== SLIDE 3 ==================== */

#slide-3 { background-color: #F3F3F3; position: relative; overflow: hidden; }

.slide3-video {
    grid-area: 3 / 2 / 6 / 3;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
    transition: opacity 0.6s ease;
}

/* Fullscreen typewriter mode — portrait < 400px */
#slide-3.typewriter-fullscreen .slide3-video {
    opacity: 0;
    pointer-events: none;
}
#slide-3.typewriter-fullscreen .s3-4-2 {
    overflow: visible;
}
#slide-3.typewriter-fullscreen .s3-4-2 .typewriter-wrapper {
    aspect-ratio: unset !important;
    padding: 0 32px;
    z-index: 10;
    background: transparent;
    transition: background 1s ease;
}
#slide-3.typewriter-fullscreen.typewriter-dim .s3-4-2 .typewriter-wrapper {
    background: #161616;
}

.slide3-video img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: center center;
    filter: blur(0px);
    transition: filter 2s ease;
}

#slide-3.typewriter-active .slide3-video img {
    filter: blur(5px) brightness(0.3);
}

@media screen and (orientation: portrait) {
    #slide-3.typewriter-active .slide3-video img {
        filter: blur(5px) brightness(0.5);
    }
}

@media screen and (orientation: landscape) {
    .slide3-video {
        grid-area: 4 / 2 / 5 / 3;
        width: auto;
        height: 100%;
        max-width: 100%;
        aspect-ratio: 2.35 / 1;
        align-self: center;
        justify-self: center;
    }
}

@media screen and (orientation: portrait) {
    /* Shrink-wrapping the card to the image was tried here and does NOT work: the image's
       max-height would resolve against a picture whose own height is auto, so it cannot
       constrain anything. At 760x1000 that gave a 608x938 image inside a 608x600 card —
       a 338px overflow. The card therefore keeps a definite box and object-fit: contain
       letterboxes inside it; typewriter.js measures that letterbox once and shares it. */
    .slide3-video {
        grid-area: 4 / 2 / 5 / 3;
        width: auto;
        height: 100%;
        max-width: 100%;
        aspect-ratio: 3 / 4;
        align-self: center;
        justify-self: center;
    }
}

/* Cells sitting on top of the video need z-index > 0 */
#slide-3 .s3-4-2,
#slide-3 .s3-3-1, #slide-3 .s3-3-3,
#slide-3 .s3-5-1, #slide-3 .s3-5-3 {
    position: relative;
    z-index: 1;
}

/* Button cells must sit above the fullscreen wrapper (z-index 10) */
#slide-3 .s3-2-2,
#slide-3 .s3-2-3 {
    position: relative;
    z-index: 20;
}

/* Fullscreen typewriter — buttons fade with opacity */
.typewriter-fs-element {
    transition: opacity 1s ease !important;
}

/* Remove default centering from all slide 3 cells */
#slide-3 .grid-cell {
    align-items: flex-start;
    justify-content: flex-start;
}
/* Re-apply centering only to the content cells */
#slide-3 .s3-4-2 {
    align-items: stretch;
    justify-content: center;
}
/* Approach the Light — top of cell, horizontally centered */
#slide-3 .s3-2-2 {
    align-items: flex-start;
    justify-content: center;
    overflow: visible;
}
/* Contact — bottom of cell, horizontally centered */
#slide-3 .s3-2-3 {
    align-items: flex-end;
    justify-content: center;
    overflow: visible;
}

.cta {
    background: none;
    border: none;
    color: #161616;
    font-weight: normal;
    text-transform: uppercase;
    font-size: 16px;
    padding: 12px 24px;
    cursor: pointer;
    transition: all 0.3s ease, color 1s ease, -webkit-text-fill-color 1s ease;
    font-family: 'BebasNeue', sans-serif;
    text-decoration: none;
    max-width: 100%;
    white-space: nowrap;
}

@media screen and (min-width: 1025px)                        { .cta { font-size: 32px; } }
@media screen and (min-width: 701px) and (max-width: 1024px) { .cta { font-size: 29px; } }
@media screen and (max-width: 700px)                         { .cta { font-size: 26px; } }

.cta:hover { transform: translateY(-2px); color: #333333; }

.cta.shimmer { position: relative; overflow: hidden; }
.cta.shimmer::before {
    content: '';
    position: absolute;
    top: 50%; left: -100%;
    transform: translateY(-50%);
    width: 100%; height: 1em;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
    animation: shimmer 1.5s ease-in-out;
}
@keyframes shimmer {
    0%   { left: -100%; }
    100% { left: 100%; }
}

.s3-4-2 {
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent !important;
    position: relative;
    overflow: visible;
    max-width: 100%;
}

/* Wrapper is positioned by JS to exactly match the rendered picture */
.s3-4-2 .typewriter-wrapper {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    padding: 0 24px;
}

.s3-4-2 .typewriter-container {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    opacity: 1;
    visibility: visible;
    box-sizing: border-box;
    width: 100%;
}

.typewriter-line {
    font-size: 18px;
    line-height: 1.6;
    font-family: 'EmbolismSpark', cursive;
    color: #f5f0e0;
    font-weight: 500;
    text-shadow: 0 0 7px #161616, 0 0 7px #161616, 0 0 7px #161616, 0 0 7px #161616, 0 0 7px #161616, 0 0 5px #161616, 0 0 3px #161616;
    text-align: left;
    white-space: nowrap;
    margin: 0; padding: 0;
    height: 38.4px;
    display: block;
    opacity: 1;
    transform: translateY(0);
    transition: all 0.3s ease;
    overflow: visible;
    position: relative;
    width: 100%; max-width: 100%;
}
.typewriter-line.visible { opacity: 1; transform: translateY(0); }

.typewriter-cursor::after { content: ''; animation: blink 1s infinite; }
@keyframes blink {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0; }
}

.typewriter-container.fade-out { animation: fadeOut 1.5s ease forwards; }
@keyframes fadeOut {
    0%   { opacity: 1; }
    100% { opacity: 0; }
}

/* typewriter-active no longer changes background — video plays continuously */

@media screen and (min-width: 1025px) { .typewriter-line { font-size: 40px; height: 46.4px; } }
@media screen and (max-width: 1024px) { .typewriter-line { font-size: 28px; height: 28px; } }


/* ==================== SLIDE 3 — PORTRAIT ONLY ====================
   Everything below is scoped to portrait on purpose. Landscape keeps its existing
   behaviour (dimmed background, light text, blurred photo, no card flip). Placed after
   the rules above so these overrides win without needing !important. */
@media screen and (orientation: portrait) {

    /* Approaching the light no longer darkens the slide — the fullscreen text wrapper
       stays clear and the text goes dark instead of light. */
    #slide-3.typewriter-fullscreen .s3-4-2 .typewriter-wrapper,
    #slide-3.typewriter-fullscreen.typewriter-dim .s3-4-2 .typewriter-wrapper {
        background: transparent;
    }

    .typewriter-line {
        color: #171717;
        text-shadow: none;
    }

    /* ---- Picture flip card ----
       No DOM change: the <picture> itself is the card, its <img> is the front face and
       ::after is the blank back. perspective() sits inside the transform so no ancestor
       has to become a 3D context (which would have re-based absolutely positioned
       descendants elsewhere on the slide). */
    .slide3-video {
        position: relative;
        transform-style: preserve-3d;
        -webkit-transform-style: preserve-3d;
        transition: opacity 0.6s ease;
        /* Identity transform held permanently. Without it the resting state is
           `transform: none`, so at the final frame of a flip the card stops being a
           transformed box and reverts to plain painting — the geometry is identical
           (measured: 0.000px), but the rasterisation changes, and that is the 1px twitch
           right as the animation goes static. Both ends are matrix3d now, so nothing
           switches. Must stay in the same function order as .pic-flipped below, or the
           two won't interpolate cleanly. */
        transform: perspective(1200px) rotateY(0deg);
    }

    /* The flip transition is deliberately NOT on by default. If it were, rotating into
       portrait mid-session would animate a flip on its own: pic-flipped is already on the
       slide, so the instant this media query matches, the transform AND its transition
       both come alive together and the card rotates. No JS can suppress that — the resize
       handler is debounced and runs long after the animation has started. With the
       transition parked behind a class that JS only adds for a deliberate flip, an
       orientation change simply lands on the finished state. */
    /* Original easing, just slower — 0.8s -> 1.0s. Ease-in-out was tried here and read
       worse, not better: perspective already makes the card appear to crawl near 0°/180°
       and rush through 90°, so easing both ends on top of that exaggerated it.
       KEEP IN SYNC: the shadow animation below and FLIP_MS in typewriter.js. */
    #slide-3.flip-anim .slide3-video {
        /* box-shadow stays in the list: on the turn-back .flip-shadow is dropped while
           this class is still on, and without it here the shadow would snap off instead
           of fading out mid-flip. */
        transition: transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.6s ease,
                    box-shadow 0.4s ease;
    }

    .slide3-video img {
        backface-visibility: hidden;
        -webkit-backface-visibility: hidden;
        /* The card flips to a blank face, so blurring the photo underneath buys nothing.
           Dropping the filter entirely also keeps the img from becoming a grouping
           element, which is what would flatten the 3D flip. */
        filter: none;
        transition: none;
    }

    #slide-3.typewriter-active .slide3-video img { filter: none; }

    .slide3-video::after {
        content: '';
        position: absolute;
        /* Photo-sized, same as the shadow plate. At inset: 0 this was the full card box —
           opaque, and painted after ::before — so it covered the shadow's spread wherever
           the box overhangs the picture. On a wide portrait the horizontal overhang is
           ~30px against a 24px blur, which swallowed the side shadows completely. */
        inset: var(--pic-inset-y, 0) var(--pic-inset-x, 0);
        background: #F3F3F3;
        transform: rotateY(180deg);
        backface-visibility: hidden;
        -webkit-backface-visibility: hidden;
        border-radius: 4px;
    }

    /* Shadow plate inset to the photo's true rectangle. --pic-inset-* comes from the
       SAME measurement that positions the typewriter wrapper (photoBox() in
       typewriter.js), so there is one letterbox calculation in the project, not two.
       It sits on ::before, not ::after: ::after is the blank face and carries
       backface-visibility: hidden, which made the shadow invisible for the first half of
       every turn. ::before has no such flag, so it is there for the whole rotation and
       foreshortens with the card.
       Two layers, because one uniform blur reads as a glow: a tight contact shadow that
       keeps the card anchored, plus a wide ambient one doing the "lifted off the page"
       work. No X offset — this rotates about Y, so a sideways offset would swing across
       as it turns. */
    .slide3-video::before {
        content: '';
        position: absolute;
        inset: var(--pic-inset-y, 0) var(--pic-inset-x, 0);
        border-radius: 4px;
        pointer-events: none;
        box-shadow:
            0 1px 2px rgba(22, 22, 22, 0),
            0 8px 24px rgba(22, 22, 22, 0);
        transition: box-shadow 0.4s ease;
    }

    /* Resting state once the card has landed. */
    #slide-3.flip-shadow .slide3-video::before {
        box-shadow:
            0 1px 2px rgba(22, 22, 22, 0.14),
            0 8px 24px rgba(22, 22, 22, 0.20);
    }

    /* The turn itself. As the card comes up on edge the contact shadow releases and the
       ambient one spreads and deepens — it reads as being picked up — then both settle
       as it lands. Ends exactly on the resting values above, so there is no jump when
       the animation hands back over.
       Gated on BOTH classes on purpose: dropping .flip-shadow cancels this animation,
       which is what lets the turn-back fade out mid-flip instead of being held here. */
    #slide-3.flip-anim.flip-shadow .slide3-video::before {
        /* Duration mirrors the flip above — the keyframes are percentages, so the
           halfway cut-off tracks the turn automatically. */
        animation: cardTurnShadow 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    }

    /* The way back. Written as its own keyframes rather than `animation-direction:
       reverse`, because reverse also reverses the easing — the shadow would stop tracking
       the card, which plays this curve forwards. Same shapes, opposite order. */
    #slide-3.flip-anim.flip-shadow.flip-back .slide3-video::before {
        animation: cardTurnShadowOut 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    }

    @keyframes cardTurnShadowOut {
        0%   { box-shadow: 0 1px 2px rgba(22, 22, 22, 0.14), 0 8px 24px rgba(22, 22, 22, 0.20); }
        30%  { box-shadow: 0 1px 2px rgba(22, 22, 22, 0.10), 0 16px 36px rgba(22, 22, 22, 0.26); }
        /* Gone by the halfway point, before the photo comes round. */
        50%,
        100% { box-shadow: 0 1px 3px rgba(22, 22, 22, 0),    0 20px 44px rgba(22, 22, 22, 0); }
    }

    /* ---- Going into the wall, and coming back out of it -------------------
       These two replace what used to be a plain box-shadow fade on .flip-shadow being
       added and removed. A fade reads as the card DISSOLVING, because opacity carries no
       depth information — the only thing that says "how far off the wall is this" is the
       blur and the offset. So the AMBIENT layer does all the work: it collapses from
       8px/24px down to almost nothing, which is the whole depth cue.

       The contact layer deliberately does NOT darken on the way in. Darkening it was
       tried and looked wrong for two reasons. It is backwards — a receding object loses
       edge definition, it does not gain it — and this page gives it nowhere to sit: the
       blank face is #F3F3F3 on an #F3F3F3 slide, so the shadow is the ONLY thing
       describing the panel's shape, and a tight high-alpha contact immediately reads as
       a drawn border rather than as contact. It stays at or below its resting alpha and
       softens slightly, so the panel gets LESS certain as it goes in.

       THE GEOMETRY ONLY EVER TRAVELS ONE WAY. The end of the sink keeps the COLLAPSED
       blur and simply drops the alpha to zero — it does not return to the base rule's
       8px/24px. Matching those numbers was tried, on the theory that the class change
       afterwards should be a no-op, and it produced a visible jag: the shadow tightened
       to 6px and then bloomed back out to 24px while fading, so the last thing you saw
       was the shadow spreading outward at the exact moment it should have been leaving.
       Blur on a fully transparent shadow is invisible, so there was never anything to
       match — the handover is seamless either way, and this way the motion never
       reverses direction. cardRise starts from the same collapsed geometry for the same
       reason. */
    @keyframes cardSink {
        /* Lifted. */
        0%   { box-shadow: 0 1px 2px rgba(22, 22, 22, 0.14), 0 8px 24px rgba(22, 22, 22, 0.20); }
        /* Against the wall. The ambient lift has collapsed; the contact has spread a
           little and gone slightly fainter than resting, never harder. */
        55%  { box-shadow: 0 1px 4px rgba(22, 22, 22, 0.12), 0 2px 6px rgba(22, 22, 22, 0.07); }
        /* Through the surface. Blur stays where it was — only the alpha goes. */
        100% { box-shadow: 0 1px 5px rgba(22, 22, 22, 0),    0 1px 4px rgba(22, 22, 22, 0); }
    }

    /* The mirror. Written out rather than run in reverse: reversing also reverses the
       easing, and the emergence should accelerate out of the wall the same way the sink
       decelerates into it. */
    @keyframes cardRise {
        0%   { box-shadow: 0 1px 5px rgba(22, 22, 22, 0),    0 1px 4px rgba(22, 22, 22, 0); }
        45%  { box-shadow: 0 1px 4px rgba(22, 22, 22, 0.12), 0 2px 6px rgba(22, 22, 22, 0.07); }
        100% { box-shadow: 0 1px 2px rgba(22, 22, 22, 0.14), 0 8px 24px rgba(22, 22, 22, 0.20); }
    }

    /* Driven from typewriter.js. KEEP IN SYNC with SHADOW_SINK_MS and SHADOW_RISE_MS
       there — the text start is scheduled off the sink's duration, so a mismatch shows up
       as the text arriving before the card has finished settling.
       The two are NOT the same length on purpose: the sink runs 650ms because it now
       begins the instant the card lands, absorbing the beat that used to sit empty
       between the turn and the sink. The rise keeps its original 400ms and its hold. */
    #slide-3.flip-sink .slide3-video::before {
        animation: cardSink 650ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
    }
    #slide-3.flip-rise .slide3-video::before {
        animation: cardRise 400ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
    }

    @keyframes cardTurnShadow {
        /* Dead until the card passes 90°. While the photo is still facing us the shadow
           traces the card's full box — which is taller than the photo, because
           object-fit: contain letterboxes it — so it read as a white board turning along
           with the picture. Nothing is drawn until the blank face is the one showing. */
        0%,
        50%  { box-shadow: 0 1px 3px rgba(22, 22, 22, 0),    0 20px 44px rgba(22, 22, 22, 0); }
        /* Blank face has arrived and the card is still up on edge: spread and light.
           80%, not 70% — later in the turn, so the shadow has less of the rotation to sit
           through before it settles and reads as arriving with the card rather than ahead
           of it. */
        80%  { box-shadow: 0 1px 2px rgba(22, 22, 22, 0.10), 0 16px 36px rgba(22, 22, 22, 0.26); }
        /* Settled — matches the resting rule exactly so the handover is seamless. */
        100% { box-shadow: 0 1px 2px rgba(22, 22, 22, 0.14), 0 8px 24px rgba(22, 22, 22, 0.20); }
    }

    /* Face down for the whole typewriter session. */
    #slide-3.pic-flipped .slide3-video {
        transform: perspective(1200px) rotateY(180deg);
    }

    /* Fullscreen text mode used to fade the picture out, which would hide the flip
       entirely — keep the card on screen so its blank face backs the text. */
    #slide-3.typewriter-fullscreen .slide3-video { opacity: 1; }
}


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

.contact-container {
    position: relative;
    display: inline-block;
    width: 120px; height: 40px;
    cursor: pointer;
}

.contact-btn, .email-reveal {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    transition: all 0.4s cubic-bezier(0.65, 0, 0.35, 1), color 1s ease, -webkit-text-fill-color 1s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    color: #161616;
    font-size: 18px;
    cursor: pointer;
}

/* A STATIC positioning box — it never animates. The reveal lives on .email-text below.
   It used to carry the scaleX itself, but a transform applies to the whole subtree, so
   the copy button got squeezed in along with the address. The button can't simply move
   out either: the offset that anchors it (`right: 100%` landscape, `top: 100%` portrait)
   is measured against an address whose width depends on the domain, and putting it in
   flow shoves the address off centre. Animating one level down solves both — the button
   stays anchored to a box that never moves. */
.email-reveal {
    font-size: 17px;
    pointer-events: none;
    /* Sized to the address instead of inheriting the button's 120px, so the box tracks
       the text rather than the contact button it replaces. */
    left: 50%;
    width: max-content;
    transform: translateX(-50%);
    transition: none;
    /* Finger, matching the contact button it replaces — it's still a click target that
       toggles back. Not selectable: copying is the copy button's job. */
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
}

/* The reveal itself. */
/* The reveal itself. */
.email-text {
    opacity: 0;
    transform: scaleX(0);
    transition: opacity 0.4s cubic-bezier(0.65, 0, 0.35, 1),
                transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}

/* Absolute, hung off the address — its left edge here, BELOW it in portrait (see the
   portrait block after this rule). Deliberately OUT of the flex flow either way.
   In flow it was a sibling of the text, so the row's width included it and the address
   sat off-centre by half the button. Now .email-reveal sizes to the address alone, stays
   centred by its own translateX(-50%), and the button hangs outside that box: adding,
   renaming, removing or REPOSITIONING it cannot move the address by a single pixel —
   which is what lets portrait move it without a second layout for the text. */
.email-copy {
    /* One knob for the whole button — text, padding, corner and gap all scale together,
       so "15% smaller" is a single number rather than four that can drift apart. Same
       pattern as --tile-w and --corner elsewhere. The 1px border stays 1px on purpose:
       scaling it would land on a sub-pixel and render fuzzy. */
    --copy-scale: 1;
    position: absolute;
    right: 100%;
    margin-right: calc(8px * var(--copy-scale));
    opacity: 0;
    pointer-events: none;
    top: 50%;
    /* Straight box centring — the two text runs land on the same centre axis on their
       own. Measured: the visible ink of "copy" and of the address share a centre to
       within 0.000px at 16px and 0.367px at 18px. No offset, no per-breakpoint numbers. */
    transform: translateY(-50%);
    padding: calc(2px * var(--copy-scale)) calc(6px * var(--copy-scale));
    /* currentColor, so the border is exactly the ink of its own label — a fixed rgba()
       sat under the element's opacity as well, which compounded into a washed-out grey
       that didn't match the text. */
    border: 1px solid currentColor;
    border-radius: calc(4px * var(--copy-scale));
    background: transparent;
    color: inherit;
    cursor: pointer;
    /* Same face as the address, a size down — set piecemeal because the `font`
       shorthand would drag font-size back to inherit along with the family. */
    font-family: inherit;
    font-weight: inherit;
    font-size: calc(0.7em * var(--copy-scale));
    line-height: 1.25;
    white-space: nowrap;
    /* Leaving is quicker than arriving — on the way back it should just get out of the
       way, not linger. The entrance is set on the show state below. */
    transition: opacity 0.3s ease, transform 0.15s ease, background-color 0.2s ease;
    /* NO will-change here, deliberately, and it is worth knowing why it was tried.
       Pinning this button to its own layer looked like the cure for the flicker on click.
       It was not — the toast's opacity transition was — and pinning it caused a second
       problem: this element is a CHILD of .email-reveal, whose centring is
       translateX(-50%) over a max-content box. That box is an odd width, so the translate
       lands on a half pixel. Text painted normally at a fractional offset is fine, but a
       permanent layer inside that subtree keeps the address composited, and composited
       text at a half-pixel offset renders soft — which is why the address came up blurry
       on first reveal and snapped sharp again on any resize.
       The toast carries the pin instead. It sits in .contact-container, outside this
       subtree, so it costs the address nothing. */
}

.email-copy:hover {
    background: rgba(22, 22, 22, 0.07);
}

.email-copy:active { transform: translateY(-50%) scale(0.94); }

/* Sits below the address so it never covers it — ABOVE it in portrait, see the single
   portrait block below. Absolute either way, so showing it can't nudge the address
   sideways. */
.copied-toast {
    position: absolute;
    top: calc(100% - 6px);
    left: 50%;
    width: max-content;
    font-family: 'Manrope', sans-serif;
    font-size: 12px;
    letter-spacing: 0.03em;
    color: #161616;
    opacity: 0;
    pointer-events: none;
    transform: translateX(-50%) translateY(4px);
    transition: opacity 0.25s ease, transform 0.25s ease;
    /* Same fix as on .email-copy, and needed for the same reason. Chrome promotes an
       element to its own compositing layer for the duration of a transition on a
       compositor-animatable property — transform is one, OPACITY IS THE OTHER. This
       toast sits 3px from the address, so every copy was promoting it, running 0.25s,
       then demoting it, and the address a few pixels away got re-rasterised at both
       ends. It never moved; it was redrawn with grayscale instead of subpixel
       antialiasing, which is what read as the address losing focus for an instant.
       Pinning it up front means there is no promote/demote cycle left to cause it.
       Both properties are listed because both are still transitioned in landscape. */
    will-change: opacity, transform;
}

.contact-container.copied .copied-toast {
    opacity: 0.75;
    transform: translateX(-50%) translateY(0);
}

/* -------------------- CONTACT, VERTICAL --------------------
   The copy button is 15% smaller and moves from the left of the address to underneath
   it; the toast moves out of that space to above the address. Only anchors change —
   the button stays absolute inside .email-reveal and the toast stays absolute inside
   .contact-container, so THE ADDRESS DOES NOT MOVE. Everything else (scale knob,
   border, padding, font, the slow drift-in) is shared with landscape and set once.

   This block is LAST in the contact section on purpose, and must stay last. A media
   query adds no specificity, so each rule here is an exact tie with its landscape
   counterpart above — source order is the only thing deciding it. Move this block up
   past .email-copy:active or .copied-toast and those overrides silently stop applying,
   with no warning and no visual clue except on a phone. */
@media screen and (orientation: portrait) {
    .email-copy {
        --copy-scale: 0.92;
        /* Centred WITHOUT a transform: left and right both pinned to 0, an intrinsic
           width, and auto side margins. The absolute box then centres itself inside
           .email-reveal, which is sized to the address — so the button's centre lands on
           the address's centre.
           It used to be left:50% + translateX(-50%). That works too, but it spends the
           transform property on layout, and transform is a single value: anything else
           that wants it — the press feedback below, or any future animation — has to
           remember to re-state the -50% or the button jumps to starting AT the centre
           line instead of straddling it. This form cannot be knocked out that way.
           The margin shorthand also clears the landscape margin-right in one go. */
        left: 0;
        right: 0;
        width: max-content;
        /* No top margin. The VISIBLE gap is not this number: .email-reveal is 40px tall
           and the address's line box is centred in it, so there is already ~9px of dead
           box below the text's ink before this margin adds anything. At 0 the gap reads
           as ~9px, which is the floor for this rule — going tighter means shortening
           .email-reveal, and that moves the address itself.
           A measured version was tried — JS centring the button between the address and
           the bottom of the slide — and it was exact to 0.01px but looked wrong: some 48
           of those 58px are slide 3's empty bottom gutter, so it centred against an edge
           nobody can see. */
        margin: 0 auto;
        top: 100%;
        transform: none;
    }

    /* Now that centring is not a transform, the press feedback is just the scale —
       nothing to re-state, nothing to keep in sync. */
    .email-copy:active { transform: scale(0.94); }

    /* The button now occupies the space under the address, where this used to sit, so
       the toast goes above. Mirrored exactly: `bottom` replaces `top`, and the same
       -6px still tightens the gap toward the address, just from the other side.
       Coupled to .email-copy's `top: 100%` above — if the button ever goes back to the
       side in portrait, this goes back under the address. */
    .copied-toast {
        top: auto;
        bottom: calc(100% - 6px);
        /* Pure fade here — no drift. The landscape version slides 4px on entry, but the
           toast sits only 7px above the address in this layout, and animating `transform`
           promotes it to its own compositing layer for the duration. That re-rasterises
           the text next to it, which Chrome typically redraws with grayscale instead of
           subpixel antialiasing — the address does not move, but it visibly changes
           weight for a moment and reads as a twitch.
           translateY(0) rather than dropping the Y entirely, so this matches the settled
           value in .contact-container.copied exactly: identical transforms at both ends
           means there is nothing for the browser to animate, and no layer gets promoted.
           opacity alone in the transition list for the same reason. */
        transform: translateX(-50%) translateY(0);
        transition: opacity 0.25s ease;
    }
}

.contact-container.show-email .contact-btn  { opacity: 0; transform: scaleX(0); pointer-events: none; user-select: none; }
.contact-container.show-email .email-reveal { pointer-events: auto; }
.contact-container.show-email .email-text   { opacity: 1; transform: scaleX(1); }

/* The button arrives well AFTER the address has settled, on its own slow fade — it no
   longer rides the reveal. Longer hold before it starts and a gentler curve than plain
   `ease`, which brightened too early and made it read as a pop. Coming back it leaves at
   once on the base rule, no delay.
   Only OPACITY is animated here on purpose: putting the entrance on `transform` would
   collide with the press feedback, since that shares the property and the class stays on
   for the whole session. */
.contact-container.show-email .email-copy {
    opacity: 1;
    pointer-events: auto;
    /* Slow and even. cubic-bezier(0.4, 0, 0.2, 1) front-loads the brightness — most of the
       opacity lands in the first third and the rest crawls, which is what read as sharp
       even at half a second. A near-linear curve spreads it evenly across the whole
       duration, so it genuinely drifts in. */
    transition: opacity 1.2s cubic-bezier(0.45, 0.05, 0.55, 0.95) 0.9s,
                transform 0.15s ease,
                background-color 0.2s ease;
}

.contact-btn  { font-family: 'Oswald', sans-serif; font-weight: normal; }
.email-reveal { font-family: 'Manrope', sans-serif; font-weight: 500; }

@media screen and (min-width: 1025px) {
    .contact-btn  { font-size: 22px; }
    .email-reveal { font-size: 18px; }
}
@media screen and (min-width: 701px) and (max-width: 1024px) {
    .contact-btn  { font-size: 20px; }
    .email-reveal { font-size: 16px; }
}
@media screen and (max-width: 700px) {
    .contact-btn  { font-size: 17px; }
    .email-reveal { font-size: 16px; }
}


/* ==================== GLOBAL ==================== */

#slide-1, #slide-2 { position: relative; overflow: hidden; }
#slide-1 { background-color: #161616; }

.s1m-b1v-img {
    grid-column: 1 / span 7;
    /* Starts at row 2 (not 3) so the picture has a higher roof. Row 2 holds the logo,
       which stays on top: cells are z-index 1, this image is z-index 0. */
    grid-row: 2 / 6;
    position: absolute;
    /* 120% wide, pulled back 10%, so the outer 10% on each side hangs off screen and is
       clipped by #slide-1's overflow:hidden. The middle 80% is the safe zone. Still
       object-fit: contain, so the picture simply grows into that wider box instead of
       being capped at the screen width — taller picture, edges sacrificed. */
    width: 120%;
    /* Top edge capped at the MIDDLE of row 2 rather than its top. The box spans rows 2-5
       = 84% of the slide; half of row 2 is 6% of the slide, i.e. 6/84 = 1/14 of the box.
       So drop the top by 1/14 and take the same off the height. */
    top: calc(100% / 14);
    height: calc(100% - 100% / 14);
    object-fit: contain;
    /* Bottom-aligned, not centred. `contain` leaves empty space above and below the
       picture; centring split it in two, which put the picture's visible bottom ~45px
       above the box bottom — an edge CSS can't see, so anything aligned below it (the
       BUY NOW button) looked wrong. Pinning to the bottom makes the visible edge and the
       box edge the same line, and stacks all the slack at the top instead. */
    object-position: center bottom;
    z-index: 0;
    left: -10%;
    /* Matches .hero-bg so the portrait picture gets the same bottom-anchored parallax
       scale as landscape (driven from slide.js). */
    transform-origin: bottom center;
    will-change: transform;
}

.hero-bg {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background-image: url('B1-H.jpg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: auto 100%;
    will-change: transform;
    transform-origin: bottom center;
}

@media screen and (orientation: portrait) {
    .hero-bg { background-image: none; }
}

/* Slide 2's background: one flat field, with the ripple layer drawn on top of it.

   #171717 is deliberately NOT the #161616 used on slides 1 and 3. It is the ground the
   specks' resting #272727 was tuned against, and the two are read as one pair by the
   RIPPLE LAYER block further up — including inside its keyframes, which animate the fill
   away from #272727 and back. Change one without the other and the texture dies.

   #slide-2 carries no background of its own and no isolation:isolate. Both used to be
   here, and the isolation was load-bearing only because of the opaque colour beside it:
   without a stacking context the z-index:-1 layer resolved against .slides-wrapper and
   painted BEFORE #slide-2's own background, which then covered it. With nothing opaque
   painted on the parent there is nothing left to hide behind, so both go. If you ever
   give #slide-2 a background again, the isolation has to come back with it. */
.slide2-bg {
    z-index: -1;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: #171717;
}

.slides-container {
    position: relative;
    width: 100vw; max-width: 100vw;
    height: 100vh;
    overflow-y: scroll;
    scroll-behavior: smooth;
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.slides-container::-webkit-scrollbar { display: none; }

body { overflow: hidden; }

.slide { height: 100dvh; }

.slides-wrapper {
    transition: transform 0.3s ease-out;
    will-change: transform;
}
