/* ==========================================================================
   Rachele Cespi — Design System
   Palette, type and components kept from the Webflow demo; refined for
   clearer typographic hierarchy, accessibility and responsiveness.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=Anek+Gujarati:wght@300;400;500;600;700&family=Anek+Latin:wght@400&display=swap');

/* ---------- Tokens ---------- */
:root {
  /* Brand palette (from the original demo) */
  --violet-900: #1b0b39;   /* deep space violet — dark section backgrounds */
  --violet-800: #291552;
  --violet-700: #3a1f6e;
  --violet-100: #f3eefc;   /* light violet tint — alternating light sections */
  --violet-50:  #faf8ff;
  --purple-600: #6601ff;   /* primary accent / CTA */
  --purple-700: #5200d6;   /* accent hover */
  --purple-300: #b98bff;

  --white: #ffffff;
  --ink-900: #1a1a1a;      /* headings on light bg */
  --ink-700: #333333;      /* body text on light bg */
  --ink-500: #6b6b76;      /* muted / secondary text */
  --line-200: #e6e1f2;     /* borders on light bg */
  --line-dark: rgba(255, 255, 255, 0.16); /* borders on dark bg */

  --on-dark-900: #ffffff;
  --on-dark-700: #d9d2ec;
  --on-dark-500: #a89fc4;

  --success: #1c9a63;
  --danger: #e0442e;

  /* Typography */
  --font-display: "Bricolage Grotesque", "Segoe UI", sans-serif;
  --font-body: "Anek Gujarati", "Segoe UI", sans-serif;

  --text-xs: 0.8125rem;   /* 13px */
  --text-sm: 0.9375rem;   /* 15px */
  --text-base: 1rem;      /* 16px */
  --text-lg: 1.125rem;    /* 18px */
  --text-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
  --text-2xl: clamp(1.6rem, 1.4rem + 1vw, 2.1rem);
  --text-3xl: clamp(2rem, 1.6rem + 2vw, 2.75rem);
  --text-4xl: clamp(2.5rem, 1.9rem + 3vw, 3.75rem);
  --text-5xl: clamp(3rem, 2.1rem + 4.5vw, 5rem);

  --leading-tight: 1.15;
  --leading-snug: 1.3;
  --leading-normal: 1.6;

  /* Spacing (4/8pt rhythm) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-8: 3rem;
  --space-10: 4rem;
  --space-12: 6rem;
  --space-16: 8rem;

  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-lg: 28px;
  --radius-pill: 999px;

  --shadow-card: 0 8px 30px rgba(27, 11, 57, 0.12);
  --shadow-pop: 0 14px 40px rgba(102, 1, 255, 0.25);

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-fast: 150ms;
  --duration-base: 250ms;

  --container-max: 1200px;

  /* Scroll reveal: only opacity/transform animate (layout-affecting
     properties like height/margin/top would jank and cause reflow).
     The distance stays small on purpose — an element travelling 60px
     reads as heavy and contradicts the site's "zero friction" feel.
     Every reveal fires once; nothing loops or re-animates. */
  --reveal-dur: 520ms;
  --reveal-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --reveal-dist: 16px;
  --reveal-stagger: 80ms;
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}
body {
  margin: 0;
  min-height: 100dvh;
  background: var(--white);
  color: var(--ink-700);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}
img, picture, svg { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
input, textarea, select { font: inherit; }
h1, h2, h3, h4, h5 { margin: 0; font-family: var(--font-display); font-weight: 700; line-height: var(--leading-tight); color: var(--ink-900); }
p { margin: 0; }

/* Visible, accessible focus ring on every interactive element */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--purple-600);
  outline-offset: 3px;
  border-radius: 4px;
}
.on-dark a:focus-visible, .on-dark button:focus-visible { outline-color: var(--white); }

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--purple-600);
  color: var(--white);
  padding: var(--space-3) var(--space-5);
  border-radius: 0 0 var(--radius-sm) 0;
  z-index: 1000;
}
.skip-link:focus { left: 0; }

/* ---------- Layout ---------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-5);
}
@media (min-width: 768px) { .container { padding-inline: var(--space-8); } }

.section { padding-block: var(--space-12); }
@media (max-width: 767px) { .section { padding-block: var(--space-10); } }

.section--dark { background: var(--violet-900); color: var(--on-dark-700); }
.section--dark h1, .section--dark h2, .section--dark h3, .section--dark h4 { color: var(--white); }
.section--tint { background: var(--violet-100); }
/* Skills section eases in from the white About section above it instead of cutting hard */
#skills.section--tint { background: linear-gradient(180deg, var(--white) 0%, var(--violet-100) 200px, var(--violet-100) 100%); }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: calc(var(--text-xs) - 2px);
  letter-spacing: 0.14em;
  color: var(--purple-600);
  margin-bottom: var(--space-3);
}
.section--dark .eyebrow { color: var(--purple-300); }
.eyebrow::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* Glass eyebrow badges: pale, frosted, rounded pill wrapping the label.
   Top/bottom padding kept tight (vs. .btn's) so these read as small
   labels, not as buttons. */
.eyebrow--glass, .eyebrow--tint {
  padding: var(--space-1) var(--space-5);
  border-radius: var(--radius-pill);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.eyebrow--glass::before, .eyebrow--tint::before { display: none; }
.eyebrow--glass {
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: var(--white);
}
.eyebrow--tint {
  background: rgba(102, 1, 255, 0.08);
  border: 1px solid rgba(102, 1, 255, 0.18);
  color: var(--purple-700);
}

.section-head { max-width: 640px; margin-bottom: var(--space-8); }
.section-head--center { margin-inline: auto; text-align: center; }

h1, .h1 { font-size: var(--text-5xl); font-weight: 600; letter-spacing: -0.02em; }
h2, .h2 { font-size: var(--text-3xl); letter-spacing: -0.01em; margin-bottom: var(--space-4); }
h3, .h3 { font-size: var(--text-xl); font-weight: 600; margin-bottom: var(--space-2); }
h4, .h4 { font-size: var(--text-lg); font-weight: 600; }
.lede { font-size: var(--text-lg); color: var(--ink-500); max-width: 60ch; }
.section--dark .lede { color: var(--on-dark-500); }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 48px;
  padding: 0.85rem 1.75rem;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-base) var(--ease-out), background-color var(--duration-base) var(--ease-out);
}
.btn:active { transform: scale(0.97); }
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }
.btn-primary { background: var(--purple-600); color: var(--white); box-shadow: var(--shadow-pop); }
.btn-primary:hover { background: var(--purple-700); }
.btn-ghost { background: transparent; color: var(--white); border: 1.5px solid var(--line-dark); }
.btn-ghost:hover { border-color: var(--white); background: rgba(255, 255, 255, 0.08); }
.btn-outline { background: transparent; color: var(--purple-600); border: 1.5px solid var(--purple-600); }
.btn-outline:hover { background: var(--violet-100); }
.btn-sm { min-height: 40px; padding: 0.6rem 1.25rem; font-size: var(--text-xs); }

/* Glass buttons: frosted, with a reflection that tracks the cursor (--mx/--my set by JS) */
.btn-glass {
  position: relative;
  overflow: hidden;
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
}
.btn-glass::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(140px circle at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.45), transparent 60%);
  transition: opacity var(--duration-base) var(--ease-out);
}
.btn-glass:hover::after { opacity: 1; }
.btn-glass.btn-primary { background: rgba(102, 1, 255, 0.55); box-shadow: var(--shadow-pop); }
.btn-glass.btn-primary:hover { background: rgba(102, 1, 255, 0.7); }
.btn-glass.btn-ghost { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.35); }
.btn-glass.btn-ghost:hover { background: rgba(255, 255, 255, 0.16); }

/* Light variant: same frosted idea, tuned for sitting on a white/light section */
.btn-glass--light {
  background: rgba(255, 255, 255, 0.55);
  border: 1.5px solid rgba(102, 1, 255, 0.25);
  color: var(--purple-700);
  box-shadow: 0 8px 24px rgba(27, 11, 57, 0.1);
}
.btn-glass--light:hover { background: rgba(255, 255, 255, 0.75); }
.btn-glass--light::after {
  background: radial-gradient(140px circle at var(--mx, 50%) var(--my, 50%), rgba(102, 1, 255, 0.2), transparent 60%);
}
.btn-glass--light.btn-primary { background: rgba(102, 1, 255, 0.12); color: var(--purple-700); box-shadow: 0 8px 24px rgba(27, 11, 57, 0.1); }
.btn-glass--light.btn-primary:hover { background: rgba(102, 1, 255, 0.2); }

/* Arrow button: thin rounded outline + directional icon. Used for the Portfolio
   link (forward) and "back" links elsewhere (leading, mirrored icon). */
.btn-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0.6rem 1.5rem;
  border-radius: var(--radius-pill);
  border: 1.5px solid rgba(255, 255, 255, 0.6);
  color: var(--white);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  transition: background-color var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out);
}
.btn-arrow:hover { background: rgba(255, 255, 255, 0.12); border-color: var(--white); }
.btn-arrow img, .btn-arrow svg { width: 16px; height: 16px; transition: transform var(--duration-base) var(--ease-out); filter: brightness(0) invert(1); }
.btn-arrow:hover img, .btn-arrow:hover svg { transform: translateX(3px); }
.btn-arrow--back { flex-direction: row-reverse; }
.btn-arrow--back:hover img, .btn-arrow--back:hover svg { transform: translateX(-3px); }
.btn-arrow--nav { min-height: 40px; padding: 0.5rem 1.25rem; }
.btn-arrow--on-light { border-color: var(--purple-600); color: var(--purple-700); }
.btn-arrow--on-light:hover { background: var(--violet-100); }
/* The supplied arrow icons are solid white; brightness(0) turns them solid
   black instead so they stay visible on a light background. */
.btn-arrow--on-light img, .btn-arrow--on-light svg { filter: brightness(0); }

/* ---------- Nav ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(27, 11, 57, 0.88);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line-dark);
}
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: var(--space-3);
  gap: var(--space-6);
}
.nav-logo { display: flex; align-items: center; gap: var(--space-2); }
.nav-logo img { width: 40px; height: 40px; }
.nav-logo span { font-family: var(--font-display); font-weight: 700; color: var(--white); font-size: var(--text-lg); }

.nav-links {
  display: none;
  align-items: center;
  gap: var(--space-6);
}
.nav-links a {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--on-dark-700);
  padding: var(--space-2) 0;
  position: relative;
}
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -4px;
  height: 2px;
  background: var(--purple-300);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--duration-base) var(--ease-out);
}
.nav-links a:hover, .nav-links a[aria-current="page"] { color: var(--white); }
.nav-links a:hover::after, .nav-links a[aria-current="page"]::after { transform: scaleX(1); }

.nav-portfolio { display: none; }
@media (min-width: 1024px) { .nav-portfolio { display: flex; align-items: center; } }

/* Pages with a full tab nav (home + skills/*.html): the "Portfolio" forward
   button is pulled closer to the tabs (near "Contact") via a small JS module
   (see main.js, "Nudge nav-portfolio toward the tabs") instead of CSS. A
   fixed CSS offset can't adapt to how much room is actually free - that room
   shrinks a lot on narrower desktop widths and with longer tab labels (IT),
   so a flat px value that looks fine at 1440px overlaps the tabs elsewhere.
   The script measures the real gap and only takes back what's safely spare,
   leaving .nav-links completely untouched at every viewport/language. */

/* Long-form pages (e.g. app-ux-design): no tabs, so .nav-home already has
   margin-right:auto pulling all free space into the gap after it, which
   shoves .nav-portfolio flush against .nav-actions on the far right. Giving
   .nav-portfolio its own margin-right:auto splits that free space evenly
   between the two gaps instead, landing the button roughly mid-way - more
   central and visible than pinned to the edge. */
.nav:has(.nav-home) .nav-portfolio { margin-right: auto; }

/* "Back" button (to home or to portfolio depending on page). Sits right after
   the logo - not centered via the nav's space-between - so it reads as a
   familiar, browser-like back control. margin-right:auto eats the remaining
   free space, pushing .nav-actions to the far right same as before.
   Mobile: icon-only (label collapsed via font-size:0, so it's still in the
   accessible name), >=50px from the logo. Desktop: label restored, >=100px. */
.nav-home {
  display: flex;
  align-items: center;
  margin-left: calc(50px - var(--space-6));
  margin-right: auto;
}
.nav-home .btn-arrow--nav {
  padding: 0.5rem;
  min-width: 40px;
  justify-content: center;
  gap: 0;
  font-size: 0;
}
@media (min-width: 1024px) {
  .nav-home { margin-left: calc(100px - var(--space-6)); }
  .nav-home .btn-arrow--nav {
    padding: 0.5rem 1.25rem;
    min-width: 0;
    gap: var(--space-2);
    font-size: var(--text-sm);
  }
}

/* The "Rachele Cespi" wordmark next to the logo is redundant on mobile
   (narrow header, back button now closer to the logo) - icon only until
   desktop. Scoped to the sticky header so the slide-out mobile-nav panel's
   own logo+name heading is untouched. */
.site-header .nav-logo span { display: none; }
@media (min-width: 1024px) { .site-header .nav-logo span { display: inline; } }

.nav-actions { display: flex; align-items: center; gap: var(--space-4); }
.lang-switch { display: flex; gap: var(--space-1); font-family: var(--font-display); font-size: var(--text-xs); font-weight: 700; }
.lang-switch a { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); color: var(--on-dark-500); }
.lang-switch a[aria-current="true"] { color: var(--white); background: rgba(255,255,255,0.1); }
.lang-switch a:hover { color: var(--white); }

.nav-toggle {
  display: inline-flex;
  width: 44px; height: 44px;
  align-items: center; justify-content: center;
  color: var(--white);
}
.nav-toggle svg { width: 26px; height: 26px; }

@media (min-width: 1024px) {
  .nav-links { display: flex; }
  .nav-toggle { display: none; }
}

.mobile-nav {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--violet-900);
  padding: var(--space-5);
  overflow-y: auto;
}
.mobile-nav.is-open { display: block; }
.mobile-nav-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-8); }
.mobile-nav-close { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; color: var(--white); }
.mobile-nav-links { display: flex; flex-direction: column; gap: var(--space-2); }
.mobile-nav-links a {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--white);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line-dark);
}
.mobile-nav-links a[aria-current="page"] { color: var(--purple-300); border-bottom-color: var(--purple-300); }
.mobile-nav-foot { margin-top: var(--space-8); display: flex; flex-direction: column; gap: var(--space-5); }

/* ---------- Hero ---------- */
.hero {
  position: relative;
  background: radial-gradient(120% 100% at 50% 0%, var(--violet-700) 0%, var(--violet-900) 60%);
  color: var(--white);
  padding-block: var(--space-16) var(--space-12);
  overflow: clip;
}
.hero-inner { position: relative; z-index: 1; max-width: 820px; margin-inline: auto; text-align: center; }
.hero h1 { margin-bottom: var(--space-5); color: #FBECFF; text-shadow: 0 0 3px rgba(251, 236, 255, 0.6), 0 0 7px rgba(251, 236, 255, 0.6); }
.hero .lede { color: var(--white); font-weight: 300; font-size: calc(var(--text-xl) - 0.1rem); margin-bottom: var(--space-8); margin-inline: auto; }
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.hero .hero-actions { justify-content: center; }
.hero-stars { position: absolute; inset: 0; pointer-events: none; opacity: 0.5; }

/* Tools section: text and CTAs centered, lede detached from the shared
   .lede max-width so it can be centered as a block within the container */
#pick-a-tool .container { text-align: center; }
#pick-a-tool .lede { margin-inline: auto; color: var(--white); font-size: 1.3rem; }
#pick-a-tool .hero-actions { justify-content: center; margin-top: var(--space-8); }

/* Hero + Tools + Skills eyebrows: doubled breathing room before the title/body that follows */
.hero .eyebrow, #pick-a-tool .eyebrow, #skills .eyebrow { margin-bottom: var(--space-5); }

/* ---------- Tool picker (playful accent, kept from original "pick a tool") ---------- */
/* Cards are selection toggles, not links: clicking one hands the visitor that
   tool as a floating companion (see main.js) instead of navigating away. */
.tool-picker { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); margin-top: var(--space-10); }
.tool-card {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3);
  width: 100%;
  padding: var(--space-6) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--line-dark);
  background: rgba(255,255,255,0.04);
  text-align: center;
  transition: transform var(--duration-base) var(--ease-out), background-color var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.tool-card:hover { transform: translateY(-4px); background: rgba(255,255,255,0.08); border-color: var(--purple-300); }
/* Desktop tooltip: pure CSS, sourced from data-tooltip so the same string
   also feeds the mobile tap-toast in main.js (single source of truth). */
.tool-card::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 12px);
  left: 50%;
  transform: translateX(-50%) translateY(6px);
  width: max-content;
  max-width: 210px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: rgba(20, 8, 40, 0.75);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--white);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out);
  z-index: 20;
}
.tool-card:hover::after, .tool-card:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }
/* Edge cards: anchor the tooltip inward instead of centering it, so it
   can't spill past the left/right edge of the scroll view. */
.tool-card:first-child::after { left: 0; transform: translateX(0) translateY(6px); }
.tool-card:first-child:hover::after, .tool-card:first-child:focus-visible::after { transform: translateX(0) translateY(0); }
.tool-card:last-child::after { left: auto; right: 0; transform: translateX(0) translateY(6px); }
.tool-card:last-child:hover::after, .tool-card:last-child:focus-visible::after { transform: translateX(0) translateY(0); }
@media (max-width: 1023px) {
  /* Mobile/tablet use the tap-triggered .tool-toast (main.js) instead */
  .tool-card::after { display: none; }
}
.tool-card img { width: 72px; height: 72px; transition: transform var(--duration-base) var(--ease-out); }
.tool-card:hover img { transform: scale(1.06); }
.tool-card span {
  font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); color: var(--white);
  opacity: 0; transform: translateY(4px); max-height: 0;
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
}
.tool-card:hover span, .tool-card.is-selected span { opacity: 1; transform: none; max-height: none; }
.tool-card .tool-sublabel { font-family: var(--font-body); font-weight: 400; font-size: var(--text-xs); color: rgba(255,255,255,0.65); margin-top: -4px; }
.tool-card.is-selected {
  border-color: rgba(255, 255, 255, 0.7);
  background: rgba(255, 255, 255, 0.1);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 36px rgba(255, 255, 255, 0.5);
}
@media (max-width: 767px) {
  .tool-card span { display: none; }
  .tool-card.is-selected span { display: block; opacity: 1; transform: none; }
}

/* Desktop/tablet only: fixed card height in BOTH normal and hover state.
   Previously the label/sublabel used max-height:0->none to reveal on hover,
   which changed the card's own content height - since all cards share one
   CSS Grid row, one card growing on hover stretched the whole row, reading
   as a jump across every card at once. Fix: icon and label go absolute
   (removed from flow, so they can no longer affect the card's box height),
   card gets an explicit height instead of a content-driven one. Icon sits
   dead-centre at rest and docks upward on hover/selection to free up room
   for the label underneath - same hover colours/effects as before (border,
   background, lift, icon scale), just re-anchored. Mobile/touch keeps the
   original display:none-based reveal untouched below 768px (no real hover
   there anyway). */
@media (min-width: 768px) {
  .tool-card { height: 210px; }
  .tool-card img { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
  .tool-card:hover img, .tool-card.is-selected img { transform: translate(-50%, -50%) translateY(-26px) scale(1.06); }
  .tool-card span { position: absolute; left: var(--space-3); right: var(--space-3); bottom: 42px; }
  .tool-card .tool-sublabel { position: absolute; left: var(--space-3); right: var(--space-3); bottom: 22px; margin-top: 0; }
}

/* Tools layer is pinned to the viewport (.scroll-story__layer, position:fixed,
   inset:0) with no scroll of its own - it can only ever show as much as the
   screen is tall. A single row of 4 tiles kept the footprint short enough to
   clear that fixed box, but tapping a tile deformed it: img/span/sublabel
   were normal-flow content below 768px, so revealing the label on tap grew
   that one card's box, jumping the whole shared grid row. Fixed 2x2 grid of
   true 1:1 tiles instead, mirroring the >=768px pattern below: icon and
   label go absolute (out of flow) so the card's own box - size, proportions,
   position - never changes on tap, only the icon slides up to make room for
   the label, exactly like desktop's hover reveal. */
@media (max-width: 767px) {
  #pick-a-tool .eyebrow { margin-bottom: var(--space-2); }
  #pick-a-tool .lede { font-size: var(--text-base); margin-bottom: 0; }
  #pick-a-tool .tool-picker {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
    margin-top: var(--space-6);
    max-width: 420px;
    margin-inline: auto;
  }
  #pick-a-tool .tool-card { aspect-ratio: 1 / 1; height: auto; padding: var(--space-3); }
  #pick-a-tool .tool-card img {
    position: absolute; top: 50%; left: 50%;
    width: 40px; height: 40px;
    transform: translate(-50%, -50%);
  }
  #pick-a-tool .tool-card:hover img, #pick-a-tool .tool-card.is-selected img {
    transform: translate(-50%, -50%) translateY(-20px) scale(1.06);
  }
  #pick-a-tool .tool-card span { position: absolute; left: var(--space-2); right: var(--space-2); bottom: 26%; font-size: var(--text-xs); }
  #pick-a-tool .tool-card .tool-sublabel { position: absolute; left: var(--space-2); right: var(--space-2); bottom: 13%; margin-top: 0; }
  #pick-a-tool .hero-actions { margin-top: var(--space-5); gap: var(--space-3); }
}

/* ---------- Cards / Skills grid ---------- */
.skills-grid { display: grid; gap: var(--space-5); grid-template-columns: 1fr; }
@media (min-width: 640px) { .skills-grid { grid-template-columns: repeat(2, 1fr); } }

.skill-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--white);
  border: 1px solid var(--line-200);
  box-shadow: var(--shadow-card);
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.skill-card:hover { transform: translateY(-6px); box-shadow: 0 20px 45px rgba(27,11,57,0.16); }
.skill-card-head { display: flex; align-items: center; gap: var(--space-5); }
.skill-card .icon-badge { width: auto; height: auto; background: transparent; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.skill-card .icon-badge img { width: 128px; height: 128px; transition: transform var(--duration-base) var(--ease-out); }
.skill-card:hover .icon-badge img { transform: scale(1.08); }
.skill-card h3 { display: flex; flex-direction: column; gap: 2px; margin-bottom: 0; }
.skill-branch { font-size: calc(var(--text-xl) + 3pt); }
.skill-role { font-size: var(--text-sm); font-weight: 600; color: var(--purple-600); }
/* Mobile: icon on top, title/subtitle stacked below instead of side by side;
   both bumped up 3pt to hold their own at the narrower width. */
@media (max-width: 639px) {
  .skill-card-head { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
  .skill-role { font-size: calc(var(--text-sm) + 3pt); }
}
.skill-card p { color: var(--ink-500); margin-top: var(--space-2); }
.skill-card .card-link { margin-top: auto; display: inline-flex; align-items: center; min-height: 44px; gap: var(--space-2); font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm); color: var(--purple-600); }
.skill-card .card-link svg { width: 18px; height: 18px; transition: transform var(--duration-base) var(--ease-out); }
.skill-card:hover .card-link svg { transform: translateX(4px); }
.skills-cta { margin-top: var(--space-10); }
.skills-cta h3 { margin-bottom: var(--space-5); }

/* ---------- Visual pause ---------- */
/* Pinned scroll-reveal beat between Skills and Approach. `.pause-track` is
   the scroll distance the pin lasts (300vh desktop / 220vh mobile); it has
   no explicit height on `.visual-pause` itself, so the section's own box
   just matches the track's height in normal flow - no extra wrapper/z-index
   needed. `.pause-stage` is the `position: sticky` box that actually stays
   put while progress runs 0->1 (computed in main.js, module "Visual pause
   reveal"). Looks empty on entry: all four items (3 words + gloss) start
   clipped and reveal one at a time - People -> Purpose -> Experience ->
   gloss - as the user keeps scrolling, then the section releases into
   Approach. Falls back to a plain non-pinned block, everything visible, for
   prefers-reduced-motion and no-JS - see `.visual-pause--static` (added by
   main.js) and the matching <noscript> override in <head>. */
.visual-pause { padding-block: 0; }
.visual-pause .pause-track { position: relative; height: 300vh; }
@media (max-width: 767px) { .visual-pause .pause-track { height: 220vh; } }
/* Background lives on the pinned 100vh stage, not the 300vh/220vh track -
   sizing a `cover` image to the tall track would stretch/crop it across
   three screen-heights instead of one (same reason the Hero/Tools
   scroll-story keeps its backdrop separate from its tall spacer). This way
   the existing visual-pause.webp covers exactly one viewport, same as any
   other full-bleed section background. */
.visual-pause .pause-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--violet-900);
  background-image: linear-gradient(180deg, rgba(27, 11, 57, 0.4) 0%, rgba(27, 11, 57, 0.55) 100%), url('/assets/images/backgrounds/visual-pause.webp');
  background-size: cover;
  background-position: center;
}
.visual-pause--static .pause-track { height: auto; }
.visual-pause--static .pause-stage {
  position: static;
  height: auto;
  min-height: 100vh;
  min-height: 100dvh;
  padding-block: var(--space-12);
}
@media (max-width: 767px) { .visual-pause--static .pause-stage { padding-block: var(--space-10); } }

.visual-pause-stack { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.visual-pause-word { font-family: var(--font-display); font-weight: 600; font-size: clamp(2.75rem, 1.6rem + 8vw, 8rem); line-height: 1.1; color: var(--white); }
.visual-pause-gloss { margin-top: var(--space-6); font-family: var(--font-body); font-weight: 500; font-size: clamp(1rem, 0.85rem + 1.5vw, 1.5rem); color: var(--white); text-align: center; }

/* Scroll-down cue: sits top-centre of the pinned .pause-stage (position:
   sticky, so this stays put for the whole pin duration - "always visible").
   Absolutely positioned rather than laid out inline with the People/Purpose/
   Experience stack for two reasons: it must never affect that stack's
   centring, and it must never be able to push the section's own content
   past the stage's 100vh/100svh box. Not a [data-pause-item] - it's static
   throughout the reveal, not one of the scroll-triggered words. Sized with
   clamp() so it scales down on short/narrow viewports instead of eating
   into the vertical budget the stack+gloss need to stay inside 100vh. */
.pause-scroll-cue {
  position: absolute;
  /* Floor (86px) clears the sticky .site-header (66px desktop, up to 82px
     on narrow widths where the logo wordmark wraps to two lines) with a
     few px to spare, so the cue grazes the header edge instead of hiding
     behind it - the header sits at the same viewport top the stage does,
     with a higher z-index, so anything above this floor gets covered no
     matter how tall/short the screen is. The 10vh term is what lets it
     drift a bit lower (up to the 130px cap) on taller viewports. */
  top: clamp(86px, 10vh, 130px);
  left: 50%;
  transform: translateX(-50%);
  /* Base (mobile) size: original clamp x1.1. Desktop is scaled back down
     to x0.6 of the original below - the icon reads too large at wide
     viewports where the pinned stage has more breathing room. */
  width: clamp(48px, 6.6vw, 97px);
  height: auto;
  opacity: 0.85;
  pointer-events: none;
}
@media (min-width: 768px) {
  .pause-scroll-cue { width: clamp(26px, 3.6vw, 53px); }
}
@media (prefers-reduced-motion: no-preference) {
  .pause-scroll-cue { animation: pause-scroll-cue-bob 1.8s ease-in-out infinite; }
}
@keyframes pause-scroll-cue-bob {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(8px); }
}
/* Static fallback (reduced motion / no-JS): everything is already visible
   at once, nothing is gated behind scrolling, so the cue has nothing left
   to prompt - hidden instead of shown inert. */
.visual-pause--static .pause-scroll-cue { display: none; }

/* Vertical cut-reveal tokens: main.js splits each item's text into
   per-character (words) or per-word (gloss) tokens at runtime, each one
   clipped by its own `overflow: hidden` outer span with the actual glyphs
   pushed below the line in the inner span. Adding `.is-revealed` (per
   item, on scroll) is the only thing that moves the inner span back into
   place - removing the class reverses the same CSS transition for free, so
   scrolling back up hides items again without any extra JS/animation
   state. Spaces are left as plain text nodes between tokens so wrapping
   stays natural. A `.visually-hidden` copy of the untouched string sits
   alongside the split markup so screen readers get the real sentence
   instead of one token at a time. */
.pause-token { display: inline-block; overflow: hidden; vertical-align: top; }
.pause-token-inner {
  display: inline-block;
  transform: translateY(115%);
  transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
  transition-delay: calc(var(--i, 0) * 25ms);
}
[data-pause-item].is-revealed .pause-token-inner { transform: translateY(0); }

/* ---------- Stats ---------- */
.stats-row { display: grid; gap: var(--space-4); grid-template-columns: repeat(2, 1fr); margin-top: var(--space-8); }
@media (min-width: 640px) {
  .stats-row { grid-template-columns: repeat(3, 1fr); }
  .stat:not(:first-child) { border-left: 1px solid var(--line-200); }
}
.stat { display: flex; flex-direction: column; align-items: center; text-align: center; padding-inline: var(--space-3); }
.stat b {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  color: var(--purple-600);
  line-height: 1;
  /* Digits must not shift width as they count up (0->6 vs 0->10) - proportional
     digits would jiggle the number and the label beneath it every frame. */
  font-variant-numeric: tabular-nums;
  min-width: 2ch;
}
.section--dark .stat b { color: var(--white); }
.stat-label { font-family: var(--font-display); font-weight: 700; font-size: calc(var(--text-3xl) - 8pt); color: var(--purple-600); }
.section--dark .stat-label { color: var(--white); }
.stat-icon { width: 70px; height: 70px; margin: var(--space-3) 0; }
.stat-caption { font-size: var(--text-sm); color: var(--ink-500); }
.section--dark .stat-caption { color: var(--on-dark-500); }

/* The "in progress" master's degree stat: no count-up (see Stats counter
   module in main.js for why), just a normal reveal plus a marker that reads
   as "ongoing" without asserting a false-precision percentage. */
.stat-tag {
  display: block;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: calc(var(--text-xs) - 2px);
  color: var(--ink-500);
  margin-top: var(--space-1);
}
.section--dark .stat-tag { color: var(--on-dark-500); }

.stat-progress {
  position: relative;
  width: 100%;
  height: 2px;
  margin-top: var(--space-3);
  border-radius: 1px;
  background: rgba(102, 1, 255, 0.35); /* --purple-600 at low alpha: the track */
  overflow: hidden;
}
.section--dark .stat-progress { background: rgba(255, 255, 255, 0.35); }
.stat-progress::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 40%;
  height: 100%;
  background: var(--purple-600);
  animation: stat-progress-sweep 2s ease-in-out infinite;
}
.section--dark .stat-progress::after { background: var(--white); }
@keyframes stat-progress-sweep {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
  /* A looping sweep is exactly the kind of motion this setting exists to
     suppress - freeze it as a plain static, low-opacity marker instead. */
  .stat-progress::after { animation: none; opacity: 0.5; width: 30%; }
}

/* ---------- About / two column ---------- */
.split { display: grid; gap: var(--space-8); grid-template-columns: 1fr; align-items: start; }
.split.reverse { direction: rtl; } .split.reverse > * { direction: ltr; }
.portrait-frame { position: relative; border-radius: var(--radius-lg); overflow: hidden; background: transparent; }
.portrait-frame img { width: 100%; height: auto; }
@media (min-width: 900px) {
  /* Desktop: the text column's natural content height drives the row; the
     photo then matches it exactly. Taking the img out of flow (absolute,
     inset:0) is what makes this work - otherwise the img's own aspect-ratio
     height also feeds the grid's row-sizing and can end up taller than the
     text, stretching the text column and leaving dead space under it.
     This block must come after the base .portrait-frame rule above -
     same specificity, so source order decides and these overrides need
     to win. */
  .split { grid-template-columns: 0.6fr 1.4fr; align-items: stretch; }
  /* Padding on the frame (not the img) becomes the img's containing block
     inset, since inset:0 on an absolutely positioned element resolves
     against the padding edge - so this both mats the photo away from the
     rounded corners and, combined with object-fit:contain instead of
     cover, stops the subject's edges from being cropped off. */
  .portrait-frame { height: 100%; padding: var(--space-4); background: transparent; }
  .portrait-frame img { position: absolute; inset: 0; height: 100%; width: 100%; object-fit: contain; }
}

/* ---------- Tags ---------- */
.tag-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tag {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-xs);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--violet-100);
  color: var(--purple-700);
}
.section--dark .tag { background: rgba(255,255,255,0.1); color: var(--white); }

/* ---------- FAQ / Knowledge base ---------- */
.faq-list { max-width: 760px; margin-inline: auto; }
.faq-item { border-bottom: 1px solid var(--line-200); }
.faq-q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5) 0;
  text-align: left;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  color: var(--ink-900);
}
.faq-q svg { width: 22px; height: 22px; flex-shrink: 0; transition: transform var(--duration-base) var(--ease-out); color: var(--purple-600); }
.faq-item[open] .faq-q svg { transform: rotate(180deg); }
.faq-a { padding-bottom: var(--space-5); color: var(--ink-500); max-width: 65ch; }
.faq-a ul { margin: 0 0 var(--space-3); padding-left: 1.25em; }
.faq-a li { margin-bottom: var(--space-2); }
.faq-a li:last-child { margin-bottom: 0; }
.faq-a a { color: var(--purple-600); text-decoration: underline; text-underline-offset: 2px; }
.faq-q::-webkit-details-marker { display: none; }

/* ---------- Skill detail pages ---------- */
/* Full-bleed hero background, one real illustration per skill/language (no
   longer the home hero.webp placeholder). Served as a real <picture>/<img>
   (mobile variant via <source>, so the browser only downloads the file it
   needs) instead of a CSS background-image, so it sits behind the darkening
   overlay (::before) and the .container content via z-index layering. */
.skill-hero {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  padding-block: var(--space-12);
  background-color: var(--violet-900);
}
@media (max-width: 767px) { .skill-hero { padding-block: var(--space-10); } }
.skill-hero-bg { position: absolute; inset: 0; z-index: 0; }
.skill-hero-bg img { width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; }
.skill-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: linear-gradient(180deg, rgba(27, 11, 57, 0.4) 0%, rgba(27, 11, 57, 0.65) 100%);
}

/* Hero content centered, per-page title treatment (client-requested look,
   distinct from the shared home .hero styling): single H1 (diagnostic line)
   + positioning line (.lede), same outer-glow pattern as home's .hero h1 but
   scoped separately since the glow radii/opacity differ. */
.skill-hero .container { position: relative; z-index: 2; text-align: center; }
.skill-hero h1, .skill-hero .lede { margin-inline: auto; }
.skill-hero .hero-actions { justify-content: center; }
/* Eyebrow->H1->H2->H4 spacing doubled vs. the site defaults on client request
   (space before the CTA is untouched, so .lede/.hero-actions margins stay as-is). */
.skill-hero .eyebrow { margin-bottom: var(--space-5); }
.skill-hero h1 {
  color: #FBECFF;
  text-shadow: 0 0 3px rgba(251, 236, 255, 0.5), 0 0 5px rgba(251, 236, 255, 0.5);
  margin-bottom: var(--space-4);
}
.skill-hero .lede { color: #ffffff; }
.skill-hero h1, .skill-hero h2 { margin-inline: auto; }
/* H2 sized 0.2rem below the shared h2 token (wrapped in calc so the clamp()
   inside --text-3xl shrinks uniformly at every viewport, not just one end). */
.skill-hero h2 { color: #FBECFF; font-size: calc(var(--text-3xl) - 0.2rem); font-weight: 600; margin-bottom: var(--space-8); }
.skill-hero h4 { color: #ffffff; opacity: 0.85; margin-bottom: var(--space-3); }

/* Leading/trailing icons inside a CTA button (Skill pages): rendered as a
   CSS mask (background-color:currentColor clipped to the icon's shape)
   instead of an <img> + filter:brightness()/invert(). A button's label
   colour isn't just white-or-black - .btn-primary.btn-glass--light is
   purple-700, .btn-outline.btn-glass--light is purple-600, .btn-ghost is
   white - and filter tricks can only reach black or white exactly, not an
   arbitrary purple. A mask has no colour of its own, so currentColor always
   matches the label exactly, on any current or future button variant. */
.btn-icon {
  display: inline-block;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  background-color: currentColor;
  -webkit-mask-image: var(--icon);
  mask-image: var(--icon);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* Symptoms / Why it happens / What I'll do: 3-column card-less grid, thin
   vertical divider on desktop, stacked with a thin top divider on mobile.
   All three fasce sit on light-ish backgrounds (white/tint), so a single
   light divider colour works across all of them - no dark variant needed. */
.trio-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-6); margin-top: calc(var(--space-8) * 1.2); }
@media (min-width: 640px) {
  .trio-grid { grid-template-columns: repeat(3, 1fr); gap: 0; }
  .trio-item { padding-inline: var(--space-6); border-left: 1px solid var(--line-200); }
  .trio-item:first-child { border-left: 0; padding-left: 0; }
  .trio-item:last-child { padding-right: 0; }
}
.trio-item { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-3); }
@media (max-width: 639px) {
  .trio-item + .trio-item { border-top: 1px solid var(--line-200); padding-top: var(--space-5); }
}
.icon-placeholder svg { stroke: var(--ink-500); opacity: 0.55; }
.trio-item .icon-placeholder { width: 56px; height: 56px; }
/* Real per-skill icons (client-supplied SVGs) replacing .icon-placeholder as
   each skill's assets arrive - same footprint as the placeholder it replaces,
   so a page can swap icon-placeholder -> icon-tile without any other layout
   change. Full-colour illustrations, so no stroke/opacity dimming like the
   placeholder gets. */
.icon-tile { display: block; }
.trio-item .icon-tile, .deliverables-list .icon-tile { width: 56px; height: 56px; }
/* Card titles: brand purple on white-bg fasce, darker plum on the tint-bg
   "Why it happens" fascia for contrast against the lighter tint background. */
.trio-item h3 { font-size: calc(var(--text-lg) + 0.2rem); font-weight: 600; color: #6601FF; }
#why-it-happens .trio-item h3 { color: #280B61; }
.trio-item p { color: var(--ink-500); }

/* "One section = one screen" on every viewport now, not just mobile: each
   fascia fills at least one full viewport and its content is vertically
   centered. min-height (never height) so a small phone, enlarged system text
   or a tall desktop viewport grows the section instead of clipping it. Mobile
   uses svh (not vh/dvh) because it's the smallest/most conservative viewport
   unit, matching how the rest of the site already treats mobile viewport
   quirks (see CLAUDE.md on the Hero/Tools scroll-story); desktop uses plain
   vh since there's no address-bar-collapse quirk to guard against there. */
.section--screen { min-height: 100vh; display: flex; align-items: center; }
.section--screen > .container { width: 100%; }
@media (max-width: 767px) {
  .section--screen { min-height: 100svh; }
}

/* What you get: a plain list (not a grid), max 5 items, for the dark "Cosa
   ricevi" fascia - deliberately simpler than the trio grid above it. Heading
   gap scoped to this id (not global h2) since #what-you-get's h2 shares the
   generic h2 rule used sitewide. */
#what-you-get h2 { margin-bottom: calc(var(--space-4) * 2.4); }
/* Scannable icon + h4 keyphrase + body rows (replaces the plain bulleted
   list once a skill has real deliverable icons - see games-deliverable-N.svg).
   Body font-size intentionally unset here so it inherits the same base
   var(--text-base) as .trio-item p (the card body text elsewhere on the
   page) - kept in sync on purpose, not a coincidence. Icons sized -10%
   vs. the shared card-icon footprint (56px), scoped to this list only. */
.deliverables-list { max-width: 640px; margin-inline: auto; display: flex; flex-direction: column; gap: var(--space-6); }
@media (min-width: 900px) {
  /* Two columns, max 3 rows each, to save vertical space on desktop.
     grid-auto-flow:column with an explicit 3-row template places items
     1-3 in column 1 and 4-5 in column 2 deterministically - unlike
     `columns:2` (content-balanced), which could split unevenly depending
     on how long each row's text runs. 900px matches the breakpoint
     already used for two-column layouts elsewhere (.split/.portrait-frame). */
  .deliverables-list {
    display: grid;
    grid-template-rows: repeat(3, auto);
    grid-auto-flow: column;
    column-gap: var(--space-10);
    row-gap: var(--space-6);
    max-width: 980px;
  }
}
.deliverables-list li { display: flex; align-items: flex-start; gap: var(--space-4); text-align: left; }
.deliverables-list .icon-tile { flex-shrink: 0; width: calc(56px * 0.9); height: calc(56px * 0.9); }
.deliverables-list h4 { color: var(--white); margin-bottom: var(--space-1); }
.deliverables-list p { color: var(--on-dark-700); }

/* Expertise / Perfect-if: icon + mini-title + ~10 words, no card chrome. */
.icon-row { display: grid; grid-template-columns: 1fr; gap: var(--space-6); margin-top: var(--space-8); }
@media (min-width: 640px) { .icon-row { grid-template-columns: repeat(3, 1fr); } }
.icon-row-item { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-2); }
.icon-row-item .icon-placeholder, .icon-row-item .icon-tile { width: 48px; height: 48px; }
.icon-row-item h3 { font-size: var(--text-base); }
.icon-row-item p { color: var(--ink-500); font-size: var(--text-sm); }
/* Development Logs (Expertise) card titles get the same +0.2rem/600/purple
   treatment as the trio-grid fasce above (heading-to-cards gap scoped the
   same way, since .icon-row is shared with Perfect-if which must not move). */
#expertise .icon-row { margin-top: calc(var(--space-8) * 1.2); }
#expertise .icon-row-item h3 { font-size: calc(var(--text-base) + 0.2rem); font-weight: 600; color: #6601FF; }
/* Perfect-if card titles: separate treatment (lighter weight, plum) from
   Development Logs above, plus the full-viewport "one section = one screen"
   sizing (.section--screen, added directly on the section in HTML). */
#perfect-if .icon-row-item h3 { font-size: calc(var(--text-base) + 0.2rem); font-weight: 500; color: #280B61; }
/* Fit markers: real SVG check/cross, never emoji (site convention) - uses
   the existing --success/--danger tokens instead of new colours. */
.fit-marker { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.fit-marker--yes { background: rgba(28, 154, 99, 0.12); color: var(--success); }
.fit-marker--no { background: rgba(224, 68, 46, 0.12); color: var(--danger); }
.fit-marker svg { width: 16px; height: 16px; }

/* Skill-page scroll-snap controller (skill pages only, see `data-skill-scroll`
   on <html> + the "Skill scroll" module in main.js). One section per scroll
   gesture, native CSS scroll-snap - no JS scroll-hijacking. Each of the 8
   fasce carries .snap-section; min-height/centering still comes from
   .skill-hero or .section--screen, this class only owns the snap points.
   `mandatory` traps the scroll position at whatever the LAST snap-aligned
   element in the document is - confirmed by scrolling to the document's max
   scrollTop and finding it clamped ~189px short (exactly the footer's
   height) on both EN and IT, at #contact's start. Excluding #contact from
   the chain alone just moves the same trap one section earlier (to
   #perfect-if, the new "last" snap point) - the real fix is giving the
   footer its own snap-align so the true bottom of the page becomes a valid,
   reachable snap target instead of dead space past the last section. */
html[data-skill-scroll] { scroll-snap-type: y mandatory; }
.snap-section { scroll-snap-align: start; scroll-snap-stop: always; }
/* Clearance under the sticky header, without relying on scroll-snap's own
   offset hooks: both scroll-margin-top (on .snap-section) and
   scroll-padding-top (on the scroll container) were tried and reverted -
   neither is honoured by this engine's native wheel/trackpad snap-settle,
   confirmed live by reading getBoundingClientRect() after real scroll
   gestures: a fascia's raw, un-padded top kept landing flush with the
   viewport top regardless of either property (scroll-margin-top only ever
   affected scrollIntoView() jumps, not the settle-after-scroll case that
   matters for every normal down-scroll). Real padding-top on .container
   instead - inside the section's own centering flex box, so it holds even
   when a fascia overflows one screen and centering leaves no natural top
   gap. Hero is excluded: it's always the first element in the document, so
   at initial load (scrollY 0) .site-header already sits above it - nothing
   ever scrolls hero's content up under the header. */
html[data-skill-scroll] .section--screen > .container { padding-top: 100px; }
html[data-skill-scroll] .site-footer { scroll-snap-align: end; }
@media (prefers-reduced-motion: reduce) {
  html[data-skill-scroll] { scroll-snap-type: none; }
}

/* Section "spotlight": the active fascia sits at full opacity, its neighbours
   dim slightly - JS adds/removes .is-dimmed per the IntersectionObserver
   below. Base state is always opacity:1 (no FOUC before JS runs / no-JS). */
.snap-section { transition: opacity 0.25s ease; }
.snap-section.is-dimmed { opacity: 0.55; }

/* Card cascade inside a fascia: hidden below (entering) or above (already
   scrolled past) its resting position, revealed/hidden by the section's own
   active state rather than the generic one-shot reveal (see main.js) -
   bidirectional and reversible, unlike [data-reveal-group] elsewhere on the
   site. --reveal-i/--reveal-stagger reuse the same stagger tokens as the
   generic reveal for a consistent cascade feel. Exit has no stagger delay
   (0s) so leaving cards don't lag visibly behind the scroll. */
[data-snap-group] > * {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.4s var(--reveal-ease), transform 0.4s var(--reveal-ease);
  transition-delay: calc(var(--reveal-i, 0) * var(--reveal-stagger));
}
[data-snap-group] > .is-in { opacity: 1; transform: translateY(0); }
[data-snap-group] > .is-out { opacity: 0; transform: translateY(-20px); transition-delay: 0s; }
@media (prefers-reduced-motion: reduce) {
  [data-snap-group] > * { opacity: 1; transform: none; }
}

/* ---------- Contact ---------- */
.contact-card {
  background: var(--violet-900);
  color: var(--white);
  border-radius: var(--radius-lg);
  padding: var(--space-10) var(--space-6);
  text-align: center;
}
.contact-card h2, .contact-card h4 { color: var(--white); }
.contact-email { font-family: var(--font-display); font-size: var(--text-xl); color: var(--purple-300); background: none; border: 0; padding: 0; cursor: pointer; }
.contact-social { display: flex; justify-content: center; gap: var(--space-4); margin-top: var(--space-6); }
.contact-social a { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line-dark); display: flex; align-items: center; justify-content: center; }
.contact-social a:hover { background: rgba(255,255,255,0.08); }

/* Toast confirming the email address was copied; fixed to the viewport
   bottom (not the contact card) so it reads consistently regardless of
   scroll position when the button is clicked. */
.copy-toast {
  position: fixed;
  left: 50%;
  bottom: var(--space-6);
  transform: translate(-50%, 12px);
  background: var(--violet-900);
  color: var(--white);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-pop);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
  z-index: 999;
}
.copy-toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.contact-social svg { width: 20px; height: 20px; }

/* Mobile/tablet tap toast for tool descriptions - same frosted-glass look
   as the desktop .tool-card::after tooltip, same text (data-tooltip),
   fixed to the viewport bottom so it reads regardless of scroll position. */
.tool-toast {
  position: fixed;
  left: 50%;
  bottom: var(--space-6);
  transform: translate(-50%, 12px);
  max-width: calc(100vw - var(--space-8));
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-md);
  background: rgba(20, 8, 40, 0.8);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--white);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out);
  z-index: 999;
}
.tool-toast.is-visible { opacity: 1; transform: translate(-50%, 0); }

/* ---------- Forms ---------- */
.field { display: flex; flex-direction: column; gap: var(--space-2); text-align: left; margin-bottom: var(--space-5); }
.field label { font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); }
.field input, .field textarea {
  min-height: 48px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--line-200);
  background: var(--white);
  color: var(--ink-900);
}
.field textarea { min-height: 140px; resize: vertical; }
.field-hint { font-size: var(--text-xs); color: var(--ink-500); }
.field-error { font-size: var(--text-xs); color: var(--danger); display: none; }
.field.has-error input, .field.has-error textarea { border-color: var(--danger); }
.field.has-error .field-error { display: block; }

/* ---------- Footer ---------- */
.site-footer { background: var(--violet-900); color: var(--on-dark-500); padding-block: var(--space-8); }
.footer-row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-5); }
.footer-links { display: flex; flex-wrap: wrap; gap: var(--space-5); }
.footer-links a:hover { color: var(--white); }
.footer-fine { font-size: var(--text-xs); margin-top: var(--space-6); }

/* ---------- Breadcrumb ---------- */
.breadcrumb { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--on-dark-500); margin-bottom: var(--space-5); }
.breadcrumb a:hover { color: var(--white); }

/* ---------- Filter pills (portfolio) ---------- */
.filter-row { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-8); }
.filter-pill {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm);
  padding: var(--space-2) var(--space-5); border-radius: var(--radius-pill);
  border: 1.5px solid var(--line-200); color: var(--ink-700);
}
.filter-pill[aria-pressed="true"] { background: var(--purple-600); border-color: var(--purple-600); color: var(--white); }
.filter-pill:hover { border-color: var(--purple-600); }

.project-grid { display: grid; gap: var(--space-6); grid-template-columns: 1fr; }
@media (min-width: 700px) { .project-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .project-grid { grid-template-columns: repeat(3, 1fr); } }

.project-card { border-radius: var(--radius-lg); overflow: hidden; background: var(--white); border: 1px solid var(--line-200); box-shadow: var(--shadow-card); transition: transform var(--duration-base) var(--ease-out); }
.project-card:hover { transform: translateY(-6px); }
.project-card .thumb { aspect-ratio: 4/3; background: var(--violet-100); }
.project-card .thumb img { width: 100%; height: 100%; object-fit: cover; }
.project-card-body { padding: var(--space-5); }
.project-card .tag { margin-bottom: var(--space-3); }

.placeholder-note {
  border: 1.5px dashed var(--purple-300);
  background: var(--violet-50);
  color: var(--purple-700);
  border-radius: var(--radius-sm);
  padding: var(--space-4) var(--space-5);
  font-size: var(--text-sm);
  margin-bottom: var(--space-8);
}

/* ---------- Scroll story: Hero <-> Pick-a-tool pinned crossfade ----------
   A tall spacer (3 viewport-heights: hero dwell / crossfade / tools dwell)
   hosts two fixed full-screen layers whose opacity is scroll-driven by JS,
   creating the illusion of a fixed backdrop while content fades in and out
   instead of scrolling. Falls back to normal stacked sections for
   prefers-reduced-motion or if JS doesn't run. */
.scroll-story { position: relative; height: 300vh; height: 300dvh; }
.scroll-story__backdrop {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-color: var(--violet-900);
  background-image: radial-gradient(120% 100% at 50% 0%, rgba(58, 31, 110, 0.4) 0%, rgba(27, 11, 57, 0.55) 60%), url('/assets/images/backgrounds/hero.webp');
  background-size: cover;
  background-position: center;
  /* Blur is driven per-frame from main.js (scroll-story module), synced to
     the same progress/timing as Hero's own fade-out - see BLUR_MAX there. */
  will-change: filter;
}
.scroll-story__layer {
  position: fixed;
  inset: 0;
  z-index: 2;
  opacity: 0;
  pointer-events: none;
  background: transparent;
  color: var(--white);
  display: flex;
  align-items: center;
  overflow: hidden;
}
.scroll-story--static { height: auto; }
.scroll-story--static .scroll-story__backdrop { display: none; }
.scroll-story--static .scroll-story__layer {
  position: static;
  opacity: 1;
  pointer-events: auto;
  background-color: var(--violet-900);
  background-image: radial-gradient(120% 100% at 50% 0%, rgba(58, 31, 110, 0.4) 0%, rgba(27, 11, 57, 0.55) 60%), url('/assets/images/backgrounds/hero.webp');
  background-size: cover;
  background-position: center;
  min-height: 100vh;
  min-height: 100dvh;
}

/* About section: a normal (non-pinned) block, but raised above the fixed
   Hero/Tools layers so that as it scrolls up in the ordinary course of the
   page, its opaque white background visually slides over and covers Tools
   underneath - no extra animation needed, just correct stacking. */
#who { position: relative; z-index: 5; background: var(--white); border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
/* Experience stats and closing CTA sit centered beneath the photo+text+tags block,
   spanning the same width as the .split block above (no extra max-width) so the
   two stay visually aligned. .stat proportions are scaled to 0.6x the default
   size so the block reads as a light accent, not a second hero-scale statement. */
#who .stats-row { text-align: center; }
#who .stat { padding-inline: calc(var(--space-3) * 0.6); }
#who .stat b { font-size: calc(var(--text-3xl) * 0.6); }
#who .stat-label { font-size: calc((var(--text-3xl) - 8pt) * 0.6); }
#who .stat-icon { width: 42px; height: 42px; margin: calc(var(--space-3) * 0.6) 0; }
#who .stat-caption { font-size: calc(var(--text-xs) * 1.2); }
#who .stat-progress { max-width: 120px; margin-inline: auto; }
#who .hero-actions { justify-content: center; margin-top: var(--space-8); }

/* ---------- Tool companion: site-wide floating widget ----------
   Desktop: a small badge that trails the cursor at an offset.
   Mobile/tablet: a draggable floating "buddy" the visitor can reposition. */
.tool-companion {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 500;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: rgba(27, 11, 57, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: 0 8px 24px rgba(27, 11, 57, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transform: translate(-9999px, -9999px);
  transition: transform var(--duration-fast) var(--ease-out);
}
/* Parks off-screen while inactive; must be cancelled here or the JS-driven
   left/top position (both desktop follow and mobile drag) never shows up. */
.tool-companion.is-active { opacity: 1; transform: none; }
/* Mobile: a light pop while the visitor is actively dragging the buddy */
.tool-companion.is-active.is-dragging { transform: scale(1.15); }
.tool-companion img { width: 30px; height: 30px; }
/* Desktop: sized to sit right next to the cursor, at roughly 1.5x its own
   base scale, instead of reading as a separate floating badge. Bumped an
   extra 0.3x on top of that (client request) for better on-screen presence. */
@media (min-width: 1024px) {
  .tool-companion { width: 55px; height: 55px; }
  .tool-companion img { width: 31px; height: 31px; }
}
@media (max-width: 1023px) {
  .tool-companion.is-active { pointer-events: auto; cursor: grab; }
  .tool-companion.is-active:active { cursor: grabbing; }
}
@media (prefers-reduced-motion: reduce) {
  .tool-companion { transition: none; }
}

/* ---------- Utilities ---------- */
.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ---------- Scroll reveal ----------
   Generic fade+rise for static sections (About, Skills, Approach, FAQ,
   Contact) so they don't pop in abruptly next to the site's pinned scroll
   effects. --reveal-i is set inline per child by main.js on the direct
   children of a [data-reveal-group], driving the stagger purely through
   transition-delay so the JS never touches animation styles directly.
   Never put this on .scroll-story layers (Hero/Tools already write their
   own opacity every frame — a second system writing opacity there fights
   the first and both cancel out) or on [data-pause-item] (Visual Pause has
   its own dedicated reveal already). */
[data-reveal] {
  opacity: 0;
  transform: translateY(var(--reveal-dist));
  transition: opacity var(--reveal-dur) var(--reveal-ease),
              transform var(--reveal-dur) var(--reveal-ease);
  transition-delay: calc(var(--reveal-i, 0) * var(--reveal-stagger));
  will-change: opacity, transform;
}
[data-reveal].is-revealed { opacity: 1; transform: none; }

/* ---------- Portfolio archive (/portfolio/) ----------
   New portfolio page: illustrated hero (reuses the home hero.webp, same
   background-image recipe as .scroll-story__backdrop, just non-pinned) with
   category chips anchored at the bottom, an image-first card grid on a flat
   #1B0939 panel, and a teaser lightbox for in-progress projects. Kept as its
   own component set (.chip-filter/.portfolio-card/...) instead of reusing
   .filter-pill/.project-card so the old portfolio.html classes above stay
   untouched and can't leak conflicting rules onto the new markup. */
.portfolio-hero {
  position: relative;
  padding-top: var(--space-10);
  padding-bottom: var(--space-8);
  background-color: var(--violet-900);
  background-image: radial-gradient(120% 100% at 50% 0%, rgba(58, 31, 110, 0.4) 0%, rgba(27, 11, 57, 0.55) 60%), url('/assets/images/backgrounds/hero.webp');
  background-size: cover;
  background-position: center;
  text-align: center;
}
.portfolio-hero h1 { color: #FBECFF; text-shadow: 0 0 3px rgba(251, 236, 255, 0.6), 0 0 7px rgba(251, 236, 255, 0.6); margin-inline: auto; }
.portfolio-hero h2 { color: #FBECFF; font-weight: 600; margin-inline: auto; margin-bottom: 0; }
.portfolio-hero .eyebrow { margin-bottom: var(--space-5); }

.portfolio-filters {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin-top: 30px;
}
@media (max-width: 640px) { .portfolio-filters { gap: var(--space-2); padding-top: 10px; } }

/* Chip filter: same two visual states as the hero CTAs (ghost-glass /
   primary-glass), just at chip scale - a dedicated class instead of reusing
   .btn so the 10px/10px padding override doesn't have to fight .btn's own
   padding shorthand and min-height. */
.chip-filter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  font-size: var(--text-xs);
  padding: 10px 10px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--white);
  background: transparent;
  border: 1.5px solid var(--line-dark);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  transition: background-color var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.chip-filter::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(140px circle at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.45), transparent 60%);
  transition: opacity var(--duration-base) var(--ease-out);
}
.chip-filter:hover::after { opacity: 1; }
.chip-filter:hover { border-color: var(--white); background: rgba(255, 255, 255, 0.08); }
.chip-filter:active { transform: scale(0.97); }
.chip-filter[aria-pressed="true"] { background: rgba(102, 1, 255, 0.55); border-color: transparent; box-shadow: var(--shadow-pop); }
.chip-filter[aria-pressed="true"]:hover { background: rgba(102, 1, 255, 0.7); }
/* Mobile: roughly eyebrow-sized, as specced - at this size 5 chips still
   comfortably fit on one row at common phone widths (~360-430px), rather
   than the two rows the brief anticipated. */
@media (max-width: 640px) { .chip-filter { font-size: calc(var(--text-xs) - 2px); padding: 10px; } }

/* ---------- Portfolio grid + cards ---------- */
.portfolio-grid-section { background: #1B0939; }
.portfolio-grid { display: grid; gap: var(--space-6); grid-template-columns: 1fr; }
@media (min-width: 700px) { .portfolio-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .portfolio-grid { grid-template-columns: repeat(3, 1fr); } }

.portfolio-card {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--white);
  cursor: pointer;
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.portfolio-card:focus-visible { outline: 2px solid #FBECFF; outline-offset: 2px; }
.portfolio-card.is-filtered-out { opacity: 0; transform: scale(0.96); pointer-events: none; }

.portfolio-card-media { aspect-ratio: 4 / 3; overflow: hidden; background: var(--violet-100); }
.portfolio-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--duration-base) var(--ease-out); }

.portfolio-card-tags-wrap {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}
.portfolio-card-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); opacity: 0; transition: opacity var(--duration-base) var(--ease-out); }

.portfolio-card-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  padding: var(--space-5);
  background: rgba(27, 11, 57, 0.8);
  opacity: 0;
  transform: translateY(100%);
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
}
.portfolio-card-overlay h3 { color: var(--white); font-weight: 600; text-shadow: 0 0 4px rgba(255, 255, 255, 0.5); margin-bottom: var(--space-1); }
.portfolio-card-overlay p { color: var(--white); font-family: var(--font-body); font-weight: 500; font-size: var(--text-sm); margin: 0; }

.portfolio-card-footer { display: none; padding: var(--space-5); }
.portfolio-card-footer h3 { color: var(--ink-900); margin-bottom: var(--space-1); }
.portfolio-card-footer p { color: var(--ink-500); font-size: var(--text-sm); margin: 0; }

/* Desktop (>=700px, matches the grid's own 2-col breakpoint): image-only
   card at rest, hover/focus reveals the bottom-up overlay + sector tags +
   zoom + outer glow. */
@media (min-width: 700px) {
  .portfolio-card:hover, .portfolio-card:focus-within, .portfolio-card:focus {
    box-shadow: 0 0 7px rgba(251, 236, 255, 0.5);
  }
  .portfolio-card:hover .portfolio-card-media img,
  .portfolio-card:focus-within .portfolio-card-media img,
  .portfolio-card:focus .portfolio-card-media img { transform: scale(1.2); }
  .portfolio-card:hover .portfolio-card-overlay,
  .portfolio-card:focus-within .portfolio-card-overlay,
  .portfolio-card:focus .portfolio-card-overlay { opacity: 1; transform: translateY(0); }
  .portfolio-card:hover .portfolio-card-tags,
  .portfolio-card:focus-within .portfolio-card-tags,
  .portfolio-card:focus .portfolio-card-tags { opacity: 1; }
}

/* Mobile (<700px): keep the classic thumb+footer card, no hover overlay,
   sector tags always visible instead of hover-revealed. */
@media (max-width: 699px) {
  .portfolio-card-footer { display: block; }
  .portfolio-card-overlay { display: none; }
  .portfolio-card-tags { opacity: 1; }
}

/* Sector / status tag - shared by the card overlay, the mobile footer and
   the teaser lightbox. */
.pf-tag {
  display: inline-flex;
  align-items: center;
  font-family: "Anek Latin", "Segoe UI", sans-serif;
  font-weight: 400;
  font-size: 0.8rem;
  color: var(--white);
  background: rgba(40, 11, 97, 0.5);
  border: 1px solid var(--white);
  border-radius: var(--radius-pill);
  padding: 6px 10px;
  white-space: nowrap;
}

/* ---------- Teaser lightbox (in-progress projects) ---------- */
.teaser-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  background: rgba(27, 11, 57, 0.8);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-out);
}
.teaser-overlay.is-open { opacity: 1; pointer-events: auto; }
/* Frame itself never scrolls - only .teaser-modal-body does. That keeps
   .teaser-close (position:absolute against this element) anchored to the
   modal's own top-right corner regardless of how far the body content
   scrolls, instead of travelling with it. */
.teaser-modal {
  position: relative;
  width: 100%;
  max-width: 640px;
  max-height: 85vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--violet-900);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
  transform: translateY(16px) scale(0.98);
  transition: transform var(--duration-base) var(--ease-out);
}
.teaser-overlay.is-open .teaser-modal { transform: translateY(0) scale(1); }
.teaser-modal-media { aspect-ratio: 4 / 3; overflow: hidden; background: var(--violet-100); flex-shrink: 0; }
.teaser-modal-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* min-height:0 lets this flex item actually shrink to its assigned space
   and hand overflow to its own scrollbar, instead of stretching the flex
   column (and therefore the frame around .teaser-close) to fit the text. */
.teaser-modal-body { padding: var(--space-6); overflow-y: auto; min-height: 0; }
/* Desktop: media + body side by side inside a 5:3 landscape frame (was a
   stacked ~1:1 box). The media column is no longer sized by its own
   aspect-ratio - it stretches to the row's full height, so object-fit:
   cover on the img zooms/crops the source photo to fill it exactly. */
@media (min-width: 700px) {
  .teaser-modal { flex-direction: row; max-width: 880px; aspect-ratio: 5 / 3; }
  .teaser-modal-media { aspect-ratio: auto; flex: 0 0 44%; height: 100%; }
  /* flex-start, not center: with the Q&A accordion the body's content is
     usually taller than the 5:3 frame once expanded, and centring content
     that then scrolls opens the modal already scrolled past its own top
     (the start of the text sits above the fold). Collapsed by default
     (see HTML), so most opens still show extra breathing room below,
     not a wall of text - this only matters once someone expands a Q&A. */
  .teaser-modal-body { flex: 1 1 auto; height: 100%; display: flex; flex-direction: column; justify-content: flex-start; }
}
.teaser-modal-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
/* Title + status badge side by side. Row layout (not a bare sibling of
   .teaser-modal-body's own column flex) keeps .teaser-soon sized to its
   text - a flex COLUMN stretches auto-width children to fill the row by
   default, which is exactly what made the badge's pill background span
   the whole line when it lived directly in that column before. */
.teaser-modal-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-2); }
.teaser-modal-title-row h3 { margin-bottom: 0; }
.teaser-modal-body h3 { color: var(--white); font-weight: 600; text-shadow: 0 0 4px rgba(255, 255, 255, 0.5); font-size: var(--text-2xl); margin-bottom: var(--space-2); }
.teaser-tagline { color: var(--on-dark-500); font-size: var(--text-lg); margin-bottom: var(--space-4); }
.teaser-soon {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  flex-shrink: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: calc(var(--text-xs) - 2px);
  padding: var(--space-1) var(--space-4);
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.12);
  color: var(--white);
}
/* Q&A accordion inside the modal - reuses the home FAQ's <details name="…">
   markup/behaviour verbatim (native exclusive-accordion, zero JS). Only
   colour/spacing are overridden here, scoped to this container: the home
   version is tuned for a light section background, this one sits on
   --violet-900. */
.teaser-modal-qa { margin-top: var(--space-2); }
.teaser-modal-qa .faq-item { border-bottom-color: rgba(255, 255, 255, 0.18); }
.teaser-modal-qa .faq-q { padding: var(--space-3) 0; font-size: var(--text-base); color: var(--white); }
.teaser-modal-qa .faq-q svg { color: var(--purple-300); }
.teaser-modal-qa .faq-a { padding-bottom: var(--space-3); color: var(--on-dark-500); }
.teaser-close {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(27, 11, 57, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.35);
  color: var(--white);
  cursor: pointer;
}
.teaser-close:hover { background: rgba(27, 11, 57, 0.85); }
.teaser-close svg { width: 18px; height: 18px; }

/* ============================================================
   PORTFOLIO CASE STUDY — Ruikoro ("teca" .case-ruikoro-*)
   Scoped to /en(it)/portfolio/ruikoro/ only. Reuses the shared
   skeleton as-is: html[data-skill-scroll] + .snap-section +
   .section--screen (scroll-snap, header clearance, one-beat-per-
   screen sizing, all already generic/site-wide) and .trio-grid /
   .icon-tile / .btn.btn-glass for the icon columns and CTAs. Every
   rule below is a NEW scoped selector - nothing above this point
   in the file is modified, and no :root token is touched.
   Fraunces/Jost are loaded via <link> in the page's own <head>,
   then bound to the site's existing --font-display/--font-body
   custom properties *locally* on .case-ruikoro - every h1-h5/
   .lede/.btn already reads them through var(...), so the swap
   cascades without per-element overrides. */
.case-ruikoro {
  --font-display: "Fraunces", "Bricolage Grotesque", serif;
  --font-body: "Jost", "Anek Gujarati", sans-serif;
  --case-ruikoro-grad-1: linear-gradient(220deg, #0D1125, #353B5B);
  --case-ruikoro-grad-2: linear-gradient(220deg, #0D1125, #43709B);
  --case-ruikoro-grad-3: linear-gradient(220deg, #43709B, #BDF1FF);
}

/* Beat backgrounds: 3 shared 220deg gradients (vars on .case-ruikoro
   above), assigned per beat in the HTML. grad-1/grad-2 are dark, so
   headings/body run white throughout; grad-3 (the light closing beat)
   flips to dark #0D1125 text instead. Cards/columns inside a beat
   (.trio-item, .case-ruikoro-quad-item) are transparent on the
   gradient, so they inherit the same rule via the descendant selectors
   below - no separate per-card colour is needed. */
.case-ruikoro-grad-1 { background: var(--case-ruikoro-grad-1); }
.case-ruikoro-grad-2 { background: var(--case-ruikoro-grad-2); }
.case-ruikoro-grad-3 { background: var(--case-ruikoro-grad-3); }

.case-ruikoro-grad-1 h1, .case-ruikoro-grad-1 h2, .case-ruikoro-grad-1 h3,
.case-ruikoro-grad-1 h4, .case-ruikoro-grad-1 .eyebrow, .case-ruikoro-grad-1 .lede,
.case-ruikoro-grad-1 p, .case-ruikoro-grad-1 li, .case-ruikoro-grad-1 dd,
.case-ruikoro-grad-2 h1, .case-ruikoro-grad-2 h2, .case-ruikoro-grad-2 h3,
.case-ruikoro-grad-2 h4, .case-ruikoro-grad-2 .eyebrow, .case-ruikoro-grad-2 .lede,
.case-ruikoro-grad-2 p, .case-ruikoro-grad-2 li, .case-ruikoro-grad-2 dd { color: #FFFFFF; }

/* Contrast check: white on grad-2's lighter #43709B end measures ~5.2:1
   (passes AA for both body and headings), but a soft shadow - same
   treatment as the hero - adds margin for headings landing near that
   edge of the gradient. */
.case-ruikoro-grad-2 h1, .case-ruikoro-grad-2 h2, .case-ruikoro-grad-2 h3, .case-ruikoro-grad-2 h4 { text-shadow: 0 0 8px rgba(13, 17, 37, 0.35); }

.case-ruikoro-grad-3 h1, .case-ruikoro-grad-3 h2, .case-ruikoro-grad-3 h3,
.case-ruikoro-grad-3 h4, .case-ruikoro-grad-3 .eyebrow, .case-ruikoro-grad-3 .lede,
.case-ruikoro-grad-3 p, .case-ruikoro-grad-3 li, .case-ruikoro-grad-3 dd { color: #0D1125; }

/* Buttons: .btn-ghost.btn-glass (white/transparent by default) already
   reads correctly against grad-1/grad-2 with zero overrides - only the
   light grad-3 closing beat needs its own ink-on-light treatment, since
   white-on-light would be unreadable. .btn-primary/.btn-outline are
   avoided everywhere in this teca (both bake in the sitewide purple
   accent, which has no place in Ruikoro's palette). */
.case-ruikoro-grad-3 .btn.btn-ghost.btn-glass { color: #0D1125; border-color: rgba(13, 17, 37, 0.45); background: rgba(13, 17, 37, 0.06); }
.case-ruikoro-grad-3 .btn.btn-ghost.btn-glass:hover { background: rgba(13, 17, 37, 0.12); }

/* Icon-tile columns (Beat 2 / Beat 6) reuse .trio-grid/.trio-item/.icon-tile
   as-is; only the divider colour (sitewide var(--line-200), tuned for a
   light bg) needs a scoped override for the dark gradient backgrounds. */
.case-ruikoro-grad-1 .trio-item, .case-ruikoro-grad-2 .trio-item { border-color: rgba(255, 255, 255, 0.22) !important; }

/* One beat = one screen, desktop only: mobile keeps its natural scroll
   height (already signed off), but on desktop every beat must fit
   within 100vh without spilling past it. Padding is trimmed to buy
   room first; max-height + overflow:hidden is the hard backstop so a
   beat can never grow past the viewport even if content runs long. */
@media (min-width: 900px) {
  .case-ruikoro .section--screen { padding-block: var(--space-8); max-height: 100vh; overflow: hidden; }
  .case-ruikoro-hero { max-height: 100vh; }
}

/* ---------- Beat 1: Hero ---------- */
.case-ruikoro-hero {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  padding-block: var(--space-12);
  background-color: #185A74;
  background-image: url('/assets/images/portfolio/ruikoro/ruikoro-hero-background.webp');
  background-size: cover;
  background-position: center;
}
@media (max-width: 767px) { .case-ruikoro-hero { padding-block: var(--space-10); } }
.case-ruikoro-hero .container { position: relative; text-align: center; }
.case-ruikoro-hero .container, .case-ruikoro-hero h2, .case-ruikoro-hero .lede, .case-ruikoro-hero .eyebrow {
  color: #FFFAED;
  text-shadow: 0 0 6px rgba(24, 90, 116, 0.5);
}
.case-ruikoro-hero .eyebrow--glass { background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 255, 255, 0.3); }
/* Logo: a wide one-line logotype (mark + wordmark, real file is
   1116x307), not a square icon - sized by width with height:auto so it
   keeps its own ratio, capped so it never outgrows the hero column.
   Client asked for 3.5x the original 128px icon size (~448px) on both
   desktop and mobile, as the hero's foreground focal point; width:100%
   up to that cap keeps it from overflowing narrower phones. */
.case-ruikoro-logo-wrap { width: 100%; max-width: 448px; margin: var(--space-6) auto; }
.case-ruikoro-logo-wrap img { display: block; width: 100%; height: auto; margin-inline: auto; filter: drop-shadow(0 0 6px rgba(24, 90, 116, 0.5)); }
.case-ruikoro-hero h2 { margin-top: var(--space-2); }
.case-ruikoro-hero .lede { margin-inline: auto; max-width: 560px; }

/* Scheda progetto: scannable label/value strip, "-" allowed here as a
   UI separator (not prose). */
.case-ruikoro-scheda {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2) var(--space-3);
  max-width: 720px;
  margin: var(--space-6) auto;
  font-family: var(--font-body);
  font-size: var(--text-sm);
}
.case-ruikoro-scheda span:not(:last-child)::after { content: "\2014"; margin-left: var(--space-3); opacity: 0.6; }
.case-ruikoro-scheda b { font-weight: 600; }
.case-ruikoro-hero .hero-actions { justify-content: center; margin-top: var(--space-2); }

/* ---------- Beat 3 / Beat 4 / Beat 6-bis: text + mockup, alternating ---------- */
.case-ruikoro-split { display: grid; grid-template-columns: 1fr; gap: var(--space-8); align-items: center; }
@media (min-width: 900px) {
  /* Client asked for the text column to read wider than the image -
     tuned from an initial 3:2 to a softer 5:4 (text 5/9, image 4/9),
     found more harmonious - text stays the dominant element, image the
     supporting one, just less lopsided than 3:2. */
  .case-ruikoro-split { grid-template-columns: 5fr 4fr; gap: var(--space-8); }
  .case-ruikoro-split--reverse { grid-template-columns: 4fr 5fr; }
  .case-ruikoro-split--reverse .case-ruikoro-split-text { order: 2; }
  .case-ruikoro-split--reverse .case-ruikoro-split-media { order: 1; }
}
.case-ruikoro-split-media img { width: 100%; height: auto; max-width: 360px; margin-inline: auto; display: block; border-radius: var(--radius-lg); }
/* Desktop only (mobile sizing unchanged, still capped at 360px there):
   the mobile cap would otherwise also clamp the now-wider ~1fr image
   column, so it's lifted back to fill that column's actual width. */
@media (min-width: 900px) { .case-ruikoro-split-media img { max-width: 100%; } }

/* ---------- Beat 5: body + 4-column mockup grid ---------- */
.case-ruikoro-quad { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-6); margin-top: var(--space-6); }
@media (min-width: 900px) { .case-ruikoro-quad { grid-template-columns: repeat(4, 1fr); } }
.case-ruikoro-quad-item { text-align: center; }
/* Real mockups are "staged" scene shots (~2:1 portrait, taller than the
   plain phone-screen placeholders this replaced) - at the old 200px cap
   the 2x2 grid pushed #solution's height past ~2x viewport on narrow
   screens, which stops the snap-scroll reveal firing (its IntersectionObserver
   needs the section at >=55% of viewport, see main.js): capped to 120px so
   the section stays inside that threshold on real phone heights. */
.case-ruikoro-quad-item img { width: 100%; height: auto; max-width: 120px; margin-inline: auto; display: block; border-radius: var(--radius-md); margin-bottom: var(--space-4); }
@media (min-width: 900px) { .case-ruikoro-quad { margin-top: var(--space-5); } .case-ruikoro-quad-item img { max-width: 108px; margin-bottom: var(--space-2); } }

/* ---------- Beat 8 (closing): text+timeline left, mockup right, CTA centred below ---------- */
.case-ruikoro-closing-split { text-align: left; }
.case-ruikoro-closing-split .case-ruikoro-split-text h1 { font-size: var(--text-3xl); margin-bottom: var(--space-2); }
.case-ruikoro-closing-split .case-ruikoro-split-text h2 { font-size: var(--text-xl); }
.case-ruikoro-closing-split .case-ruikoro-split-text p { max-width: 52ch; }
.case-ruikoro-closing-split .case-ruikoro-split-media img { max-width: 280px; }
/* Desktop only. Unlike Beat 3/4's landscape scenes, this mockup is
   portrait (0.757 ratio) - inside the hard 100vh cap, sizing it by
   width like the others would make it taller than the section has
   room for. Capped by height instead so it grows as large as the
   viewport genuinely allows without triggering the overflow:hidden
   backstop and getting silently cropped. */
@media (min-width: 900px) {
  .case-ruikoro-closing-split .case-ruikoro-split-media img { width: auto; max-width: 100%; max-height: 50vh; }
  /* This mockup is height-capped, not width-driven, so - unlike Beat
     3/4 - its column doesn't need to stay narrow to "make room": extra
     width there just sits unused. Giving text back the wider column
     (vs. the shared 0.7/1.7 split) matters in IT, where the closing
     paragraph is long enough that the narrow column pushed text taller
     than the image and became the section's real height driver. */
  .case-ruikoro-closing-split { grid-template-columns: 1.15fr 0.85fr; }
  /* Closing's own CTA row sits right under the split - tightened here
     (sitewide .skills-cta default is space-10) to free up more of the
     100vh budget for the mockup above it. */
  #closing .skills-cta { margin-top: var(--space-5); }
}

/* Timeline: horizontal on both desktop and mobile - two points side by
   side, connected by a line spanning between the dots via
   justify-content:space-between (the line's left/right edges land
   exactly on the two dots because they sit at the row's own edges). */
.case-ruikoro-timeline { position: relative; display: flex; justify-content: space-between; gap: var(--space-4); margin: var(--space-5) 0 0; max-width: 420px; }
.case-ruikoro-timeline::before { content: ""; position: absolute; top: 6px; left: 6px; right: 6px; height: 2px; background: #0D1125; opacity: 0.4; }
.case-ruikoro-timeline-point { position: relative; display: flex; flex-direction: column; align-items: flex-start; text-align: left; gap: var(--space-2); max-width: 180px; }
.case-ruikoro-timeline-dot { flex-shrink: 0; width: 14px; height: 14px; border-radius: 50%; background: #0D1125; }
.case-ruikoro-timeline-label { font-family: var(--font-body); font-size: var(--text-sm); color: #0D1125; text-align: left; }
.case-ruikoro-timeline-label b { display: block; font-family: var(--font-display); font-weight: 600; color: #0D1125; margin-bottom: var(--space-1); }

/* ============================================================
   PORTFOLIO CASE STUDY — D-Stress ("teca" .case-dstress-*)
   Scoped to /en(it)/portfolio/d-stress/ only. Same skeleton reuse
   as .case-ruikoro-* above (scroll-snap, .section--screen, .btn/
   .trio-grid/.icon-tile), same "riporta gli aggiustamenti Ruikoro"
   structural rules (100vh cap desktop-only, height:auto on every
   mockup image, font/colour vars scoped locally, never :root).
   Every rule below is a NEW scoped selector — nothing above this
   point is modified. Lexend is loaded via <link> in the page's own
   <head>, bound locally to --font-display/--font-body. */
.case-dstress {
  --font-display: "Lexend", "Bricolage Grotesque", sans-serif;
  --font-body: "Lexend", "Anek Gujarati", sans-serif;
}
/* Client asked for 3 explicit weights by role, not just a family
   swap: Black on H1/strong titles, Bold on H2/subtitles, Light on
   body/label copy. H3/H4 (insight-card and duo-card titles) count
   as an additional "subtitle" tier so they take the H2 Bold weight;
   eyebrow (a label) and .lede (a tagline, prose-like) fall under the
   "body/label" bucket and take Light, same as running <p> copy. */
.case-dstress h1 { font-weight: 900; }
.case-dstress h2, .case-dstress h3, .case-dstress h4 { font-weight: 700; }
.case-dstress p, .case-dstress li, .case-dstress dd,
.case-dstress .eyebrow, .case-dstress .lede, .case-dstress-scheda { font-weight: 300; }

/* Two backgrounds, ONE shared text-colour pair on both (unlike
   Ruikoro's two distinct pairs) — client's instruction is explicit:
   same ink on either bg. Written as two classes anyway (not one
   shared selector) to match the established per-beat-class pattern
   and stay easy to diverge later if she ever asks for a difference. */
.case-dstress-bg-a { background: #C9C4B7; }
.case-dstress-bg-b { background: #EAE8E3; }
.case-dstress-bg-a h1, .case-dstress-bg-a h2, .case-dstress-bg-a h3,
.case-dstress-bg-a h4, .case-dstress-bg-a .eyebrow, .case-dstress-bg-a .lede,
.case-dstress-bg-b h1, .case-dstress-bg-b h2, .case-dstress-bg-b h3,
.case-dstress-bg-b h4, .case-dstress-bg-b .eyebrow, .case-dstress-bg-b .lede { color: #403937; }
.case-dstress-bg-a p, .case-dstress-bg-a li, .case-dstress-bg-a dd,
.case-dstress-bg-b p, .case-dstress-bg-b li, .case-dstress-bg-b dd { color: #525252; }

/* Buttons: unlike Ruikoro (dark beats where the default white/glass
   ghost button already read fine, ink-on-cream needed only for one
   light closing beat), EVERY D-Stress background is light/warm —
   so the ink treatment is applied once at the teca root, not per-bg. */
.case-dstress .btn.btn-ghost.btn-glass { color: #403937; border-color: rgba(64, 57, 55, 0.35); background: rgba(64, 57, 55, 0.06); }
.case-dstress .btn.btn-ghost.btn-glass:hover { background: rgba(64, 57, 55, 0.12); }

/* One beat = one screen, desktop only — same hard cap as Ruikoro. */
@media (min-width: 900px) {
  .case-dstress .section--screen { padding-block: var(--space-8); max-height: 100vh; overflow: hidden; }
  .case-dstress-hero { max-height: 100vh; }
}

/* ---------- Beat 1: Hero ---------- */
.case-dstress-hero {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  padding-block: var(--space-12);
  background-color: #C9C4B7;
  background-image: url('/assets/images/portfolio/d-stress/d-stress-hero-background.webp');
  background-size: cover;
  background-position: center;
}
@media (max-width: 767px) { .case-dstress-hero { padding-block: var(--space-10); } }
.case-dstress-hero .container { position: relative; text-align: center; }
/* No glow/text-shadow on this hero — explicit client instruction
   ("nessuna indicazione diversa" wasn't given, so plain #403937). */
.case-dstress-hero .container, .case-dstress-hero .eyebrow, .case-dstress-hero .lede { color: #403937; }
.case-dstress-hero .eyebrow--glass { background: rgba(64, 57, 55, 0.08); border-color: rgba(64, 57, 55, 0.25); }
/* Logo: wide one-line logotype (real file viewBox 2806x772, ratio
   ~3.64 — same shape as Ruikoro's), sized by width with height:auto.
   448px cap matches the house convention already set on Ruikoro's
   hero logo (prominent, foreground focal point). */
.case-dstress-logo-wrap { width: 100%; max-width: 448px; margin: var(--space-6) auto; }
.case-dstress-logo-wrap img { display: block; width: 100%; height: auto; margin-inline: auto; }
.case-dstress-hero .lede { margin-inline: auto; max-width: 560px; }

/* Scheda progetto: scannable label/value strip, "—" allowed here as
   a UI separator (not prose). */
.case-dstress-scheda {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2) var(--space-3);
  max-width: 720px;
  margin: var(--space-6) auto;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: #403937;
}
.case-dstress-scheda span:not(:last-child)::after { content: "\2014"; margin-left: var(--space-3); opacity: 0.6; }
.case-dstress-scheda b { font-weight: 700; }
.case-dstress-hero .hero-actions { justify-content: center; margin-top: var(--space-2); }

/* ---------- Beat 3 / Beat 4: text + mockup, alternating ----------
   Real mockups are square (2761x2760 / 2760x2760), not landscape
   like Ruikoro's — sizing them by width the same way would make
   them as tall as they are wide and blow the hard 100vh cap. Same
   principle used for Ruikoro's portrait Beat 7 image: cap by
   max-height on desktop instead, let width auto-follow. */
.case-dstress-split { display: grid; grid-template-columns: 1fr; gap: var(--space-8); align-items: center; }
@media (min-width: 900px) {
  .case-dstress-split { grid-template-columns: 0.9fr 1.1fr; gap: var(--space-8); }
  .case-dstress-split--reverse { grid-template-columns: 1.1fr 0.9fr; }
  .case-dstress-split--reverse .case-dstress-split-text { order: 2; }
  .case-dstress-split--reverse .case-dstress-split-media { order: 1; }
}
.case-dstress-split-media img { width: auto; height: auto; max-width: 100%; margin-inline: auto; display: block; border-radius: var(--radius-lg); }
@media (max-width: 899px) { .case-dstress-split-media img { max-width: 380px; } }
@media (min-width: 900px) { .case-dstress-split-media img { max-height: 62vh; } }

/* ---------- Beat 5: solution "vetrina" — body + single showcase
   image, given more room than Beat 3/4 since there's no competing
   3/4-up grid, just one image meant to be the craft moment. Real
   file is portrait (0.75 ratio, matches Ruikoro's closing mockup
   shape), so height-capped the same way, just a taller cap. ---------- */
.case-dstress-split-media--feature img { max-width: 320px; }
@media (min-width: 900px) { .case-dstress-split-media--feature img { max-height: 72vh; } }

/* ---------- Beat 6: "duo" — 2 real UI screenshots (not abstract
   icons like Ruikoro's icon-tile SVGs) next to title+body, 2
   columns. Sized bigger than an icon-tile since these are photo
   crops meant to actually show the extrusion/palette, not glyphs. */
.case-dstress-duo { display: grid; grid-template-columns: 1fr; gap: var(--space-8); margin-top: var(--space-6); }
@media (min-width: 640px) { .case-dstress-duo { grid-template-columns: repeat(2, 1fr); } }
.case-dstress-duo-item { text-align: center; }
.case-dstress-duo-item img { width: auto; height: auto; max-width: 220px; margin-inline: auto; display: block; border-radius: var(--radius-md); margin-bottom: var(--space-4); }
@media (min-width: 900px) { .case-dstress-duo-item img { max-height: 38vh; } }

/* ---------- Beat 7: closing — plain centred text (no split/mockup
   in this brief, unlike Ruikoro's), disclaimer line set bold. ---------- */
.case-dstress-disclaimer { font-weight: 700; max-width: 60ch; margin-inline: auto; }

/* ---------- Beat 7: closing — text+disclaimer left, mockup right, CTA
   centred below. Mirrors .case-ruikoro-closing-split exactly: flag-left
   text column, portrait mockup capped by height (not width) on desktop
   so it can grow as large as the 100vh budget allows without the split's
   generic max-height:62vh cap keeping it smaller than it needs to be. */
.case-dstress-closing-split { text-align: left; }
.case-dstress-closing-split .case-dstress-split-text h1 { font-size: var(--text-3xl); margin-bottom: var(--space-2); }
.case-dstress-closing-split .case-dstress-split-text h2 { font-size: var(--text-xl); }
.case-dstress-closing-split .case-dstress-split-text p { max-width: 52ch; margin-inline: 0; }
.case-dstress-closing-split .case-dstress-split-media img { max-width: 280px; }
@media (min-width: 900px) {
  .case-dstress-closing-split .case-dstress-split-media img { width: auto; max-width: 100%; max-height: 50vh; }
  /* Height-capped mockup doesn't need a narrow column to "make room" the
     way the landscape/square Beat 3-5 mockups do, so - as in Ruikoro -
     give the text column back the extra width instead of the shared
     0.9/1.1 split; the IT closing paragraph is long enough to be the
     section's real height driver otherwise. */
  .case-dstress-closing-split { grid-template-columns: 1.15fr 0.85fr; }
  #closing .skills-cta { margin-top: var(--space-5); }
}
