/* ============================================================================
   automatapro.ai — theme tokens + page layout
   Shares the visual world of the generated imagery: poured concrete floors,
   warm walnut millwork, brushed brass fixtures, frosted glass, one restrained
   warm-amber accent. Dense/dashboard aesthetic: small type, real numbers
   visible, technical register (see brief.md).
   ========================================================================== */

:root {
  /* --- six colour roles -------------------------------------------------- */
  --sc-canvas:     #17181b;  /* poured concrete, cool dark grey             */
  --sc-surface:    #201e1a;  /* raised panels, walnut-tinted dark           */
  --sc-ink:        #ece7dd;  /* frosted glass / drafting paper off-white    */
  --sc-ink-soft:   #a49c8e;  /* warm-tinted secondary text, never flat gray */
  --sc-accent:     #c98a35;  /* brushed brass / warm amber, one accent only */
  --sc-accent-ink: #17130c;  /* text sitting on the accent                  */

  --sc-font-display: "Archivo", system-ui, sans-serif;
  --sc-font-text:    "IBM Plex Sans", system-ui, sans-serif;
  --sc-font-mono:    "IBM Plex Sans", ui-monospace, monospace;

  --sc-shadow-color: 28 18% 3%;
}

/* --------------------------------------------------------------- reset -- */
body { overflow-x: clip; }
img { max-width: 100%; height: auto; }

/* -------------------------------------------------------------- nav bar -- */
.site-bar {
  position: fixed; inset: 0 0 auto 0;
  top: env(safe-area-inset-top, 0px);
  z-index: var(--sc-z-chrome);
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sc-3) var(--sc-gutter);
  pointer-events: none;
}
.site-bar__mark {
  display: flex; flex-direction: column; align-items: center; gap: var(--sc-1);
  pointer-events: auto;
  text-decoration: none;
  color: var(--sc-ink);
  font-family: var(--sc-font-display);
  font-weight: 600;
  font-size: var(--sc-t-sm);
  letter-spacing: var(--sc-track-snug);
}
/* Third size-bump round (client review, 60px -> ~120px doubled again on
   desktop). A full 120px on a 390px-wide phone would dominate the screen, so
   mobile gets a more modest bump instead (60px -> 80px) via the shared
   @media (max-width: 860px) breakpoint below, rather than doubling there too.
   Every downstream clearance fix this caused (hero-copy offset, .lp-back
   margin-top on all 5 sub-pages) is retuned alongside this change -- see
   their own comments for the new measured values. */
.site-bar__mark img { width: 120px; height: 120px; border-radius: var(--sc-r-sm); }
.site-bar__mark b { color: var(--sc-accent); font-weight: 600; }
/* Client correction: with the mark at 120px it's the tallest item in
   .site-bar's centred flex row, so its own top sits flush at the bar's
   padding-top (12px) -- Contattaci and the panel instead centre lower
   (62.375px, matching 3.9rem, see .panel's top comment). The client wants
   the logo lowered to meet THEM, not the other way around.
   position:relative + top (not margin-top) so this doesn't change the flex
   row's own content-box height -- a margin-top on the row's tallest item
   grows the row itself, which then re-centres Contattaci further down too
   and the two chase each other (measured: margin-top pushed Contattaci from
   62.375px to 87.55px, breaking the match this rule exists to create).
   Desktop only (matches every other nav-layout breakpoint in this file,
   861px); at mobile widths the mark is smaller (80px) and not the tallest
   item, so it already centres naturally with no push needed. */
@media (min-width: 861px) {
  .site-bar__mark { position: relative; top: 3.1479rem; }
}

.cta {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--sc-font-text);
  font-weight: 600;
  font-size: var(--sc-t-sm);
  padding: 0.7em 1.4em;
  border-radius: var(--sc-r-pill);
  text-decoration: none;
  white-space: nowrap;
  transition: transform var(--sc-d-fast) var(--sc-ease-out),
              background-color var(--sc-d-fast) var(--sc-ease-out);
}
.cta--nav {
  pointer-events: auto;
  background: color-mix(in oklab, var(--sc-surface) 70%, transparent);
  color: var(--sc-ink);
  border: 1px solid var(--sc-hairline-strong);
  backdrop-filter: blur(14px);
  /* Shifted left of .panel's own column (fixed, right: var(--sc-gutter),
     width 15rem) instead of the earlier fix of pushing .panel down to clear
     this button -- client wants Contattaci out of the panel's column
     entirely. margin-right eats space inside .site-bar's flex row
     (justify-content: space-between), moving the button's right edge left
     by that amount from the bar's own right padding edge (which sits at the
     same x as .panel's right edge), so 15rem clears the panel's full width
     plus a visible gap. Mobile override (narrower 12.5rem panel) below.
     Values verified empirically with Playwright across 5 viewports -- see
     nav-layout-reshuffle-report.md. */
  margin-right: calc(15rem + var(--sc-6));
}
.cta--primary { background: var(--sc-accent); color: var(--sc-accent-ink); border: none; }
.cta:hover { transform: translateY(-1px); }
.cta:active { transform: scale(0.97); }
.cta:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }

/* ------------------------------------------------------- nav actions --- */
/* Wraps .lang-switch + .cta--nav as .site-bar's single second flex child
   (EN pilot round, 2026-09-18), so .cta--nav's own tuned margin-right
   above (which clears .panel's column, verified across 5 viewports) still
   lands at the same position relative to .panel -- the margin lives on
   .cta--nav itself, unchanged, and .site-bar's space-between still has
   exactly two top-level children (.site-bar__mark, .site-bar__actions), so
   nothing about that layout math shifts. .lang-switch simply rides along
   just to .cta--nav's left inside the same box. */
.site-bar__actions { display: flex; align-items: center; gap: var(--sc-4); }

.lang-switch {
  display: flex; align-items: center; gap: var(--sc-1);
  pointer-events: auto;
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-snug);
}
.lang-switch__current,
.lang-switch__link {
  padding: 0.3em 0.35em;
  line-height: 1;
  border-radius: var(--sc-r-sm);
}
.lang-switch__current { color: var(--sc-ink); font-weight: 600; }
.lang-switch__link {
  color: var(--sc-ink-soft);
  text-decoration: none;
  transition: color var(--sc-d-fast) var(--sc-ease-out);
}
.lang-switch__link:hover,
.lang-switch__link:focus-visible { color: var(--sc-accent); }
.lang-switch::after {
  content: "";
  width: 1px; height: 0.9em;
  background: var(--sc-hairline-strong);
  margin-inline: 0.1em;
}

@media (max-width: 860px) {
  /* Same reasoning as .cta--nav's own mobile reset just below in this file:
     at 390/360px there is no room for a desktop-style shift, so the whole
     .site-bar__actions group (lang switch + Contattaci) sits flush at the
     bar's right padding edge, gap tightened slightly. */
  .site-bar__actions { gap: var(--sc-2); }
  .lang-switch { font-size: 0.7rem; }

  /* Client request (2026-09-19), after an earlier attempt at making the
     logo scroll away with the Hero on mobile caused a real production bug
     (the Hero's own scroll-pin mechanics detached the logo from normal
     page scroll entirely -- reverted). Simpler, safer ask instead: remove
     the logo from mobile altogether, site-wide (all 18 pages, home +
     sub-pages, IT/EN/PT -- this rule lives in the shared mobile block all
     of them load). .site-bar keeps `justify-content: space-between`
     between its two children (.site-bar__mark, .site-bar__actions); with
     .site-bar__mark display:none, that's now a single visible flex child,
     and space-between with one item resolves to flex-start (LEFT) --
     re-justify to flex-end so .site-bar__actions doesn't silently jump
     from the right edge to the left one now that there's nothing to
     space-between against. Verified empirically with Playwright, not just
     reasoned about: without this override .site-bar__actions actually
     did move to the left edge. */
  .site-bar__mark { display: none; }
  .site-bar { justify-content: flex-end; }
}

/* --------------------------------------------------------------- hero --- */
/* Two lightweight planes beyond the video itself, each moving at a
   different rate under data-sc-parallax so the hero reads as depth rather
   than a single background with text fades (hero-depth.md). The rear plane
   is the daylight haze reading further back than the desk; the near plane
   is the room's own foreground shadow, occluding the bottom edge and
   overtaking the frame at a faster rate as the reader scrolls. */
.hero-plane { position: absolute; inset: 0; pointer-events: none; }
.hero-plane--atmos {
  z-index: 1;
  background: radial-gradient(120% 60% at 78% 8%,
    color-mix(in oklab, var(--sc-ink) 14%, transparent) 0%, transparent 60%);
  mix-blend-mode: screen;
}
.hero-plane--near {
  z-index: 5;
  background: linear-gradient(to top,
    color-mix(in oklab, var(--sc-canvas) 92%, transparent) 0%,
    transparent 34%);
}
.hero-copy { display: flex; flex-direction: column; gap: var(--sc-4); align-items: flex-start; }
/* Overrides scrollcraft.css's .sc-copy--lead bottom-anchor (loaded first, so
   equal-specificity .hero-copy here wins). The nav logo mark growing pushes
   .site-bar taller; at common laptop heights the bottom-anchored badge/
   headline block can be tall enough to creep up under the mark. Trimming the
   anchor offset shifts the whole block down a bit, restoring clearance under
   the nav without touching the shared component.

   Retuned for the third round (mark doubled 60px -> 120px on desktop, mark
   bottom edge now at y=132 at every height once the nav's own top padding is
   included). The old -1.25rem trim (tuned for the 60px mark) left the badge
   overlapping the mark by ~16px at 1440x900. Widened to -3rem: re-measured,
   badge top now lands at y=145 at 1440x900 (~13px clear of the mark) and
   y=254 at 1440x1024 (comfortably clear). This also happens to reduce, not
   worsen, the PRE-EXISTING short-viewport overlap at 1280x720 (badge was
   already rendering off the top of the viewport at y=-22 with the old trim,
   at the OLD 60px mark size, before this round's change at all -- moving the
   whole block down brings it back on-screen, y=5.5, though the taller mark
   still overlaps the headline there; a full fix needs touching hero-copy's
   vh-based rhythm for short viewports, out of this change's scope, same as
   noted in the mobile 360x640 case below in the max-width:860 block). */
.hero-copy { bottom: calc(clamp(3rem, 12vh, 9rem) - 3rem); }
.badge {
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-wide);
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  border: 1px solid var(--sc-hairline-strong);
  border-radius: var(--sc-r-pill);
  padding: 0.4em 0.9em;
  margin: 0;
  /* Sits on the hero's brightest patch (the window behind the desk) at the
     start of the scrub, where the corner scrim has already thinned out this
     high up the frame -- an outline-only pill lets the bright glass show
     straight through the letterforms. An opaque-enough chip fill makes it
     read regardless of what the clip is doing underneath. */
  background: color-mix(in oklab, var(--sc-canvas) 62%, transparent);
}
.hero-copy h1 { max-width: 16ch; }
.ink-accent { color: var(--sc-accent); }
.hero-copy__desc { max-width: 42ch; }

/* ----------------------------------------------------------- benefits -- */
.act--benefits .sc-wrap { display: flex; flex-direction: column; gap: var(--sc-9); }
.benefits-intro { max-width: 48ch; }
.eyebrow {
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-wide);
  text-transform: uppercase;
  color: var(--sc-accent);
  margin: 0 0 var(--sc-2);
}
.benefits-intro h2 { margin-top: 0; }

.hero-stats {
  list-style: none;
  display: flex;
  gap: var(--sc-8);
  margin: var(--sc-7) 0 0;
  padding: var(--sc-4) 0;
  border-top: 1px solid var(--sc-hairline);
  border-bottom: 1px solid var(--sc-hairline);
}
.hero-stats li {
  display: flex;
  align-items: baseline;
  gap: var(--sc-2);
  font-family: var(--sc-font-mono);
}
.hero-stats span {
  font-family: var(--sc-font-display);
  font-weight: 600;
  font-size: var(--sc-t-xl);
  color: var(--sc-ink);
  font-variant-numeric: tabular-nums;
}
.hero-stats em {
  font-style: normal;
  font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-wide);
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}

.benefits-split { display: grid; grid-template-columns: 1.4fr 1fr; gap: var(--sc-8); align-items: start; margin-top: var(--sc-7); }
.benefits-lead { display: flex; flex-direction: column; gap: var(--sc-6); }
.benefits-figure { margin: 0; border-radius: var(--sc-r-lg); overflow: clip; box-shadow: var(--sc-e2); }
.benefits-figure img { display: block; width: 100%; height: auto; object-fit: cover; }

.benefit-metric { display: flex; flex-direction: column; gap: var(--sc-2); }
.benefit-metric__value {
  font-family: var(--sc-font-display);
  font-weight: 600;
  font-size: var(--sc-t-3xl);
  color: var(--sc-accent);
  letter-spacing: var(--sc-track-tight);
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.benefits-lead .benefit-metric__value { font-size: var(--sc-t-4xl); }
.benefit-metric h3 { margin: 0; font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-lg); }
.benefit-metric .sc-body { font-size: var(--sc-t-sm); }
.text-link {
  color: var(--sc-ink);
  font-weight: 600;
  font-size: var(--sc-t-sm);
  text-decoration-color: var(--sc-accent);
  text-underline-offset: 0.25em;
  width: fit-content;
}
.benefits-aside { display: flex; flex-direction: column; gap: var(--sc-6); }
.benefits-aside .sc-rule { margin-block: var(--sc-2); }

.benefits-extra { margin-top: var(--sc-7); border-top: 1px solid var(--sc-hairline); padding-top: var(--sc-7); }
/* This eyebrow sits above a 2x2 grid of headed items (h4s), not a single
   line of copy like every other .eyebrow on the page (Il Nostro Impegno,
   Referenze, the 3 portfolio industry tags, Email) -- it is functioning as a
   sub-section heading, not a micro-label, and read too small/subordinate at
   the shared .eyebrow size (client review). Scoped to this one instance so
   the shared class stays untouched for its genuine micro-label uses.

   Round 2 (client re-reported "nothing changed", and they were right): the
   Reverted per client review: after seeing it enlarged in the browser,
   the client preferred it back to match every other eyebrow on the page
   (Il Nostro Impegno, Referenze, the portfolio/solutions industry tags,
   Email) -- consistency with the rest of the page's small-label pattern
   over standing out as a bigger sub-heading. No scoped override needed;
   this falls back to the shared .eyebrow rule above. */
.extra-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sc-6) var(--sc-8); }
.extra-list li { padding-top: var(--sc-4); border-top: 1px solid var(--sc-hairline); display: flex; flex-direction: column; align-items: flex-start; gap: var(--sc-2); }
.extra-list h4 { font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-base); margin: 0 0 var(--sc-2); }
.extra-list .sc-body { font-size: var(--sc-t-sm); }

/* ------------------------------------------------------------ services -- */
/* .services-copy is vertically centred (align-items:center below), not
   bottom-anchored like .sc-copy--lead/--trail. The shared .sc-scrim--band is
   built for bottom-anchored copy (opaque at 0%, transparent by 58% up), which
   leaves the top ~40% of this frame undarkened -- exactly where the intro
   heading and first two steps sit. Point the scrim at the copy: darken a band
   centred on the vertical middle instead, tapering to transparent at both
   edges so the frame still reads at top and bottom. */
.act--services .sc-scrim--band {
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in oklab, var(--sc-canvas) 58%, transparent) 20%,
    color-mix(in oklab, var(--sc-canvas) 84%, transparent) 50%,
    color-mix(in oklab, var(--sc-canvas) 58%, transparent) 80%,
    transparent 100%);
}
.services-stage { display: flex; align-items: center; }
.services-copy { position: relative; z-index: var(--sc-z-copy); width: 100%; padding-inline: var(--sc-gutter); max-width: var(--sc-maxw); margin-inline: auto; display: grid; grid-template-columns: 0.9fr 1.1fr; gap: var(--sc-9); align-items: center; }
.services-intro h2 { margin-bottom: var(--sc-4); }
.services-intro .sc-body { max-width: 34ch; }

/* Il Pannello di Controllo (.panel, styled further down) is fixed top-right
   for the entire page, and this act is exactly where it lands fully resolved
   -- its own payoff moment (see script.js). Rather than shrink or reposition
   the panel during its own climax, give the steps column enough right-hand
   clearance that its text can never render under the panel's footprint.
   Scoped to widths where services-copy is still the 2-column grid: below
   860px it stacks to 1fr (see the shared breakpoint further down) and the
   panel itself drops to a small metrics-only strip that doesn't reach this
   column at all. */
@media (min-width: 861px) {
  .act--services .services-copy { padding-right: 17rem; }
}

.steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sc-5); }
.steps__item { display: flex; align-items: baseline; gap: var(--sc-4); }
.steps__mark {
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-sm);
  color: var(--sc-accent-ink);
  background: var(--sc-accent);
  width: 1.8em; height: 1.8em; flex: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 600;
}
.steps .sc-lede { max-width: 40ch; }

/* ----------------------------------------------------------------- team -- */
.team-intro { max-width: 48ch; margin-bottom: var(--sc-9); }
.team-body { display: grid; grid-template-columns: 0.9fr 1.1fr; gap: var(--sc-8); align-items: start; }
.team-figure { margin: 0; border-radius: var(--sc-r-lg); overflow: clip; box-shadow: var(--sc-e2); }
.team-figure img { display: block; width: 100%; height: auto; object-fit: cover; }

.expertise-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sc-6); }
.expertise-list li { padding-top: var(--sc-5); border-top: 1px solid var(--sc-hairline); display: flex; flex-direction: column; gap: var(--sc-2); }
.expertise-list li:first-child { padding-top: 0; border-top: none; }
.expertise-icon { width: 28px; height: 28px; color: var(--sc-accent); }
.expertise-list h3 { font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-lg); margin: 0; }
.expertise-list .sc-body { font-size: var(--sc-t-sm); }

.commitment { margin-top: var(--sc-10); padding-top: var(--sc-8); border-top: 1px solid var(--sc-hairline-strong); max-width: 56ch; }
.commitment__quote { font-size: var(--sc-t-xl); color: var(--sc-ink); margin-bottom: var(--sc-4); }

/* ------------------------------------------------------------ portfolio --
   Squeeze carousel (client review #6, replaces the old scroll-driven pan
   rail): a row of 3 panels, one active/wide, two compressed to narrow
   clickable strips. flex-grow is the only thing that transitions on
   click -- a discrete, one-shot transition, not a per-frame scroll-scrubbed
   one, so animating a layout property here doesn't fight this project's
   transform/opacity-only rule for continuous scroll motion. */
.portfolio-intro { max-width: 48ch; margin-bottom: var(--sc-6); }

.carousel { display: flex; gap: var(--sc-3); align-items: stretch; height: clamp(22rem, 46vw, 34rem); }
.carousel__panel {
  position: relative;
  flex: 1 1 0%;
  min-width: 0;
  border: 1px solid var(--sc-hairline-strong);
  border-radius: var(--sc-r-lg);
  overflow: clip;
  background: var(--sc-surface);
  box-shadow: var(--sc-e1);
  cursor: pointer;
  transition: flex-grow 520ms var(--sc-ease-out), box-shadow var(--sc-d-base) var(--sc-ease-out);
}
.carousel__panel:not(.is-active) { flex-grow: 1; }
.carousel__panel:not(.is-active):hover,
.carousel__panel:not(.is-active):focus-visible { flex-grow: 1.6; }
.carousel__panel.is-active { flex-grow: 8; cursor: default; box-shadow: var(--sc-e3); }
.carousel__panel:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: -3px; }

.carousel__media { position: absolute; inset: 0; z-index: 0; }
.carousel__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Solutions carousel variant: Solutions.tsx has no source photos (the real
   site uses lucide icons, not images, for these 4 cards), so this swaps the
   photographic <img> for a large centered icon on a tinted panel. Same
   scrim/spine/content layering on top still applies unchanged. */
.carousel__media--icon {
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(120% 100% at 50% 20%,
      color-mix(in oklab, var(--sc-accent) 16%, transparent) 0%, transparent 70%),
    var(--sc-surface);
}
.carousel__media--icon svg {
  width: clamp(2.75rem, 9vw, 4.5rem);
  height: clamp(2.75rem, 9vw, 4.5rem);
  color: var(--sc-accent);
  opacity: 0.85;
}
/* Scrim so the spine label and the active panel's copy both stay legible
   over whatever the photo is doing underneath. */
.carousel__panel::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to top,
    color-mix(in oklab, var(--sc-canvas) 88%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 38%, transparent) 46%,
    color-mix(in oklab, var(--sc-canvas) 66%, transparent) 100%);
}

.carousel__content {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; justify-content: flex-end; gap: var(--sc-2);
  padding: var(--sc-6);
  opacity: 0;
  visibility: hidden;
  transition: opacity 280ms var(--sc-ease-out), visibility 0s linear 280ms;
}
.carousel__panel.is-active .carousel__content {
  opacity: 1;
  visibility: visible;
  transition: opacity 320ms var(--sc-ease-out) 180ms, visibility 0s linear;
}
.carousel__content h3 { color: var(--sc-ink); font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-xl); margin: 0; }
.carousel__content .sc-body { font-size: var(--sc-t-sm); max-width: 42ch; }
.carousel__content .text-link { font-size: var(--sc-t-xs); }
/* flex-wrap + min-width:0 here matter beyond narrow-screen tidiness: in the
   compressed strip state this content block is much narrower than its
   natural/unwrapped width, and .carousel__panel clips overflow. A
   non-wrapping flex row pushes the second metric fully outside that clip
   box while compressed -- invisible (harmless) but also geometrically
   non-intersecting, which was silently keeping its fire-once
   IntersectionObserver counter (scrollcraft.js's own entry-counter block)
   from ever triggering until the panel was later widened (found in
   verification: the "soddisfazione" counter stayed at 0 indefinitely while
   its panel was compressed). Wrapping keeps every counter within its
   panel's actual box at every width, so it counts up as soon as the panel
   scrolls into view, active or not. */
.carousel__content .case__metrics { display: flex; flex-wrap: wrap; row-gap: var(--sc-1); gap: var(--sc-2) var(--sc-4); font-size: var(--sc-t-sm); color: var(--sc-accent); font-weight: 600; font-variant-numeric: tabular-nums; }
.carousel__content .case__metrics span { min-width: 0; }
.carousel__content .case__metrics b { font-family: var(--sc-font-display); font-size: var(--sc-t-lg); }

/* Compressed-strip label: reads as a vertical spine so a case still
   identifies itself narrow. Hidden once its panel is the active hero. */
.carousel__spine {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: flex-end; justify-content: center;
  padding: var(--sc-5) 0;
  writing-mode: vertical-rl;
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-wide);
  text-transform: uppercase;
  color: var(--sc-ink);
  white-space: nowrap;
  opacity: 1;
  transition: opacity 200ms var(--sc-ease-out);
  pointer-events: none;
}
.carousel__panel.is-active .carousel__spine { opacity: 0; }

.portfolio-note { color: var(--sc-ink-soft); margin-top: var(--sc-6); }

/* Referenze/Portfolio single-card "case deck" (client, 2026-09-19: after
   two earlier single-card attempts were rejected in favour of the
   squeeze-carousel -- see #solutions-carousel, unchanged below -- the
   client came back with a concrete reference (a Stripe/Linear-style
   testimonial component: giant ghost index number with cursor parallax,
   an oval badge, a large central line of copy, a name/role row, round
   prev/next buttons) and precise content-mapping instructions. Cards are
   absolutely stacked inside .case-deck__stage and crossfade only on
   opacity/transform (this project's rule for continuous motion), toggled
   by the same .is-active convention the squeeze-carousel uses elsewhere.
   Content mapping (client-specified, differs from the reference itself):
   the oval badge holds the case TITLE, not a company name or number --
   the sequential number already lives in the big ghost numeral, so
   repeating it in the badge was redundant. No scrolling company-name
   ticker (client: explicitly cut). See the dedicated case-deck init in
   script.js for the crossfade, autoplay and mouse-parallax logic. */
/* Client, round 6: wrap the whole component in the same bordered/rounded
   surface panel look as the Soluzioni squeeze-carousel's own panels
   (.carousel__panel below), instead of floating directly on the page
   background. */
.case-deck {
  position: relative;
  overflow: clip;
  padding: var(--sc-6);
  border: 1px solid var(--sc-hairline-strong);
  border-radius: var(--sc-r-lg);
  background: var(--sc-surface);
  box-shadow: var(--sc-e1);
}
.case-deck__stage { position: relative; min-height: clamp(15rem, 27vw, 19rem); }

/* Vertically centred on the stage (client, round 3: a top-anchored
   version -- the earlier fix for the company-name collision -- left the
   number floating disconnected above the text column instead of centred
   against it, which read worse than the collision it fixed). */
.case-deck__bignum {
  position: absolute;
  top: 50%; left: -0.02em;
  transform: translate(0, -54%);
  z-index: 0;
  font-family: var(--sc-font-display);
  font-weight: 700;
  font-size: clamp(8rem, 19vw, 14rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: color-mix(in oklab, var(--sc-accent) 34%, transparent);
  pointer-events: none;
  user-select: none;
  will-change: transform;
  transition: transform 200ms var(--sc-ease-out);
}

/* Horizontal slide (client, 2026-09-19: "deve scorrere a destra sinistra,
   non alto o basso" -- was a vertical crossfade, now a directional
   horizontal one). Transform is owned by script.js, not this class: the
   .is-active rule only handles opacity/pointer-events, because the same
   card needs two different resting transforms depending on whether it's
   about to enter or has just exited, which a single CSS rule can't express
   -- script.js sets the "from" position instantly (transition: none, one
   forced reflow), then the "to" position on the next frame so the
   transition var(--sc-d-slow) actually animates it. */
.case-deck__card {
  position: absolute; inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sc-5);
  max-width: 46rem;
  /* No padding of its own (round 6): the outer .case-deck box now supplies
     all the edge spacing, so the card just fills it -- stacking the card's
     own top/bottom padding on top of the box's, and the stage's min-height
     on top of both, was the dead-space-before-the-nav-buttons complaint. */
  padding: 0;
  opacity: 0;
  transform: translateX(0);
  pointer-events: none;
  /* Client feedback (round 3): even 680ms still read as too quick -- slowed
     further. --sc-ease-out -- cubic-bezier(0.23, 1, 0.32, 1) -- is a
     heavily front-loaded curve (most of the travel happens in the first
     ~100ms, everything after is a slow coast to rest), tuned for this
     site's scroll-reveal fades, not for a distance a viewer needs to
     visibly track. The transform gets its own even, ease-in-out curve
     instead so the slide reads at a steady pace across the whole duration;
     opacity keeps the site's standard curve. */
  transition: opacity 420ms var(--sc-ease-out), transform 1050ms cubic-bezier(0.65, 0, 0.35, 1);
}
.case-deck__card.is-active { opacity: 1; pointer-events: auto; }

.case-deck__badge {
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  border: 1px solid var(--sc-hairline-strong);
  border-radius: var(--sc-r-pill);
  background: var(--sc-surface);
  padding: var(--sc-2) var(--sc-4);
  margin: 0;
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  font-weight: 600;
  letter-spacing: var(--sc-track-wide);
  text-transform: uppercase;
  color: var(--sc-ink);
}

/* Client: 36ch was wrapping onto 3-4 short lines with a lot of unused
   width to their right, inside a card that has plenty of room (max-width
   46rem). Widened the measure and bumped the size a touch so it reads as
   fewer, fuller lines instead. */
.case-deck__desc {
  margin: 0;
  max-width: 54ch;
  font-family: var(--sc-font-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 0.95rem + 0.6vw, 1.3rem);
  line-height: 1.5;
  color: var(--sc-ink-soft);
}

.case-deck__footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sc-4) var(--sc-6); }
/* Client, round 9: the rule was counted as part of .case-deck__name-row's
   own width, so right-aligning that row (via the org block's align-items:
   flex-end) aligned the RULE's edge with the sector line below, not the
   company name's own last letter -- "KFS 30 LLC" and "Servizi
   Professionali" read as misaligned by exactly the rule's width. Taking
   the rule out of flow (absolutely positioned, anchored past the row's
   right edge) means the row's box is just the company text again, so it
   aligns with the sector line the normal way, and the rule sits outside
   that column instead of inside it. */
.case-deck__rule { position: absolute; left: 100%; top: 50%; margin-left: var(--sc-2); width: 1.25rem; height: 1px; background: var(--sc-accent); transform: translateY(-50%); }
.case-deck__name-row { position: relative; display: flex; align-items: center; }
/* Client, round 7: company name pushed to the row's right edge (was left,
   right after the rule) -- the link badge, when a case has one, now sits
   on the left instead, swapping the two sides. margin-left: auto on the
   org block (the flex item that comes after the link in markup order)
   pushes it and it alone to the far right; align-items: flex-end keeps
   the shorter sector line flush with the company name above it rather
   than left-hanging off the block's shrink-to-fit width. Sector line
   spacing notes from round 3 (tight line-height, zero gap) still apply. */
.case-deck__org { display: flex; flex-direction: column; align-items: flex-end; gap: 0; margin-left: auto; }
/* Client: swap the hierarchy -- company name is now the headline element
   (bigger, brand accent), the case description above it steps back to
   supporting body copy. */
.case-deck__company {
  margin: 0;
  font-family: var(--sc-font-display);
  font-weight: 700;
  font-size: clamp(1.5rem, 1.15rem + 1.6vw, 2.2rem);
  line-height: 1.05;
  letter-spacing: var(--sc-track-snug);
  color: var(--sc-accent);
}
.case-deck__sector { margin: 0; font-size: var(--sc-t-sm); color: var(--sc-ink-soft); }

/* Client, round 4: moved out of .case-deck__footer into its own row right
   after the description (still markup order desc -> metrics -> footer) --
   sharing a flex row with the company name meant a long name pushed the
   metrics onto a second line, so their vertical position shifted with
   every case. As a sibling of the footer instead, they sit at a fixed
   spot regardless of company-name length. */
.case-deck__card .case__metrics {
  display: flex; flex-wrap: wrap; gap: var(--sc-2) var(--sc-4);
  font-size: var(--sc-t-sm); color: var(--sc-accent); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.case-deck__card .case__metrics b { font-family: var(--sc-font-display); font-size: var(--sc-t-lg); }

/* Client: the site link (DATEX today, potentially other cases later) was a
   plain underlined line stranded at the very bottom of the card. Now an
   oval badge sat inline in the footer row -- matches the look of the title
   badge above the description, just accent-coloured to read as a link
   rather than a label. Round 7: sits on the left, right after the rule
   (the company name/sector block is what now carries margin-left: auto to
   the row's right edge, swapped from the other way round). */
.case-deck__link {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  border: 1px solid var(--sc-accent);
  border-radius: var(--sc-r-pill);
  padding: var(--sc-1) var(--sc-4);
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  font-weight: 600;
  letter-spacing: var(--sc-track-wide);
  color: var(--sc-accent);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--sc-d-base) var(--sc-ease-out), color var(--sc-d-base) var(--sc-ease-out);
}
.case-deck__link svg { width: 0.85em; height: 0.85em; flex: 0 0 auto; }
.case-deck__link:hover, .case-deck__link:focus-visible {
  background: var(--sc-accent);
  color: var(--sc-accent-ink);
}

.case-deck__nav { position: relative; z-index: 2; display: flex; gap: var(--sc-3); margin-top: var(--sc-4); }
.case-deck__btn {
  position: relative;
  width: 3rem; height: 3rem;
  border-radius: var(--sc-r-pill);
  border: 1px solid var(--sc-hairline-strong);
  background: var(--sc-surface);
  color: var(--sc-ink);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  overflow: clip;
  transition: box-shadow var(--sc-d-base) var(--sc-ease-out);
}
.case-deck__btn::before {
  content: "";
  position: absolute; inset: 0;
  background: var(--sc-accent);
  transform: translateX(-100%);
  transition: transform var(--sc-d-base) var(--sc-ease-out);
}
.case-deck__btn[data-dir="next"]::before { transform: translateX(100%); }
.case-deck__btn:hover::before,
.case-deck__btn:focus-visible::before { transform: translateX(0); }
.case-deck__btn svg { position: relative; z-index: 1; color: inherit; transition: color var(--sc-d-base) var(--sc-ease-out); }
.case-deck__btn:hover svg,
.case-deck__btn:focus-visible svg { color: var(--sc-accent-ink); }
.case-deck__btn:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
/* Client: on phones the prev/next buttons drive an up/down slide instead
   of left/right (see script.js's isStackedAxis), so they show chevron-up/
   -down icons there instead of left/right -- each button ships both SVGs,
   CSS just swaps which one is visible per breakpoint. */
.case-deck__icon--v { display: none; }

@media (max-width: 860px) {
  .case-deck { padding: var(--sc-5); }
  .case-deck__stage { min-height: clamp(24rem, 100vw, 30rem); }
  .case-deck__bignum { font-size: clamp(5rem, 28vw, 8rem); top: 50%; transform: translate(0, -58%); }
  .case-deck__card { max-width: none; padding: 0; gap: var(--sc-4); }
  .case-deck__icon--h { display: none; }
  .case-deck__icon--v { display: block; }
  .case-deck__desc { max-width: none; font-size: clamp(0.95rem, 0.85rem + 1vw, 1.1rem); }
  .case-deck__company { font-size: clamp(1.35rem, 1.05rem + 3.2vw, 1.9rem); }
  /* Client: on phones the box's own padding (var(--sc-5), 1.5rem) is
     narrower than the rule's protrusion past the name row (was sized for
     desktop's roomier var(--sc-6) padding), so it ran past the edge with
     no breathing room left. Shrunk to fit inside the smaller mobile
     padding instead. */
  .case-deck__rule { width: 0.85rem; margin-left: var(--sc-1); }
}

@media (prefers-reduced-motion: reduce) {
  .case-deck__card, .case-deck__bignum, .case-deck__btn::before { transition-duration: 1ms !important; }
}

@media (max-width: 860px) {
  /* Three narrow columns don't work at phone widths even compressed; stack
     vertically instead, with the click transition on height rather than
     flex-grow so each collapsed case still reads as a labelled bar. */
  .carousel { flex-direction: column; height: auto; }
  .carousel__panel { flex: 0 0 auto; height: 4rem; transition: height 420ms var(--sc-ease-out), box-shadow var(--sc-d-base) var(--sc-ease-out); }
  .carousel__panel.is-active { height: 26rem; }
  .carousel__spine {
    writing-mode: horizontal-tb;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    padding: 0 var(--sc-4);
  }
}

@media (prefers-reduced-motion: reduce) {
  .carousel__panel, .carousel__content, .carousel__spine { transition-duration: 1ms !important; transition-delay: 0s !important; }
}

/* ------------------------------------------------------------- contact -- */
/* No longer a pinned one-viewport stage (client round 3 — see the long note
   on #contatti in index.html). It used to be `justify-content: center` inside
   a 900px sticky stage holding ~403px of content, which is where the ~250px
   of empty background above "Contattaci" came from, and `overflow-y: auto`
   existed only to rescue content from that fixed height budget. Both are gone:
   the section is now ordinary document flow sized by its own content, so the
   copy starts immediately under the Solutions carousel with nothing but normal
   section rhythm in between. */
/* Half the usual --sc-section lead-in above the contact card. With the pin
   removed, the boundary between the Solutions carousel and Contact was
   measured at 273px of empty background (129.6px of Solutions' bottom pad +
   129.6px of Contact's own top pad at 1440x900 — i.e. exactly the page's
   standard section rhythm, no longer a Contact-specific defect). That rhythm
   is right between two sections a visitor is still travelling through, but
   Contact is the terminal frame and its card carries a heavy internal padding
   and a visible border of its own, so it does not need the full run-up to
   read as a new section. Halving it brings the boundary to ~143px: an
   unmistakable section change with no dead frame. */
.act--contact.sc-section { padding-top: calc(var(--sc-section) / 2); }

/* ---- History, rounds 4-5 (superseded by round 6 below — kept because the
   diagnosis of bugs (a) and (b) is still correct; only the FIX for (a) has
   changed) --------------------------------------------------------------
   Round 4: un-pinning left this act sized purely by its content, which was
   592-763px across widths — SHORTER than any viewport it is shown in. (a) A
   section whose top lies past the page's maximum scroll offset is an anchor
   the browser can never satisfy, so every "Contattaci" target stalled at the
   page bottom and a click from there moved nothing: a link that visibly does
   nothing. (b) The same shortfall parked the footer nav row only 99-119px
   above the viewport's bottom edge, inside the fixed ElevenLabs launcher's
   hit-zone — hit-tested with elementFromPoint, footer links resolved to
   ELEVENLABS-CONVAI instead of themselves. Round 4's fix was a `min-height:
   100vh` floor, reasoned as the mathematically exact minimum: this act is
   the LAST thing in the document, so "top <= max scroll" expands to
   "actHeight >= viewport" exactly, and 100vh was treated as a hard floor
   that could not be shrunk without reopening bug (a).
   Round 5 fixed a side effect of that floor: on wide/short viewports the
   floor's slack piled up top-aligned AFTER the footer as dead background
   (401px at 1440x900, 559px at 1920x1080 — what the client screenshotted).
   The fix pushed that slack ABOVE the footer instead, via `display: flex`
   on the act plus `margin-bottom: auto` on .close__inner, landing the
   document's true end on the footer plus only the padding-bottom band below.
   That reduced TRAILING dead space after the footer, but did nothing about
   the gap the slack now occupied BEFORE the footer — which is exactly what
   round 6 below re-measures and fixes. */

/* ---- Round 6: the floor itself was the wrong shape of fix ------------
   Client report: "a large empty black area before the footer", worst on
   tall/narrow windows — a shape not in rounds 4-5's viewport set. Measured
   at a representative tall window (1041x1900) with round 5's rules intact:
   act natural (content-only) height is 662px, but `min-height: 100vh`
   forces the act to 1900px border-box, and round 5's flex/margin-auto
   funnels that entire 1238px of slack into the gap between the contact card
   and the footer — precisely the defect reported. Same mechanism, smaller
   magnitude, at every other size: gapBeforeFooter measured (consent
   pre-accepted, so the cookie banner doesn't skew the footer hit-test) at
   1920x1080 was 424px, at 1440x1024 389px, at 768x1024 302px, at 1041x1600
   962px — all pure dead space the floor manufactures and round 5 relocates,
   not content.

   Round 4's floor was justified on a narrower definition of bug (a) than the
   one actually reported: "#contatti's own top pixel must be scrollable to
   exactly the viewport top." But no code on this page requires that. Plain
   `<a href="#contatti">` clicks (rail-nav dots, footer links, header/footer
   CTAs) are native browser anchor navigation — scrollcraft.css sets `html {
   scroll-behavior: smooth }` and nothing in script.js intercepts these
   clicks (only the rail-nav's own IntersectionObserver, and the separate
   "Avanza"/"Torna su" buttons, which already clamp their own targets with
   `Math.min(target, maxScroll())` — see maxScroll()/next-click above,
   ~line 436). A browser clamps an unreachable anchor target to the
   document's true max scroll on its own; it does not refuse to scroll. So
   the REAL bug (a) is narrower: does a "Contattaci" click ever produce NO
   visible movement from a starting point that isn't already the true page
   bottom? That is testable directly, and is what should gate the floor —
   not literal top-pixel reachability.

   Re-verified with Playwright at every viewport below, with the floor
   removed entirely (padding-bottom band and round 5's --sc-section/2 top
   padding untouched) and round 5's now-pointless flex/margin-auto rules
   removed with it (they only had an effect while the floor supplied extra
   height to redistribute; with no floor, .close__inner is never taller than
   its own content, so the auto margin resolves to 0 everywhere and the
   flex column does nothing — dead code once the floor is gone):

     viewport      #contatti  natural  max scroll  reachable?  gapBeforeFooter  dead-link
                   top(px)    height                            before -> after   regression?
     1440x900       9407      659      9167  NO (-240)   265 -> 24px      no (moves at 0, mid; not-moving only at true bottom, same before/after)
     1440x1024       9903      659      9539  NO (-364)   389 -> 24px      no
     1920x1080      10463      681     10063  NO (-400)   424 -> 24px      no
     1280x720        8559      652      8491  NO  (-68)    92 -> 24px      no
     1024x768        8332      660      8224  NO (-108)   132 -> 24px      no
      768x1024      10588      746     10310  NO (-278)   302 -> 24px      no
       390x844       9686      886      9728  yes  (+42)    24 -> 24px     no (floor never bound here even before)
       360x640       8976      883      9218  yes (+242)    24 -> 24px     no (floor never bound here even before)
      1041x1600     11675      662     10737  NO (-938)   962 -> 24px      no
      1041x1900     12875      662     11637 NO (-1238)  1262 -> 24px      no

   "reachable? NO" now describes most sizes, same as round 4 found — but with
   the floor gone, a click from any starting position still ABOVE the page's
   true max scroll produces real scroll movement, lands on the clamped max
   (browser's native behaviour, confirmed via window.scrollY before/after —
   not a guess), and that clamped landing sits inside or right at the
   contact card: at the worst case (1920x1080, -400px), the visitor sees the
   bottom slice of the Soluzioni carousel bleeding in above a fully-visible
   Contact card and footer, not a blank section. That is the same kind of
   frame a visitor gets scrolling any ordinary page to its real end — not
   the round-4 defect, where the click did nothing at all because the target
   was unreachable from EVERY approach, mid-page included. Clicking from a
   mid-page starting position (tested at ~50% of max scroll, every size)
   still moves visibly at every viewport; only clicking from the position
   already AT max scroll is a no-op, which is correct — there is nowhere
   further to go. Footer/widget clearance (elementFromPoint hit-test on every
   footer link, cookie banner pre-accepted) still passes at every size,
   because the padding-bottom rule below is independent of the floor and
   unchanged.

   Net effect of removing the floor: gapBeforeFooter collapses to a flat 24px
   (the section-rhythm top padding above, halved by the rule at the top of
   this block) at every width instead of scaling up to 1262px on tall
   windows, with zero dead-link or widget-clearance regressions measured. No
   partial floor (e.g. 60vh) was needed — full removal already clears both
   bars round 4 set out to fix, re-verified rather than assumed. */

/* Widget-clearance band — independent of the floor above (it is inline
   padding inside the act's own border-box, not extra height contingent on
   min-height binding), and its own justification re-verified unchanged this
   round: on short windows the act's content is already taller than the
   viewport regardless of any floor, and without this band the footer nav
   hit-tests to ELEVENLABS-CONVAI instead of itself. Measured, not guessed:
   the collapsed launcher plus its speech bubble is ~95px tall and sits
   ~38px off the bottom edge (bubble spans y 712-806 in an 844px-tall
   window), i.e. a ~135px band. 10rem/160px clears it with ~25px to spare
   for the bubble's copy reflowing to an extra line. Unconditional rather
   than behind a breakpoint because the failure depends on viewport HEIGHT
   as much as width. */
.act--contact.sc-section { padding-bottom: calc(10rem + env(safe-area-inset-bottom, 0px)); }

.close {
  display: flex;
  flex-direction: column;
  gap: var(--sc-2);
  padding-inline: var(--sc-gutter);
  max-width: var(--sc-maxw);
  margin-inline: auto;
  width: 100%;
}
.close__inner { display: flex; flex-direction: column; }
.contact-grid { display: grid; grid-template-columns: 1.3fr 1fr; gap: var(--sc-6); align-items: start; }
.contact-box {
  background: color-mix(in oklab, var(--sc-surface) 82%, transparent);
  border: 1px solid var(--sc-hairline-strong);
  border-radius: var(--sc-r-lg);
  padding: var(--sc-5);
  box-shadow: var(--sc-e2);
  backdrop-filter: blur(10px);
}
.contact-box h2 { margin: 0 0 var(--sc-2); }
.contact-box > .sc-body { max-width: 46ch; }
.contact-box .sc-rule { margin-block: var(--sc-2); }
.contact-box h3 { font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-lg); margin: 0 0 var(--sc-1); }

/* Real behaviour (client review): "Richiedi contatto" is a plain outbound
   link to the Vercel-hosted contact form (repo/src/components/Contact.tsx),
   not an inline form on this page -- swapped in place of the former
   compact Nome/Email/Messaggio form + fetch()-based submit handler. */
.contact-box__link { margin-top: var(--sc-3); gap: 0.6em; }
.contact-box__link-icon { width: 1.1em; height: 1.1em; transition: transform var(--sc-d-fast) var(--sc-ease-out); }
.contact-box__link:hover .contact-box__link-icon { transform: translate(2px, -2px); }

.contact-card { padding: var(--sc-4); border: 1px solid var(--sc-hairline); border-radius: var(--sc-r-lg); display: flex; flex-direction: column; gap: var(--sc-2); }
.contact-card__email { color: var(--sc-ink); font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-lg); text-decoration-color: var(--sc-accent); }
.contact-card__tagline { font-size: var(--sc-t-xs); color: var(--sc-ink-soft); margin: 0; line-height: var(--sc-leading-body); }
.contact-card__social.foot__social { margin-left: 0; gap: var(--sc-4); }
.contact-card__social.foot__social svg { width: 22px; height: 22px; }

/* Il Pannello di Controllo (fixed top-right, see the panel block further
   down) holds its resolved state through this act too, and this is the
   act's own right-hand column -- give it the same right-hand clearance the
   Services act's step column already needed for the same reason (found in
   re-review: without it, the panel visually covers part of the tagline
   text at common desktop widths). Scoped to the same breakpoint the panel
   itself uses for its full 3x3 layout. */
@media (min-width: 861px) {
  .contact-grid { padding-right: 17rem; }
}

.foot {
  padding-top: var(--sc-4);
  border-top: 1px solid var(--sc-hairline);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sc-3) var(--sc-6);
  font-size: var(--sc-t-xs);
  color: var(--sc-ink-soft);
}
/* Compact footer for the Contact act on index.html. The one-viewport height
   budget that originally forced this is gone (the act is no longer pinned),
   but the tighter rhythm still reads better directly under .contact-grid than
   the full-size footer does, so it stays — now with real breathing room above
   it rather than a hard constraint. */
.foot--pinned { padding-top: var(--sc-4); margin-top: var(--sc-4); gap: var(--sc-1) var(--sc-6); }
.foot__wordmark { font-family: var(--sc-font-display); font-weight: 600; font-size: var(--sc-t-sm); color: var(--sc-ink); }
.foot__wordmark b { color: var(--sc-accent); font-weight: 600; }
.foot__links { display: flex; gap: var(--sc-4); }
.foot__links a { color: var(--sc-ink-soft); text-decoration: none; }
.foot__links a:hover { color: var(--sc-ink); }
.foot__social { display: flex; gap: var(--sc-3); margin-left: auto; }
.foot__social a { display: inline-flex; }
.foot__social svg { width: 16px; height: 16px; color: var(--sc-ink-soft); }
.foot__social a:hover svg { color: var(--sc-accent); }
.foot__legal { flex-basis: 100%; margin: 0; }
.foot__legal a { color: var(--sc-ink-soft); }

/* =========================================================================
   IL PANNELLO DI CONTROLLO — the signature move's static shell.
   All motion (transform/opacity) is written by script.js each frame, driven
   off each act's own --sc-p. This block only lays the nodes out and sets
   their resting (fully resolved) position/appearance.
   ========================================================================== */
.panel {
  position: fixed;
  z-index: var(--sc-z-chrome);
  /* 5.2rem -> 11rem -> back to 5.2rem -> 3.9rem -> var(--sc-3) -> back to
     3.9rem: three-way top alignment with the logo mark and Contattaci. Client
     correction: the var(--sc-3) round raised Contattaci and this panel up to
     match the logo's top -- the client actually wanted the opposite, the
     LOGO lowered to meet Contattaci/panel's original, lower position
     (62.375px, matching 3.9rem). So Contattaci is back to its default
     centred-in-.site-bar position (no align-self override) and this panel is
     back to 3.9rem; see .site-bar__mark's own margin-top (styles.css) for the
     matching logo push-down. */
  top: calc(env(safe-area-inset-top, 0px) + 3.9rem);
  right: var(--sc-gutter);
  width: 15rem;
  padding: var(--sc-4);
  border-radius: var(--sc-r-lg);
  border: 1px solid var(--sc-hairline-strong);
  background: color-mix(in oklab, var(--sc-surface) 55%, transparent);
  backdrop-filter: blur(16px);
  box-shadow: var(--sc-e2);
  pointer-events: none;
  /* Bugfix (client screenshot report, "un altro riquadro che non si capisce
     bene sotto"): the scattered nodes' rotated bounding boxes are wider/
     taller than their grid cell (a 68px cell holds an ~80px rotated square),
     so at low resolve-progress several nodes' corners genuinely rendered
     outside this box's own edge -- no backdrop blur, no shared background,
     just a bare rotated card floating over the hero photo, which is exactly
     the "unclear second box" the client saw. Clipping to this box's own
     rounded rect keeps every card contained in the one frame the visitor is
     meant to read as a single instrument. */
  overflow: hidden;
}
.panel__label {
  font-family: var(--sc-font-mono);
  font-size: 0.66rem;
  letter-spacing: var(--sc-track-wide);
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin: 0 0 var(--sc-3);
}
.panel__grid {
  position: relative;
  display: grid;
  /* minmax(0, 1fr), not bare 1fr: a grid item's default min-width is auto
     (its own min-content), so a column whose cell holds an unbreakable word
     ("RIDUZIONE", "COMPETENZE") can demand more than an equal third and the
     row still "succeeds" at 1fr -- it just grows past its share instead of
     wrapping. Measured this live (Playwright, desktop 1440px): the 3
     columns rendered at 74/67.5/60.69px instead of ~68.67px each, pushing
     .panel__grid's real content width to 226px inside its own 206px box.
     .panel's overflow:hidden (added for the rotated-card bug above) then
     silently clipped that 20px overshoot off the right edge -- carrying
     away both the "RIDUZIONE COSTI"/"PROSSIMO PASSO" cells' own right
     portion AND the x=95 wire's real on-screen position with it, which is
     why the right-column vertical line looked simply absent rather than
     misplaced. minmax(0, 1fr) forces true equal thirds and lets the text
     wrap into that width instead. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(3, 1fr);
  gap: var(--sc-3);
  aspect-ratio: 1 / 1;
}
.panel__node {
  position: relative;
  border-radius: var(--sc-r-sm);
  border: 1px solid var(--sc-hairline-strong);
  background: color-mix(in oklab, var(--sc-canvas) 55%, transparent);
  will-change: transform, opacity;
}
.panel__node--metric {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  border-color: color-mix(in oklab, var(--sc-accent) 45%, var(--sc-hairline-strong));
  padding: 0.2rem;
  overflow: clip;
}
.panel__value {
  font-family: var(--sc-font-display);
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--sc-accent);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  opacity: 0;
  will-change: opacity;
}
.panel__unit { font-style: normal; font-size: 0.55rem; color: var(--sc-ink); opacity: 0; will-change: opacity; }
/* Client screenshot: "RISPARMIATO" (the longest single word among the 3
   metric captions) was overflowing its cell -- same root cause and same
   fix as .panel__topic's "Competenze" overflow above: this was still at
   the old larger font-size (0.5rem) and the wider --sc-track-wide (0.08em)
   letter-spacing, never brought in line with .panel__topic's tuned values
   when that fix landed. Matched to the same 0.46rem/0.03em here too. */
.panel__caption { font-style: normal; font-size: 0.46rem; color: var(--sc-ink); letter-spacing: 0.03em; text-transform: uppercase; margin-top: 0.15rem; opacity: 0; will-change: opacity; }
/* The 6 short-label cells (L'Attività / Il Processo / Le Competenze / Casi
   Reali / Soluzioni Pronte / Prossimo Passo) -- one per non-Benefits
   section. Same mono/uppercase treatment as .panel__label (the panel's own
   title) rather than .panel__caption's plain sans, since these read as the
   cell's whole content rather than a caption under a number. */
.panel__node--topic {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: 0.25rem;
}
.panel__topic {
  font-family: var(--sc-font-mono);
  font-style: normal;
  /* Client round-3 screenshot: "LE COMPETENZE" alone was visibly poking past
     its cell's left/right edges at 1440px, the other 5 labels fit fine.
     Measured (Playwright, real getBoundingClientRect at 1440px, all 9 nodes
     resolved): the cell's available inner width (cell width minus its own
     0.25rem/4px padding each side) is ~52.7px. "Le Competenze" already
     wraps to 2 lines ("Le" / "Competenze") same as any other 2-word label,
     but "Competenze" alone -- one unbreakable word -- measured 64px at the
     previous 0.56rem/0.08em, ~11px wider than the cell had room for, so it
     overflowed past both edges instead of wrapping further. The other 5
     labels' longest single word (e.g. "Soluzioni", "Prossimo") was always
     short enough to fit at that size, which is why only this one showed
     the overflow.
     Tried forcing a further mid-word break instead (min-width: 0 +
     overflow-wrap/word-break on this flex item) and reverted it: browser
     testing showed word-break: break-word alone is what actually forces a
     break here, but it also changes the min-content sizing the browser
     picks for the OTHER 5 labels' box, which broke fine-fitting words like
     "Il Problema" into "IL PROBLEM"/"A" -- worse than the original bug.
     Went with a small uniform font-size/letter-spacing reduction instead,
     applied to all 6 labels (not just this one) so they stay visually
     consistent with each other: at 0.46rem/0.03em, "Competenze" alone
     measures ~48.9px, leaving a real ~1.9px margin on each side inside the
     cell (re-verified live for all 6 labels, not just this one -- see the
     round-3 report for the full measurement table). */
  font-size: 0.46rem;
  line-height: 1.2;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--sc-ink);
  opacity: 0;
  will-change: opacity;
}
/* Nested inside .panel__grid (position: relative), inset 0, so this SVG's
   0-100 coordinate space always equals the grid's own real box -- not
   .panel's box minus an independent inset, which drifted out of sync with
   the grid the moment .panel__label's height (or any other chrome sharing
   .panel) changed without the wires' own inset being retuned to match. */
.panel__wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.panel__wires .wire {
  stroke: var(--sc-accent);
  /* Client round-3: wants to try a more subtle version of the orange wires
     before deciding whether to drop them entirely -- "present but discreet",
     not gone. Thinned from 0.6 to 0.35 (viewBox units); paired with the
     lower peak opacity in script.js's wireOpacity (was 0.6, now 0.35 -- see
     the smoothstep(0, 1, resolvedFrac) comment there). */
  stroke-width: 0.35;
  opacity: 0;
  vector-effect: non-scaling-stroke;
  will-change: opacity;
}

@media (max-width: 860px) {
  /* Desktop mark doubled to 120px this round (see .site-bar__mark img
     above); a full 120px would dominate a 390px-wide phone screen (nearly a
     third of its width), so mobile gets a smaller bump instead, 60px -> 80px
     -- clearly bigger than the previous round but not overwhelming. Checked
     visually at 390 and 360px. */
  .site-bar__mark img { width: 80px; height: 80px; }

  /* Measured (Playwright, 390x844 and 360x640): a desktop-style horizontal
     shift does not fit here. The gap between the logo mark's right edge and
     .panel's left edge is only ~63px at 390px wide (mark ~95px + panel
     200px + tight edge offsets leave almost nothing in between), while the
     Contattaci button itself is ~109px wide -- it physically cannot sit
     between them with real breathing room on both sides at this width.
     Kept horizontally where flexbox puts it (bar's right padding edge) and
     rely on .panel's own top offset below for vertical clearance instead,
     same mechanism as the pre-existing mobile fix. Resets the desktop
     margin-right back to 0 so .cta--nav returns to its normal
     space-between position instead of inheriting the 17rem desktop shift,
     which at this width would ram it straight into the logo mark. */
  .cta--nav { margin-right: 0; }

  /* Client request (2026-09-19): hide the Knowledge Panel entirely on
     mobile -- "copre troppo, su mobile non ha senso". Desktop keeps it
     unchanged (the .panel rule at ~line 775 above, outside this media
     query). This supersedes a long history of mobile-specific panel
     resizing/repositioning attempts (9-cell grid, font/padding shrinks, a
     documented unresolved overlap with the hero H1 at 360x640) -- all of
     that tuning is now moot since the element doesn't render here at all.
     aria-hidden already on the panel itself, so nothing extra needed for
     accessibility. */
  .panel { display: none; }

  .benefits-split, .team-body, .services-copy, .contact-grid { grid-template-columns: 1fr; }
  .hero-stats { flex-wrap: wrap; gap: var(--sc-5); }
  .services-copy { gap: var(--sc-6); padding-block: var(--sc-7); }
  .extra-list { grid-template-columns: 1fr; }
  .foot { grid-template-columns: 1fr; }

  /* Act 6 (Contattaci) is ordinary flow now, not a pinned one-viewport
     stage, so the whole "keep it inside one viewport" budget that this
     block used to enforce is obsolete: the section grows to fit. The nav
     bar is still fixed, and .sc-section's own padding-block already
     clears it, so nothing extra is needed here beyond the tighter
     stacked-card rhythm below. */
  .close { gap: var(--sc-2); }
  .contact-grid { gap: var(--sc-3); }
  /* The padding-right reserved here for the Knowledge Panel's mobile
     top-right footprint is gone -- the panel is now display:none on mobile
     entirely (see the .panel rule above), so this workaround is moot. */
  .contact-box .sc-rule { display: none; }
  .contact-box__link { margin-top: var(--sc-2); }
  .contact-card { padding: var(--sc-3); gap: var(--sc-1); }
  .contact-box { padding: var(--sc-3); }
  .hero-copy h1 { max-width: none; }
  /* Lift the hero copy clear of the ElevenLabs chat widget. This is the real,
     reproducible half of the client's "Inizia Ora doesn't work" report, and it
     only happens on phones: measured at 390x844, the widget's expanded card
     occupies x 54-358 / y 708-812 — nearly the full width of the screen — while
     .sc-copy--lead's `bottom: clamp(3rem,12vh,9rem)` put the CTA at
     x 20-123 / y 700-743. The widget renders at z-index 1000 in its own shadow
     root, so it wins the hit test and swallows the tap: document.elementFromPoint
     at the CTA's own centre returned <elevenlabs-convai>, and four real
     Playwright click attempts on the CTA all failed with "elevenlabs-convai
     intercepts pointer events". The button was never broken as markup — it was
     simply unreachable. .sc-copy--lead is bottom-anchored, so padding-bottom
     here pushes its content up rather than growing the box: 5rem moves the CTA
     to y 620-663 (re-measured), a clear 45px above the widget, and
     elementFromPoint at its centre now returns the anchor itself. */
  .hero-copy { padding-bottom: 5rem; }
}

@media (prefers-reduced-motion: reduce) {
  .panel__node, .panel__value, .panel__unit, .panel__caption, .panel__topic, .panel__wires .wire { transition: opacity 220ms var(--sc-ease-out); }
}

/* =========================================================================
   COOKIE CONSENT BANNER — vanilla-JS port of the live site's CookieConsent,
   driven by consent.js. Reuses the .cta button styles and colour tokens
   already defined above rather than inventing a new visual language for a
   single banner.
   ========================================================================== */
.cookie-banner {
  position: fixed;
  inset: auto 0 0 0;
  /* Above the ElevenLabs widget's own z-index (1000, observed at runtime)
     as well as --sc-z-chrome: a consent prompt gating two live tracking
     tags must never lose a stacking fight with third-party chrome. */
  z-index: 1100;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sc-4);
  padding: var(--sc-4) var(--sc-gutter);
  /* Reserve clear space on the right at every width: the ElevenLabs chat
     widget's launcher lives fixed bottom-right (see README's pre-existing
     "widget overlaps content" note) and, once loaded, its invisible
     hit-testing box can extend well past its visible button. Keeping the
     banner's own content and buttons out of that corner avoids handing it
     an unclickable Accept button. */
  padding-right: calc(var(--sc-gutter) + 4.5rem);
  padding-bottom: calc(var(--sc-4) + env(safe-area-inset-bottom, 0px));
  background: color-mix(in oklab, var(--sc-surface) 92%, transparent);
  border-top: 1px solid var(--sc-hairline-strong);
  backdrop-filter: blur(16px);
  box-shadow: var(--sc-e2);
  transform: translate3d(0, 100%, 0);
  opacity: 0;
  transition: transform var(--sc-d-base) var(--sc-ease-out),
              opacity var(--sc-d-base) var(--sc-ease-out);
}
.cookie-banner--visible { transform: none; opacity: 1; }
.cookie-banner__text {
  flex: 1 1 320px;
  margin: 0;
  font-family: var(--sc-font-text);
  font-size: var(--sc-t-sm);
  line-height: var(--sc-leading-body);
  color: var(--sc-ink-soft);
}
.cookie-banner__text a { color: var(--sc-accent); text-decoration-color: var(--sc-accent); }
.cookie-banner__actions { display: flex; gap: var(--sc-2); flex-shrink: 0; }
.cookie-banner__actions .cta { padding: 0.65em 1.2em; font-size: var(--sc-t-xs); }

@media (prefers-reduced-motion: reduce) {
  .cookie-banner { transition: opacity 220ms var(--sc-ease-out); transform: none; }
}

/* =========================================================================
   IN-PAGE RAIL NAV — minimal section-dot navigation for the home page's six
   acts. Not a traditional nav bar: a small fixed vertical rail of dots,
   consistent with the restrained "architetti milanese" aesthetic. Highlights
   the current act (toggled by script.js via IntersectionObserver) and jumps
   to a section on click using ordinary in-page anchors (html already sets
   scroll-behavior: smooth in scrollcraft.css), which does not fight the
   engine's own rAF-driven reads of native scroll position.
   ========================================================================== */
.rail-nav {
  position: fixed;
  z-index: var(--sc-z-chrome);
  right: calc(var(--sc-gutter) / 2);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sc-3);
}
.rail-nav__dot {
  position: relative;
  display: block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--sc-ink-soft);
  opacity: 0.45;
  text-decoration: none;
  transition: opacity var(--sc-d-fast) var(--sc-ease-out),
              background-color var(--sc-d-fast) var(--sc-ease-out),
              transform var(--sc-d-fast) var(--sc-ease-out);
}
.rail-nav__dot:hover { opacity: 0.8; transform: scale(1.3); }
.rail-nav__dot[aria-current="true"] {
  background: var(--sc-accent);
  opacity: 1;
  transform: scale(1.4);
}
.rail-nav__dot:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }
.rail-nav__label {
  position: absolute;
  right: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-right: var(--sc-2);
  padding: 0.3em 0.7em;
  border-radius: var(--sc-r-sm);
  background: color-mix(in oklab, var(--sc-surface) 85%, transparent);
  border: 1px solid var(--sc-hairline-strong);
  color: var(--sc-ink);
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--sc-d-fast) var(--sc-ease-out);
}
.rail-nav__dot:hover .rail-nav__label,
.rail-nav__dot:focus-visible .rail-nav__label { opacity: 1; }

@media (max-width: 860px) {
  /* A vertical rail competes with the mobile metrics panel (top-right) and
     the ElevenLabs widget (bottom-right); drop it rather than crowd either. */
  .rail-nav { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .rail-nav__dot { transition: opacity 150ms var(--sc-ease-out); }
}

/* ==========================================================================
   Scroll controls — "Avanza" (next section) + "Torna su" (back to top)
   --------------------------------------------------------------------------
   Placed bottom-LEFT deliberately. Measured occupancy of the other corners at
   1440x900: .site-head spans the full top edge; .panel occupies roughly
   x 1128-1368 / y 83-370; .rail-nav sits at x ~1396-1404 / y ~380-520; the
   ElevenLabs launcher and its speech bubble live bottom-right from about
   x 1100 / y 760 down. Bottom-left is the only region free at every scroll
   depth, and it stays free at 390x844 too (where .rail-nav is hidden and the
   panel shrinks to an 8rem top-right chip).
   Restraint, not decoration: 2.5rem squares, hairline border, the same
   translucent surface + blur as .panel, no bounce, no pulse. The only motion
   is a 1px nudge and a border-colour change on hover.
   ========================================================================== */
.scroll-ctl {
  position: fixed;
  /* Above --sc-z-chrome (60) and above the ElevenLabs widget's own z-index
     (1000, observed at runtime), below the cookie banner (1100, which must
     keep winning). The widget's card is 304px wide and reaches x 54 at
     390px viewport width, only 6px clear of these buttons; if its copy ever
     grows, the buttons must still take the tap rather than silently becoming
     a second dead control — which is exactly the failure mode this round was
     asked to fix on the hero CTA. */
  z-index: 1050;
  /* 1.25rem, not --sc-gutter. The page's content column is capped at
     --sc-maxw (1312px), so at 1440 the left margin is only ~64px while the
     "Avanza" pill is ~101px wide: sitting it on the gutter pushed it 37px
     over the left edge of the Contact card (seen in a scroll-position
     screenshot, partially covering a line of body copy). Tucking it to 20px
     keeps the whole pair inside the margin at that width. */
  left: 1.25rem;
  /* --sc-ctl-lift is written by script.js from the cookie banner's MEASURED
     height (0 when no banner). The banner is a full-width fixed bar on the
     bottom edge at a higher z-index, so on a first visit it would sit straight
     on top of these buttons — the one moment a new visitor most needs an
     obvious way to move down the page. A hardcoded lift was tried first and
     was wrong: the banner is 82px tall at 1440x900 but 201px at 390x844
     (it wraps to three lines), so the buttons were still buried on phones.
     Measuring is the only thing that holds at every width and text length. */
  bottom: calc(var(--sc-4) + env(safe-area-inset-bottom, 0px) + var(--sc-ctl-lift, 0px));
  display: flex;
  flex-direction: column;
  gap: var(--sc-1);
  transition: bottom var(--sc-d-base) var(--sc-ease-out),
              opacity var(--sc-d-base) var(--sc-ease-out);
}

.scroll-ctl__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border-radius: var(--sc-r-sm);
  border: 1px solid var(--sc-hairline-strong);
  background: color-mix(in oklab, var(--sc-surface) 72%, transparent);
  backdrop-filter: blur(16px);
  color: var(--sc-ink-soft);
  cursor: pointer;
  transition: color var(--sc-d-fast) var(--sc-ease-out),
              border-color var(--sc-d-fast) var(--sc-ease-out),
              transform var(--sc-d-fast) var(--sc-ease-out),
              opacity var(--sc-d-fast) var(--sc-ease-out);
}
.scroll-ctl__btn svg { width: 1.05rem; height: 1.05rem; flex: none; }
.scroll-ctl__btn[hidden] { display: none; }
.scroll-ctl__btn:hover { color: var(--sc-ink); border-color: var(--sc-accent); }
.scroll-ctl__btn:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
.scroll-ctl__btn:active { transform: scale(0.94); }
#scroll-next:hover { transform: translateY(1px); }
#scroll-top:hover { transform: translateY(-1px); }

/* The "advance" button carries a word as well as a glyph: a bare chevron is
   exactly the ambiguous decoration the style guide objects to, whereas a
   labelled control reads unmistakably as something to press — which is the
   entire point of the client's request. The back-to-top button stays icon-only
   (with an aria-label) because an upward arrow in a page corner is already an
   unambiguous, conventional affordance. */
.scroll-ctl__btn--next {
  width: auto;
  padding-inline: 0.75rem;
  font-family: var(--sc-font-mono);
  font-size: var(--sc-t-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

@media (max-width: 860px) {
  .scroll-ctl { left: 0.75rem; bottom: calc(var(--sc-3) + env(safe-area-inset-bottom, 0px) + var(--sc-ctl-lift, 0px)); }
  /* Stand the controls down while the consent banner is open — phones only.
     The banner is 201px tall at 390x844 (three wrapped lines), so the lift
     that keeps the buttons reachable pushes them a quarter of the way up the
     screen, where they land squarely on the hero's body copy and cover the
     first few characters of two lines (seen in a 390x844 screenshot). There
     is no free corner at that width: the ElevenLabs card takes x 54-358 along
     the bottom and the metrics panel takes the top right. Deferring for the
     one tap it takes to answer the banner is better than either obscuring the
     copy or burying the buttons. Desktop keeps them: the banner is only 82px
     there and the lifted pair still sits in the empty left margin. */
  :root[data-consent-open] .scroll-ctl { opacity: 0; pointer-events: none; }
  .scroll-ctl__btn { width: 2.25rem; height: 2.25rem; }
  /* Drop the word on phones: the buttons sit near the thumb rest and the
     narrower pair keeps well clear of the ElevenLabs launcher opposite. */
  .scroll-ctl__btn--next { width: 2.25rem; padding-inline: 0; }
  .scroll-ctl__label { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the controls fully functional, just stop them moving. script.js
     already swaps scroll behaviour to 'auto' under the same query, so the
     jump is instant rather than animated — the buttons still work. */
  .scroll-ctl, .scroll-ctl__btn { transition: color 150ms linear, border-color 150ms linear; }
  .scroll-ctl__btn:active, #scroll-next:hover, #scroll-top:hover { transform: none; }
}

/* ElevenLabs widget: mobile scroll jitter (client report, 2026-09-19) — the
   launcher visibly lags/"follows" for a frame or two during a scroll
   gesture on phones, then snaps back to its fixed position, instead of
   staying perfectly anchored. This is a well-known mobile-browser quirk
   (mainly iOS Safari) where a `position:fixed` element the browser hasn't
   promoted to its own compositor layer can be repainted a frame behind a
   fast scroll instead of moving in lockstep with it. The widget is a
   third-party shadow-DOM custom element (`@elevenlabs/convai-widget-embed`)
   -- its own internal fixed-positioned launcher lives INSIDE that shadow
   root, out of reach for our CSS, but the custom element's own HOST box
   (`<elevenlabs-convai>` itself, in the light DOM) is stylable from here,
   and forcing ITS compositor layer is enough in practice to carry the
   shadow content along smoothly, since the browser composites the whole
   custom element as one paint unit. Standard mitigation: promote it to its
   own GPU layer so the browser tracks it via composited scrolling instead
   of repainting on the main thread. Mobile-only, since this is where the
   report came from and where this class of jank is common; not applied on
   desktop to avoid promoting a layer with no reported problem there.
   NOTE: mobile-browser fixed-position jank is a device/OS rendering
   behaviour that Playwright's desktop-Chromium emulation cannot fully
   reproduce -- this is the standard, documented fix for the exact symptom
   described, but real confirmation needs a real phone. */
@media (max-width: 860px) {
  elevenlabs-convai {
    transform: translateZ(0);
    will-change: transform;
  }
}

/* Hero video autoplay (client request, 2026-09-20) — everything scrollcraft.css
   does for a scrub video (fill the stage, crop via object-fit:cover, crossfade
   over the poster via .sc-has-clip) is scoped to video[data-sc-scrub], since
   that's the only kind of video the engine itself ever reveals or lays out.
   The hero clip no longer goes through that pipeline (see index.html ACT 1
   comment) but script.js still applies the same .sc-has-clip class by hand
   once it's ready to play, so both the layout rule and the opacity rule need
   a matching copy here rather than in the engine file.
   Bug fixed 2026-09-23: only the opacity half was added at first — without
   position/size/object-fit the video rendered at its own intrinsic (portrait)
   aspect ratio instead of filling the stage, so on wide/landscape screens it
   only covered a narrow strip on the left with empty space to the right
   (client screenshot, desktop browser). */
.sc-stage video[data-sc-autoplay] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 380ms var(--sc-ease-out);
}
.sc-has-clip video[data-sc-autoplay] { opacity: 1; }
