/* ── Portfolio shared components ──────────────────────────────────────────────
   Structure that is genuinely the same on more than one page lives HERE, once.
   Pages keep only their own layout/composition/colour on top (CLAUDE.md →
   Conventions → Shared components).

   Loaded byte-identical in every page's <head> (same rule as tokens.css and the
   Swup vendor tags) at `?v=1`, immediately AFTER tokens.css — so every var()
   below resolves — and BEFORE research-insights.css and each page's inline
   <style>, so any page rule of equal specificity wins without !important.

   Because it is a render-blocking <link> in <head> there is no first-paint flash,
   and it matches the head-plugin's persistTags so it survives every Swup visit.

   ── WHAT'S IN HERE ──────────────────────────────────────────────────────────
     1. Page-transition curtain   (was duplicated inline in all 4 pages)
     2. Glass surface             (the hairline-::before + backdrop-filter pill)
     3. Modal scaffold            (.ks-modal on the three project pages)
   Vary a component through its custom-property hooks or a modifier class —
   never by re-pasting the block with tweaked literals. */


/* ══ 1. Page transition curtain (Swup-driven; pink circle w/ project name) ════
   The curtain lives outside #swup so it persists across swaps. Its clip-path is
   animated via the html classes Swup toggles during a visit:
     is-changing  → present the whole visit (enables the transition)
     is-leaving   → out phase: descend top → centre (cover)
     is-rendering → in  phase: reveal centre → bottom (after content swap)

   Gotcha kept from the old inline copies: the is-leaving and is-rendering
   *content* rules must each repeat EVERY property the other sets (opacity +
   transform). Swup toggles the classes with no base state, so an omitted
   property snaps untransitioned (this once made the label jump 20px). */
.page-transition{
  position:fixed;inset:0;z-index:9999;
  background:var(--curtain-pink);
  display:flex;align-items:center;justify-content:center;
  clip-path:circle(75vmax at 50% -76vmax);
  -webkit-clip-path:circle(75vmax at 50% -76vmax);
  pointer-events:none;will-change:clip-path;
}
html.is-changing .page-transition{
  transition:clip-path .9s var(--ease-curtain),
             -webkit-clip-path .9s var(--ease-curtain);
}
html.is-leaving .page-transition{
  clip-path:circle(75vmax at 50% 50%);
  -webkit-clip-path:circle(75vmax at 50% 50%);
}
html.is-rendering .page-transition{
  clip-path:circle(75vmax at 50% 176vmax);
  -webkit-clip-path:circle(75vmax at 50% 176vmax);
}
.page-transition-content{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;padding:0 40px;gap:12px;
  font-family:var(--font-sans);
  opacity:0;transform:translateY(20px);
}
html.is-leaving .page-transition-content{
  opacity:1;transform:translateY(0);
  transition:opacity .4s ease .45s, transform .4s ease .45s;
}
html.is-rendering .page-transition-content{
  opacity:0;transform:translateY(0);transition:opacity .3s ease;
}
.page-transition-eyebrow{font-size:clamp(12px,1.1vw,15px);font-weight:400;letter-spacing:0.14em;text-transform:uppercase;color:rgba(255,255,255,0.65);}
.page-transition-label{font-size:24px;font-weight:600;letter-spacing:-0.03em;line-height:1.1;color:#fff;max-width:14ch;}
/* Reduced motion: the curtain still runs — Swup awaits the transition end, so
   killing it outright (transition:none) would strand the visit — but at ~0s, so
   the 0.9s clip-path sweep and the label's slide-in are effectively instant. */
@media(prefers-reduced-motion:reduce){
  html.is-changing .page-transition{transition-duration:.01ms;}
  html.is-leaving .page-transition-content,html.is-rendering .page-transition-content{transition-duration:.01ms;transition-delay:0s;}
}


/* ══ 2. Glass surface ═════════════════════════════════════════════════════════
   The portfolio's one surface recipe: a near-transparent fill that blurs what is
   behind it, a soft drop shadow, and a 1px "hairline" ring painted by a masked
   ::before (padding-box XOR border-box = a gradient border no `border` can do).
   It was pasted 20+ times across the four pages; this is that paste, once.

   USING IT
     Add a selector to the two lists below (that is the `%`-placeholder style the
     conventions call for), or put class="glass" on a new element. The call site
     still owns its own layout: position, size, padding, radius, font, colour,
     and any transition beyond the shared box-shadow/transform lift.

   HOOKS — set on the call site, defaults are the values 15 of the 20 sites used
     --glass-fill      background painted under the blur   rgba(0,0,0,0.004)
     --glass-filter    the backdrop-filter recipe          var(--glass-blur)
     --glass-rest      box-shadow at rest                  var(--glass-shadow)
     --glass-hairline  the ::before ring gradient          var(--glass-sheen)

   The `position:relative` is load-bearing (the ::before is absolutely positioned
   against it); call sites that need fixed/absolute simply say so and win, since
   this file loads before every page's <style>.

   PREFIX PAIRS (no autoprefixer here — CLAUDE.md → Browsers): backdrop-filter
   and the whole mask family are inseparable pairs. `mask` is a shorthand and
   resets mask-composite, so both shorthands MUST come before both composites. */
.glass,
.ks-btn,            /* pill button — preply · kyivstar · deal */
.cc-nav-btn,        /* contact popup trigger — index */
.works-close,       /* works carousel close — index */
.life-close,        /* life deck close — index */
.cs-back,           /* case-study back link — kyivstar */
.ks-modal-close,    /* modal close / prev / next — all 3 project pages */
.dd-btn,            /* card CTA — preply · deal */
.ov-view-switcher,  /* desk/grid switcher — preply · deal */
.ks-view-switcher,  /* desk/grid switcher — kyivstar */
.ins-mh-close,      /* R&I mobile header close — assets/shared/research-insights.css.
                       Its layout rules are inside that file's ≤599px block; the
                       surface here is unconditional, which is harmless because
                       its .ins-mobile-header parent is display:none above 600px. */
.intro-back,        /* tilted glass plate behind the intro video — index */
.work-card,         /* carousel card — index */
.pr-sheet{          /* frosted reading panel — preply */
  position:relative;
  background:var(--glass-fill,rgba(0,0,0,0.004));
  -webkit-backdrop-filter:var(--glass-filter,var(--glass-blur));
          backdrop-filter:var(--glass-filter,var(--glass-blur));
  box-shadow:var(--glass-rest,var(--glass-shadow));
}
.glass::before,
.ks-btn::before,
.cc-nav-btn::before,
.works-close::before,
.life-close::before,
.cs-back::before,
.ks-modal-close::before,
.dd-btn::before,
.ov-view-switcher::before,
.ks-view-switcher::before,
.ins-mh-close::before,
.intro-back::before,
.work-card::before,
.pr-sheet::before{
  content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;
  background:var(--glass-hairline,var(--glass-sheen));
  -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
          mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
          mask-composite:exclude;
  pointer-events:none;
}

/* The shared lift. Only the pills that actually respond to a pointer are listed;
   .intro-back / .pr-sheet / the switchers are decorative or containers.
   `transition` stays on the call site because several sites compose it with
   their own (opacity, top/right, …) and a shorthand here would clobber that. */
.glass:hover,
.ks-btn:hover,
.cc-nav-btn:hover,
.works-close:hover,
.life-close:hover,
.cs-back:hover,
.ks-modal-close:hover,
.dd-btn:hover,
.ins-mh-close:hover,
.work-card:hover{
  box-shadow:var(--glass-shadow-hover);transform:translateY(-1px);
}
.glass:active,
.ks-btn:active,
.cc-nav-btn:active,
.works-close:active,
.life-close:active,
.cs-back:active,
.dd-btn:active{
  box-shadow:var(--glass-shadow-sm);transform:translateY(0);
}

/* Pill button — byte-identical on preply · kyivstar · deal. Everything glassy
   comes from the lists above; this is just its shape and type. */
.ks-btn{
  display:inline-flex;align-items:center;justify-content:center;gap:10px;
  padding:12px 18px;border-radius:var(--r-lg);
  border:none;cursor:pointer;
  font-family:var(--font-sans);
  font-size:15px;font-weight:500;letter-spacing:0;
  color:#fff;text-decoration:none;
  transition:box-shadow .2s ease,transform .15s ease;
}

/* Modal close pill — byte-identical on preply · kyivstar · deal. The same
   element type serves as .ks-modal-next / .ks-modal-prev, which each page
   re-anchors (and kyivstar re-rounds) on top of this. The top/right/bottom/left
   transitions are what make that re-anchoring animate between breakpoints. */
.ks-modal-close{
  position:fixed;top:max(var(--page-pad-y,28px),env(safe-area-inset-top));right:max(var(--page-pad-x,60px),env(safe-area-inset-right));z-index:11;
  display:inline-flex;align-items:center;gap:10px;
  padding:12px 16px;border-radius:var(--r-lg);
  border:none;cursor:pointer;
  font-family:var(--font-sans);font-size:15px;color:#fff;text-decoration:none;
  opacity:0;
  transition:opacity .7s ease,box-shadow .2s ease,transform .15s ease,
             top .3s ease, right .3s ease, bottom .3s ease, left .3s ease;
}
.ks-modal-close img{transform:translateY(1.5px);}
.ks-modal.open .ks-modal-close{opacity:1;}
/* Contact popup: hold the Close button back until the entrance cascade finishes. */
.cc-modal.open .ks-modal-close{transition:opacity .4s ease .7s,box-shadow .2s ease,transform .15s ease;}

/* Modal stepper (preply · kyivstar · deal) — byte-identical on all three pages,
   so the base rule moves here WITH the portrait override: leaving the base in
   the pages would let it beat this media query, since page <style> loads last.
   Each page still owns its own `#<modal>.open .dd-stepper{opacity:1}`.
   Portrait only, the dots stack into a column on the right edge and gain the
   glass surface; in landscape the stepper is a bare row on top with no surface
   at all — which is why .dd-stepper can't join the two lists above. */
.dd-stepper{
  position:fixed;top:calc(var(--page-pad-y,28px) + 22px);left:50%;transform:translate(-50%,-50%);z-index:11;
  display:flex;align-items:center;gap:10px;
  opacity:0;transition:opacity .7s ease;
}
@media (orientation: portrait){
  .dd-stepper{
    top:50%;left:auto;right:max(var(--page-pad-x,60px),env(safe-area-inset-right));transform:translateY(-50%);flex-direction:column;gap:16px;
    padding:24px 16px;border-radius:var(--r-xl);
    background:var(--glass-fill,rgba(0,0,0,0.004));
    -webkit-backdrop-filter:var(--glass-filter,var(--glass-blur));
            backdrop-filter:var(--glass-filter,var(--glass-blur));
    box-shadow:var(--shadow-sm);
    max-height:calc(100vh - 120px);overflow-y:auto;-webkit-overflow-scrolling:touch;
    scrollbar-width:none;-ms-overflow-style:none;
  }
  @supports (height:100dvh){
    .dd-stepper{max-height:calc(100dvh - 120px);}
  }
  .dd-stepper::-webkit-scrollbar{display:none;}
  .dd-stepper::before{
    content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;
    background:var(--glass-hairline,var(--glass-sheen));
    -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
            mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
    -webkit-mask-composite:xor;
            mask-composite:exclude;
    pointer-events:none;
  }
}


/* ══ 3. Modal scaffold ════════════════════════════════════════════════════════
   The full-screen folder dialogs on preply · kyivstar · deal. All three had
   their own copy and all three had drifted — different dim values (.7 vs .85)
   and two different fade mechanisms at two different speeds (.3s vs .7s). One
   scaffold now, one behaviour: an opacity fade over .3s behind --modal-dim.

   Two classes, two jobs — `is-shown` mounts the modal (display), `open` runs the
   fade. Pages add is-shown, force a reflow, then add open, so the transition has
   a frame to start from; on close they reverse it and drop is-shown on
   transitionend. Keep them separate: collapsing them loses the entrance fade.

   Escape-to-close and the focus trap are behaviour, not style — see
   window.modalEscape and window.modalFocus in assets/shared/portfolio.js. */
.ks-modal{
  display:none;position:fixed;inset:0;z-index:100;
  background:var(--modal-dim);
  align-items:center;justify-content:center;
  opacity:0;
  transition:opacity .3s ease;
  padding:60px 40px;
}
.ks-modal.is-shown{display:flex;}
.ks-modal.open{opacity:1;}


/* ══ 4. Folder ════════════════════════════════════════════════════════════════
   The CSS-drawn macOS-style folder: a masked back plate, a contents layer, and
   a front lid that tilts forward on `.peek` while the pop-out thumbnails rise
   out of the pocket. Structurally identical on home (`.icon-works`/`.icon-notes`)
   and all three project pages (`.ks-folder`) — including the SVG silhouette,
   which was the same ~500-byte data URI inlined EIGHT times.

   Colours come from three custom properties set on the folder element:
     --folder-back      the back plate
     --folder-front-a   lid gradient, top
     --folder-front-b   lid gradient, bottom
   tokens.css ships the four palette triples (yellow / red / blue / pink) to
   assign them from. Pages keep only position, colour and their pop-out layout.

   iOS Safari gotchas, both load-bearing (moved here with the rules they guard):
   - Scale the folder with `scale3d(...)`, never `scale(...)` — a 2D scale on a
     preserve-3d subtree flattens the lid's rotateX and the folder stops opening.
     See home's breakpoint rules, which are the only ones that scale it.
   - Every -webkit- prefix here is half of an inseparable pair (perspective,
     perspective-origin, transform-style, backface-visibility, mask) — edit both
     or neither; there is no autoprefixer in this repo. */
.folder{
  position:relative;display:block;width:98px;height:86px;
  perspective:700px;-webkit-perspective:700px;
  perspective-origin:50% 100%;-webkit-perspective-origin:50% 100%;
  transform-style:preserve-3d;-webkit-transform-style:preserve-3d;
}
/* Folder silhouette (tab + smooth curve) drawn with an SVG mask, so each folder
   keeps its --folder-back colour but gets the exact macOS-style shape. */
.folder-back{
  position:absolute;left:0;right:0;bottom:0;height:78px;background:linear-gradient(180deg,rgba(255,255,255,0.10) 0%,rgba(0,0,0,0.13) 100%),var(--folder-back);z-index:0;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='4 0 120 93' preserveAspectRatio='none'%3E%3Cpath d='M124 85C124 89.4183 120.418 93 116 93H12C7.58172 93 4 89.4183 4 85V8C4 3.58172 7.58172 0 12 0H38C40 0 41.5 0.5 42.5 1.5L49 5.5C50.5 7 51.5 8 53 8H116C120.418 8 124 11.5817 124 16V85Z' fill='%23000'/%3E%3C/svg%3E") center/100% 100% no-repeat;
          mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='4 0 120 93' preserveAspectRatio='none'%3E%3Cpath d='M124 85C124 89.4183 120.418 93 116 93H12C7.58172 93 4 89.4183 4 85V8C4 3.58172 7.58172 0 12 0H38C40 0 41.5 0.5 42.5 1.5L49 5.5C50.5 7 51.5 8 53 8H116C120.418 8 124 11.5817 124 16V85Z' fill='%23000'/%3E%3C/svg%3E") center/100% 100% no-repeat;
}
.folder-front{
  position:absolute;bottom:0;left:0;width:100%;height:64px;
  background:linear-gradient(180deg,var(--folder-front-a) 0%,var(--folder-front-b) 100%);
  border-radius:var(--r-xs);z-index:3;
  transform-origin:50% 100%;transition:transform .4s ease;
  backface-visibility:hidden;-webkit-backface-visibility:hidden;
  box-shadow:0 6px 16px rgba(0,0,0,.18);
}
/* The lid tilt is shared; the deeper open-state shadow is NOT — preply and deal
   deliberately keep the resting shadow while the lid is open, home and kyivstar
   swap in a bigger one. Those two add it back as a one-line page override. */
.peek .folder-front{transform:rotateX(-40deg);}

/* The macOS app-icon image used by the non-folder desktop icons (home's CV.pdf)
   and, on the project pages, by any folder that shows an image instead. */
.mac-icon{
  width:auto;height:auto;
  max-width:90px;max-height:80px;
  object-fit:contain;
  filter:drop-shadow(0 2px 5px rgba(0,0,0,.18));
  pointer-events:none;
  display:block;
}

/* Project-page folder chrome. Scoped to .ks-folder rather than shared with
   home's .icon-item, which has its own label size, its own .po geometry, and —
   deliberately — NO .folder-content rule: without one, home's pop-outs position
   against .folder and their z-index competes with .folder-front, so two of them
   ride in FRONT of the lid. Giving .folder-content a stacking context here is
   what keeps every project-page pop-out behind it. */
.ks-folder{
  position:absolute;z-index:6;
  display:flex;flex-direction:column;align-items:center;gap:8px;
  cursor:grab;user-select:none;
  transition:transform .12s ease,filter .12s ease;
  touch-action:none;pointer-events:all;
}
.ks-folder:hover{filter:none;}
.ks-folder.dragging{cursor:grabbing;transition:none;filter:drop-shadow(0 4px 10px rgba(0,0,0,.22));z-index:50;}
.ks-folder .icon-label{
  color:#fff;font-size:14px;font-weight:500;text-align:center;
  text-shadow:0 1px 6px rgba(0,0,0,.55);letter-spacing:.01em;
  pointer-events:none;line-height:1.2;
}
.ks-folder .folder-content{position:absolute;inset:0;z-index:1;pointer-events:none;}
/* Only the reveal is shared. The `.po` base rule itself is NOT: preply's
   pop-outs are fixed-size rounded tiles with a drop shadow, kyivstar's and
   deal's are unrounded, clipped and shadowless. Hoisting one of those two and
   letting the other override would leave the odd page inheriting properties it
   never sets (`overflow:hidden` would start clipping preply's tiles), so all
   three keep their own — as does home, whose `.po` differs again. */
.ks-folder.peek .po{opacity:1;}
