/* ═══════════════════════════════════════════════════════════════════
   rock.css — shared rock & roll layer
   ═══════════════════════════════════════════════════════════════════

   The palette, the glitch system, the button treatment and the native
   cursors, in one place so every page in the site resolves them from the
   same source instead of each carrying its own drifting copy.

   LINK THIS LAST, after any page's own <style> block. The cursor rules at
   the bottom have to outrank the `cursor: pointer` declarations that page
   components set on their own classes, and source order is what does it.
   ═══════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════
   ROCK & ROLL — palette + glitch system
   ═══════════════════════════════════════════════════════

   Palette. Neon yellow is the primary accent (it replaces the old
   gold); green, blue and burgundy are the secondaries, handed out to
   individual case studies through the per-modal vars those sections
   already declare (--yp, --mz, --tm, --ab, --bk-orange).

   The *-rgb triplets are space-separated so they can be dropped into
   `rgb(var(--x) / <alpha>)` wherever a translucent tint is needed —
   an rgba() hex can't be parameterised, a triplet can. */
:root {
    --rk-black:    #0A0A0A;
    --rk-white:    #FFFFFF;
    --rk-yellow:   #E8FF00;  --rk-yellow-rgb:   232 255 0;
    --rk-green:    #00FF6A;  --rk-green-rgb:    0 255 106;
    --rk-blue:     #2B6BFF;  --rk-blue-rgb:     43 107 255;
    /* Deep burgundy is a fill colour — at 4.5:1 it fails as type on
       near-black, so anything textual uses the lifted tint instead. */
    --rk-burgundy:      #9E1030;  --rk-burgundy-rgb:      158 16 48;
    --rk-burgundy-lite: #E23A60;  --rk-burgundy-lite-rgb: 226 58 96;

    --rk-accent:     var(--rk-yellow);
    --rk-accent-rgb: var(--rk-yellow-rgb);

    /* The two channels an RGB-split is built from. */
    --rk-ch-a: var(--rk-green);
    --rk-ch-b: var(--rk-blue);
}

::selection { background: var(--rk-accent); color: var(--rk-black); }

/* ── Text glitch ────────────────────────────────────────
   Two offset copies of the text, coloured to the split channels and
   sliced with clip-path. `content: attr(data-text)` is what makes the
   copies real text rather than a duplicated element — the data-text
   attribute is filled in by script for anything that doesn't carry it
   in the markup.

   "Smooth" is the whole brief here, so the keyframes sit at rest for
   most of the cycle and the slips stay in the 1–4px range: it reads as
   a signal fault, not a shake. The copies also fade in and out rather
   than popping. */
.glitch {
    position: relative;
    /* inline-block is load-bearing: on a plain inline box the pseudo's
       top:0 anchors to the line box rather than the glyph box, and on
       display type that lands the copies visibly high. The flex elements
       this used to break (gallery arrows) are simply not promoted any
       more, and .rk-btn re-declares inline-flex further down. */
    display: inline-block;

    /* Slip amplitude. A fixed px offset that reads well on a 12px link
       is invisible under 12rem display type, so the keyframes scale
       off this and large type raises it. */
    --rk-slip: 3px;
}
.glitch::before,
.glitch::after {
    content: attr(data-text);
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    pointer-events: none;
    opacity: 0;
    transition: opacity .4s ease;
    /* Painted on top, normal blending, no z-index:-1. Two earlier
       attempts failed here and both are worth not repeating:
       z-index:-1 drops the copy underneath any ancestor that paints a
       background (the hero stacks several) so it never showed; and
       mix-blend-mode:screen washed the copy to white wherever it
       overlapped the cream base text, which on display type is nearly
       the whole glyph — the slip was computing perfectly and was still
       invisible. Opaque copies replace the base inside their clipped
       band, which is what actually reads as a displaced scanline. */
    white-space: inherit;
}
.hero-highlight { color: var(--rk-accent); }
/* Display type carries a proportionally larger slip. */
#vp-hero-headline .glitch { --rk-slip: 12px; }
.glitch::before { color: var(--rk-ch-a); }
.glitch::after  { color: var(--rk-ch-b); }

.glitch:hover::before,
.glitch:focus-visible::before,
.glitch.is-glitching::before {
    animation: rkSlipA 2.6s cubic-bezier(.2,.8,.2,1) infinite;
}
.glitch:hover::after,
.glitch:focus-visible::after,
.glitch.is-glitching::after {
    animation: rkSlipB 2.6s cubic-bezier(.2,.8,.2,1) infinite;
}

/* Always-on variant — the hero words. Longer cycle, calmer, so it can
   run forever without becoming wallpaper noise. */
.glitch-loop::before { animation: rkSlipA 5.4s cubic-bezier(.2,.8,.2,1) infinite; }
.glitch-loop::after  { animation: rkSlipB 5.4s cubic-bezier(.2,.8,.2,1) infinite; }
.glitch-loop { animation: rkHum 5.4s cubic-bezier(.2,.8,.2,1) infinite; }

/* Opacity is keyframed rather than set on the rule. At rest the two
   copies land exactly on the real glyphs, and screen-blending three
   coloured layers there washes the text to near-white — which ate the
   accent colour off the headline. Holding them at 0 between slips
   keeps the base colour intact and makes each slip land harder. */
@keyframes rkSlipA {
    0%, 56%, 78%, 100% { opacity: 0;  transform: translate3d(0,0,0); clip-path: inset(0 0 0 0); }
    60%  { opacity: .9; transform: translate3d(calc(var(--rk-slip) * -1), 1px, 0);  clip-path: inset(14% 0 56% 0); }
    64%  { opacity: .9; transform: translate3d(calc(var(--rk-slip) * .7), -1px, 0); clip-path: inset(48% 0 24% 0); }
    68%  { opacity: .9; transform: translate3d(calc(var(--rk-slip) * -.6), 0, 0);   clip-path: inset(72% 0 6% 0); }
    86%  { opacity: 0; }
    88%  { opacity: .7; transform: translate3d(calc(var(--rk-slip) * .5), 0, 0);    clip-path: inset(30% 0 46% 0); }
    92%  { opacity: 0;  transform: translate3d(0,0,0); clip-path: inset(0 0 0 0); }
}
@keyframes rkSlipB {
    0%, 56%, 78%, 100% { opacity: 0;  transform: translate3d(0,0,0); clip-path: inset(0 0 0 0); }
    60%  { opacity: .9; transform: translate3d(var(--rk-slip), -1px, 0);            clip-path: inset(56% 0 14% 0); }
    64%  { opacity: .9; transform: translate3d(calc(var(--rk-slip) * -.7), 1px, 0); clip-path: inset(20% 0 52% 0); }
    68%  { opacity: .9; transform: translate3d(calc(var(--rk-slip) * .6), 0, 0);    clip-path: inset(4% 0 74% 0); }
    86%  { opacity: 0; }
    88%  { opacity: .7; transform: translate3d(calc(var(--rk-slip) * -.5), 0, 0);   clip-path: inset(44% 0 32% 0); }
    92%  { opacity: 0;  transform: translate3d(0,0,0); clip-path: inset(0 0 0 0); }
}
/* Sub-pixel skew on the real text — just enough to sell the slip. */
@keyframes rkHum {
    0%, 58%, 72%, 100% { transform: skewX(0deg); }
    61%                { transform: skewX(.6deg); }
    66%                { transform: skewX(-.4deg); }
}

/* ── Card glitch ────────────────────────────────────────
   Replaces the old BorderGlow halo on the works banners. Two copies of
   the card's own image (handed over as --rk-img by script) are stacked
   behind a scanline wash; on hover they split to the green and blue
   channels and slice. */
.glitch-card {
    position: relative;
    isolation: isolate;
    border-radius: .75rem;
    overflow: hidden;
    border: 1px solid rgb(var(--rk-accent-rgb) / .14);
    transition: border-color .35s ease;
}
.glitch-card::before,
.glitch-card::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: var(--rk-img);
    background-size: cover;
    background-position: center;
    mix-blend-mode: screen;
    opacity: 0;
    pointer-events: none;
    transition: opacity .35s ease;
    z-index: 2;
}
.glitch-card::before { filter: url(#rk-keep-green); }
.glitch-card::after  { filter: url(#rk-keep-blue); }

.glitch-card:hover { border-color: rgb(var(--rk-accent-rgb) / .55); }
.glitch-card:hover::before {
    opacity: .55;
    animation: rkSlipA 2.6s cubic-bezier(.2,.8,.2,1) infinite;
}
.glitch-card:hover::after {
    opacity: .55;
    animation: rkSlipB 2.6s cubic-bezier(.2,.8,.2,1) infinite;
}
/* Scanline wash — sits above the split copies, only on hover. */
.glitch-card > .rk-scan {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    opacity: 0;
    transition: opacity .35s ease;
    background: repeating-linear-gradient(
        to bottom,
        rgb(var(--rk-accent-rgb) / .07) 0 1px,
        transparent 1px 4px
    );
}
.glitch-card:hover > .rk-scan {
    opacity: 1;
    animation: rkScan 3.2s linear infinite;
}
@keyframes rkScan {
    0%   { background-position: 0 0; }
    100% { background-position: 0 96px; }
}

/* ── Buttons ────────────────────────────────────────────
   One rock treatment for every button on the site: squared off, mono,
   accent hairline, and a fill that wipes in from the left on hover
   while the label runs the text glitch. */
/* The existing button classes are folded into the selector rather than
   having .rk-btn added to every call site — several of these buttons
   live inside case-study markup that is duplicated per pane, and this
   block sits late enough in the sheet to win on source order. */
.rk-btn,
.cv-download-btn,
.cs-back-home {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    min-height: 44px;
    padding: .9rem 2rem;
    border: 1px solid var(--rk-accent);
    border-radius: 0;
    background: transparent;
    color: var(--rk-accent);
    font-family: 'DM Mono', monospace;
    font-size: .62rem;
    letter-spacing: .22em;
    text-transform: uppercase;
    text-decoration: none;
    overflow: hidden;
    isolation: isolate;
    transition: color .3s ease, border-color .3s ease, box-shadow .3s ease;
}
.rk-btn::before,
.cv-download-btn::before,
.cs-back-home::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--rk-accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform .38s cubic-bezier(.16,1,.3,1);
}
.rk-btn:hover,        .rk-btn:focus-visible,
.cv-download-btn:hover, .cv-download-btn:focus-visible,
.cs-back-home:hover,    .cs-back-home:focus-visible {
    color: var(--rk-black);
    /* The offset shadows are the channel split, held on the button
       edge — a chromatic fringe rather than a glow. */
    box-shadow: 0 0 0 1px var(--rk-accent),
                3px 0 0 rgb(var(--rk-green-rgb) / .5),
               -3px 0 0 rgb(var(--rk-blue-rgb) / .5);
    animation: rkBtnSlip .42s cubic-bezier(.2,.8,.2,1) 1;
}
.rk-btn:hover::before,        .rk-btn:focus-visible::before,
.cv-download-btn:hover::before, .cv-download-btn:focus-visible::before,
.cs-back-home:hover::before,    .cs-back-home:focus-visible::before {
    transform: scaleX(1);
}
.rk-btn:focus-visible,
.cv-download-btn:focus-visible,
.cs-back-home:focus-visible { outline: 2px solid var(--rk-white); outline-offset: 3px; }

/* One quick displacement as the fill wipes through — the button
   "catches" rather than pulsing. */
@keyframes rkBtnSlip {
    0%, 100% { transform: translate3d(0,0,0); }
    22%      { transform: translate3d(-2px,1px,0); }
    48%      { transform: translate3d(2px,-1px,0); }
    72%      { transform: translate3d(-1px,0,0); }
}

/* The old gold treatment: soft radius, ambient glow, lift on hover.
   None of it belongs in the rock set. */
.cv-download-btn {
    margin-top: -0.75rem;
    margin-bottom: -0.75rem;
    animation: none;
}
.cv-download-btn:hover { transform: none; }

/* ── Link hover ─────────────────────────────────────────
   Everything clickable that isn't a button lands on the accent and
   picks up a hairline channel split, so the whole site hovers in the
   same language. */
a:hover, .hoverable:hover { color: var(--rk-accent); }

@media (prefers-reduced-motion: reduce) {
    .glitch::before, .glitch::after,
    .glitch-loop, .glitch-loop::before, .glitch-loop::after,
    .glitch-card::before, .glitch-card::after,
    .glitch-card > .rk-scan {
        animation: none !important;
    }
    .glitch-loop::before, .glitch-loop::after { opacity: 0; }
}

/* Splitting channels is a per-frame filter on a full-bleed image; on
   phones that is the difference between 60fps and 20. Drop it. */
@media (pointer: coarse), (hover: none) {
    .glitch-card::before, .glitch-card::after,
    .glitch-card > .rk-scan { display: none; }
}

/* The native cursors that used to live here now ship in cursor.css, so
   pages that should not take this whole palette can still carry them —
   and so an iframed document can link the cursors on their own. Link
   cursor.css after this file. */

/* The old-film grain that used to live here now ships in grain.css, so
   the archived pages can carry it without taking this whole palette. */

/* ── 3D spatial scroll zoom ─────────────────────────────
   Companion to spatial.js, which writes the per-frame transform. The
   perspective has to live on the *wrapper*, not the moving element:
   `perspective` on an element applies to its children, so putting it on
   the image itself would do nothing to that image's own translateZ.

   Keep --rk-perspective and PERSPECTIVE in spatial.js in step — the
   script derives its cover-scale headroom from that number. */
:root { --rk-perspective: 1100px; }

.cs-parallax-wrap,
/* The works banner moves the wrapper itself, so its perspective has to
   come from the card above it — an element's own `perspective` applies
   to its children, never to its own transform. */
.project-card,
.img-zoom-wrap,
.hero-photo-wrap,
.p-wrap,
.video-hero,
.bk-hero,
.yooz-hero,
.hero {
    perspective: var(--rk-perspective);
    transform-style: preserve-3d;
}

.cs-parallax,
.parallax-img-wrapper,
.img-zoom,
.hero-photo,
.p-img,
.bk-hero-bg,
.yooz-hero-bg,
#heroBg {
    transform-origin: center center;
    /* Without this the receding edge of a tilted image shimmers as the
       rasteriser re-samples it each frame. */
    backface-visibility: hidden;
    will-change: transform;
}

/* about-ab's .img-zoom shipped an 0.8s transition for its one-shot
   scale-in. Now that a rAF loop writes transform every frame, that
   transition would make each photo chase the scroll a beat behind. */
.img-zoom { transition: none; }

@media (prefers-reduced-motion: reduce) {
    .cs-parallax, .parallax-img-wrapper, .img-zoom, .hero-photo,
    .p-img, .bk-hero-bg, .yooz-hero-bg, #heroBg {
        transform: none !important;
        will-change: auto !important;
    }
}

/* ── Yellow duotone tint ────────────────────────────────
   Opt-in. Put .rk-tint on an image's *wrapper* (not the <img>) and the
   picture reads as a one-colour poster print in the accent.

   Deliberately not applied to the campaign imagery in the case studies
   or the works cards: those are the client deliverables themselves, and
   recolouring them would misrepresent the work that is being shown. It
   is for editorial photography — the portraits — where a duotone is a
   treatment rather than a distortion.

   `mix-blend-mode: color` takes hue and saturation from this layer and
   luminosity from the image below, which is what a duotone is. Doing it
   this way rather than with a filter chain on the <img> matters here:
   several of these photographs already carry their own inline
   `filter: brightness()/grayscale()` to sit behind text, and a filter
   on the image would have to fight an inline style and would throw away
   that existing art direction. A blended layer composes on top of it.

   isolation: isolate is load-bearing — without a stacking context the
   blend reaches past the wrapper and tints whatever is behind it. */
.rk-tint {
    position: relative;
    isolation: isolate;
    --rk-tint-strength: 1;
}
.rk-tint::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    background: var(--rk-accent);
    mix-blend-mode: color;
    opacity: var(--rk-tint-strength);
}
/* A second, much lighter multiply pass. `color` alone preserves the
   original luminance exactly, which on already-dark photographs reads
   more brown than neon; this pulls the darks down so the yellow that is
   left reads vividly against them. */
.rk-tint::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    background: var(--rk-accent);
    mix-blend-mode: multiply;
    opacity: calc(var(--rk-tint-strength) * 0.35);
}

/* The tint is for the photograph, not for anything sitting on top of it.
   These wrappers also hold caption overlays, and at z-index 2 the blend
   was landing on those too — the labels and their dark scrim came out
   tinted and barely readable. Lifting every non-image child above the
   blend keeps captions, labels and links legible. */
.rk-tint > *:not(img) { z-index: 3; }

@media (pointer: coarse), (hover: none) {
    /* Two blended layers per image is real compositing work on a phone;
       the `color` pass alone still reads as a duotone. */
    .rk-tint::before { display: none; }
}

/* ── Tailwind gold utilities ────────────────────────────
   tailwind.css is a prebuilt, purged artifact with the old gold baked
   into .text-gold / .border-gold / selection. The tailwind.config block
   in the page only takes effect under the CDN build, so it cannot reach
   these. Overriding here — same specificity, later in the cascade —
   re-points them at the accent without editing a generated file, so
   regenerating tailwind.css can't silently undo it. */
.text-gold      { color: var(--rk-accent); }
.text-gold\/80  { color: rgb(var(--rk-accent-rgb) / .8); }
.text-gold\/50  { color: rgb(var(--rk-accent-rgb) / .5); }
.border-gold    { border-color: var(--rk-accent); }
.bg-gold\/10    { background-color: rgb(var(--rk-accent-rgb) / .1); }
.hover\:text-gold:hover        { color: var(--rk-accent); }
.hover\:border-gold\/40:hover  { border-color: rgb(var(--rk-accent-rgb) / .4); }
.group:hover .group-hover\:text-gold { color: var(--rk-accent); }
.selection\:bg-gold ::selection,
.selection\:bg-gold::selection { background-color: var(--rk-accent); color: var(--rk-black); }

/* ── Per-page accent themes ─────────────────────────────
   Standalone pages pick a palette colour by putting one of these on
   <html>. `html.x` outranks the bare `:root` above, which a page-level
   :root override could not do — this stylesheet loads last. */
html.rk-theme-burgundy { --rk-accent: var(--rk-burgundy-lite); --rk-accent-rgb: var(--rk-burgundy-lite-rgb); }
html.rk-theme-green    { --rk-accent: var(--rk-green);         --rk-accent-rgb: var(--rk-green-rgb); }
html.rk-theme-blue     { --rk-accent: var(--rk-blue);          --rk-accent-rgb: var(--rk-blue-rgb); }
