/* =============================================================
   HOME-B — Sticky projects section (A/B variant)
   Scrollytelling pattern: numbered text blocks on the left activate
   as they cross the viewport center; a single sticky card on the
   right crossfades between each project's mockup. Reference: eve.dev
   -style scroll section (screenshot approved by Vicente, 2026-07-27).
   Reuses design tokens from styles.css (--space-*, --fs-*, --card-radius,
   --elev-1, --gray-900, --purple-500/900) — no hardcoded spacing/type.
   ============================================================= */

.sticky-projects {
  position:   relative; /* anchor for the is-ended absolute CTA row */
  width:      100%;
  background: var(--bg-hero);
  padding:    120px 96px 140px; /* matches .vision-scroll rhythm */
  overflow:   visible;
}

.sticky-projects__inner {
  max-width: none;
  margin:    0 auto;
}

/* ── Header: title left / intro + CTA right ─────────────────── */
/* Single sticky unit: header + card pin together and — crucially —
   RELEASE together at the section end, so the card never slides up
   under the header. Left blocks scroll beside it (pulled up with a
   negative margin on __list below). */
.sticky-projects__pin {
  position:              sticky;
  top:                   var(--topbar-h, 0px);
  z-index:               5;
  display:               grid;
  grid-template-columns: minmax(440px, 1fr) minmax(0, 1.5fr);
  grid-template-rows:    auto auto;
  column-gap:            64px;
  align-items:           start;
  /* Transparent left area must not block clicks on scrolling blocks. */
  pointer-events:        none;
}
.sticky-projects__pin > * { pointer-events: auto; }

/* Opaque strip ABOVE the pinned header (viewport top → header top,
   i.e. the topbar zone): already-read blocks scrolling out must not
   reappear there. When the pin is not stuck yet, the strip only covers
   part of the section's own top padding — invisible. */
.sticky-projects__pin::before {
  content:        "";
  position:       absolute;
  bottom:         100%;
  left:           0;
  right:          0;
  height:         calc(var(--topbar-h, 52px) + 1px);
  background:     var(--bg-hero);
  pointer-events: none;
}

.sticky-projects__header {
  grid-column:           1 / -1;
  background:            var(--bg-hero);
  display:               grid;
  /* Same template as the pin/body so the intro column aligns with the card */
  grid-template-columns: minmax(440px, 1fr) minmax(0, 1.5fr);
  gap:                   64px;
  align-items:           start;
  padding:               var(--space-24) 0 var(--space-32);
}
/* Soft fade below the pinned header, LEFT column only (a grid item —
   it must never tint the card, which sits right below the header). */
.sticky-projects__pin::after {
  content:        "";
  grid-column:    1;
  grid-row:       2;
  align-self:     start;
  height:         48px;
  width:          100%;
  background:     linear-gradient(to bottom, var(--bg-hero), transparent);
  pointer-events: none;
}

.sticky-projects__title {
  font-family:    var(--font-headings);
  font-size:      var(--fs-48);
  font-weight:    var(--fw-display);
  letter-spacing: var(--ls-display);
  line-height:    56px;
  color:          var(--white);
  margin:         0;
}

/* 1px in-flow sentinel for the pin-state observer (sticky-projects.js) —
   pulled up by its own height so it adds no visual gap. */
.sticky-projects__pin-sentinel {
  height:     1px;
  margin-top: -1px;
}

/* While the header is pinned (.is-pinned set by sticky-projects.js),
   title + lead recede to 10% so the scrolling blocks take the focus —
   hovering either one restores it to 100% (Vicente 2026-07-28).
   .is-ended keeps the SAME dimmed state after the section's scroll
   finishes — no flash back to 100% while it scrolls away. */
.sticky-projects__title,
.sticky-projects__lead { transition: opacity 0.35s ease; }
#stickyProjects.is-pinned .sticky-projects__title,
#stickyProjects.is-pinned .sticky-projects__lead,
#stickyProjects.is-ended  .sticky-projects__title,
#stickyProjects.is-ended  .sticky-projects__lead { opacity: 0.1; }
#stickyProjects.is-pinned .sticky-projects__title:hover,
#stickyProjects.is-pinned .sticky-projects__lead:hover,
#stickyProjects.is-ended  .sticky-projects__title:hover,
#stickyProjects.is-ended  .sticky-projects__lead:hover { opacity: 1; }

.sticky-projects__intro {
  /* Row layout (Vicente 2026-07-28): justified lead on the left,
     CTA buttons to its RIGHT (was stacked in a column). */
  display:         flex;
  flex-direction:  row;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--space-32);
  /* Baseline-align the lead paragraph with the title's cap height rather
     than the grid row top. */
  padding-top:    var(--space-8);
}

.sticky-projects__lead {
  /* 2-line clamp retired 2026-07-28: with the CTAs sitting to the
     right, the narrower justified column legitimately needs 3-4
     lines — truncating it cut real content. */
  font-family: var(--font-text);
  font-size:   var(--fs-15);
  font-weight: var(--fw-body);
  line-height: 25px; /* 4 × 25 = 100px = CTA stack height */
  color:       var(--white);
  /* Width + leading tuned so the paragraph wraps to exactly 4 lines
     of 25px = 100px — the exact height of the two stacked CTAs
     (44 + 12 + 44). At 60ch it still rendered 3 lines (Geist's ch is
     narrow → ~78 chars/line); 47ch ≈ 59 chars/line → 235 chars ≈ 4
     lines. Screenshot-verified 2026-07-28. */
  flex:        0 1 47ch;
  min-width:   0;
  text-align:  justify; /* justified block, not ragged (Vicente 2026-07-28) */
  margin:      0;
}

.sticky-projects__cta {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  gap:             var(--space-8);
  height:          44px;
  padding:         0 24px;
  /* width: fit-content retired (Vicente 2026-07-28) — it blocked the
     cta-row's align-items: stretch, so this button couldn't match
     "Ask me about this project"'s width. */
  background:      #878DEA;
  border:          1px solid #878DEA;
  color:           var(--bg-main);
  font-family:     var(--font-text);
  font-size:       16px;
  font-weight:     500;
  line-height:     24px;
  text-decoration: none;
  cursor:          pointer;
  transition:      opacity 0.2s ease, background-color 0.5s ease,
                   border-color 0.5s ease, color 0.5s ease; /* matches card crossfade */
}
.sticky-projects__cta:hover { opacity: 0.85; }

.sticky-projects__cta-row {
  /* ── SAVED CONFIG (Vicente 2026-07-28, "por si acaso") ──
     Stacked column hugging the intro's right: "All projects" ABOVE
     "Ask me…", equal widths via align-items: stretch. This base rule
     IS that config — to revert the experiment below, just delete the
     #stickyProjects.is-pinned .sticky-projects__cta-row block. */
  display:        flex;
  flex-direction: column;
  align-items:    stretch;
  gap:            var(--space-12);
  flex:           0 0 auto;
}

/* ── Header CTAs hide during the scroll; an end-of-flow copy takes
   over (Vicente 2026-07-28). While the section is pinned (and after
   it ends) the top-right stacked buttons fade out; a second CTA row
   sits in NORMAL FLOW at the section's tail, so it shows up exactly
   when the scroll finishes — just before the next section. ── */
.sticky-projects__intro .sticky-projects__cta-row {
  transition: opacity 0.35s ease;
}
#stickyProjects.is-pinned .sticky-projects__intro .sticky-projects__cta-row,
#stickyProjects.is-ended  .sticky-projects__intro .sticky-projects__cta-row {
  opacity:        0;
  pointer-events: none;
}

/* End row: side-by-side, aligned with the project texts' left edge. */
.sticky-projects__cta-row--end {
  flex-direction: row;
  align-items:    center;
  justify-content: flex-start;
}
.sticky-projects__cta-row--end .sticky-projects__cta,
.sticky-projects__cta-row--end .sticky-projects__cta-secondary {
  width: auto; /* natural widths in the row (stretch is for the stack) */
}

/* Secondary: quiet white-10% chip-style button → /contact */
.sticky-projects__cta-secondary {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  height:          44px;
  padding:         0 24px;
  /* SOLID gray derived from the main background (was translucent
     white-10% — see-through over the fixed-bottom position). Same
     tone-on-tone recipe as the vision pills. */
  background:      color-mix(in srgb, var(--white) 10%, var(--bg-main));
  color:           var(--white);
  font-family:     var(--font-text);
  font-size:       16px;
  font-weight:     500;
  line-height:     24px;
  text-decoration: none;
  overflow:        hidden;
  transition:      background-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
                   color 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.sticky-projects__cta-secondary:hover {
  background: var(--white);
  color:      var(--bg-main);
}

/* Text swap: "Ask me about this project" slides up and out, "Reach out"
   slides in from below — one fluid motion, synced with the colour shift. */
.sticky-projects__cta-swap {
  position: relative;
  display:  inline-block;
  overflow: hidden;
}
.sticky-projects__cta-swap-default {
  display:    block;
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              opacity   0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.sticky-projects__cta-swap-hover {
  position:        absolute;
  inset:           0;
  display:         flex;
  align-items:     center;
  justify-content: center;
  transform:       translateY(100%);
  opacity:         0;
  transition:      transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
                   opacity   0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.sticky-projects__cta-secondary:hover .sticky-projects__cta-swap-default,
.sticky-projects__cta-secondary:focus-visible .sticky-projects__cta-swap-default {
  transform: translateY(-100%);
  opacity:   0;
}
.sticky-projects__cta-secondary:hover .sticky-projects__cta-swap-hover,
.sticky-projects__cta-secondary:focus-visible .sticky-projects__cta-swap-hover {
  transform: translateY(0);
  opacity:   1;
}
@media (prefers-reduced-motion: reduce) {
  .sticky-projects__cta-secondary,
  .sticky-projects__cta-swap-default,
  .sticky-projects__cta-swap-hover { transition: none; }
}
.sticky-projects__cta-secondary:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 4px;
}

/* CTA tint follows the active card background (set by sticky-projects.js).
   Text colours picked per background for WCAG AA at 16px/500. */
[data-active-face="0"] .sticky-projects__cta {           /* Housing Help teal (Vicente 2026-07-28) */
  background: #0E4F43; border-color: #0E4F43;
  color: #fff; /* the HH app's blue-green as bg now — ~9:1 with white, AA ✓ */
}
[data-active-face="1"] .sticky-projects__cta {           /* VELOQUE maroon (Vicente 2026-07-28) */
  background: #6E1423; border-color: #6E1423; color: #fff; /* ~8.5:1 on white text — AA ✓ */
}
[data-active-face="2"] .sticky-projects__cta {           /* Beloo orange-red */
  background: #ff4630; border-color: #ff4630; color: var(--bg-main);
}
[data-active-face="3"] .sticky-projects__cta {           /* Delta dark red */
  background: #C01933; border-color: #C01933; color: #fff;
}
.sticky-projects__cta i     { font-size: 18px; transition: transform 0.2s ease; }
.sticky-projects__cta:hover i { transform: translateX(4px); }
.sticky-projects__cta:focus-visible {
  outline:        2px solid var(--white);
  outline-offset: 4px;
}

/* ── Body: left list / right sticky card ─────────────────────── */
.sticky-projects__body {
  display:               grid;
  grid-template-columns: minmax(440px, 1fr) minmax(0, 1.5fr);
  gap:                   64px; /* matches header column gap */
  align-items:           start;
}

/* ── Left: numbered blocks ────────────────────────────────────── */
.sticky-projects__list {
  grid-column:    1;
  display:        flex;
  flex-direction: column;
  /* Pull the list up so the first block starts level with the card
     (the pin occupies header + 48px + card height in normal flow).
     --spb-card-h is measured by sticky-projects.js. */
  margin-top:     calc(-1 * var(--spb-card-h, 500px));
}

/* Each step gives its block ~a viewport of scroll travel; the block
   itself is sticky inside it, so it PINS below the header for a moment
   until its step runs out and the next block pushes in (eve.dev
   sticky-stack pattern). The LAST step has the same height, so the
   final block pins and dwells for the same scroll amount before the
   whole unit (header + card + block) rides out of the section. */
.sticky-projects__step {
  /* Step height = distance from the pin position to the bottom of the
     viewport (minus an 80px peek). Result: the moment a block takes the
     sticky spot, the NEXT one is already peeking in from below — a
     continuous conveyor, no dead scroll between projects. */
  min-height: calc(100vh - var(--topbar-h, 0px) - var(--spb-header-h, 210px) - 48px - 80px);
}

.sticky-projects__block {
  position:       sticky;
  top:            calc(var(--topbar-h, 0px) + var(--spb-header-h, 210px) + 48px);
  /* eve-style push gap: sticky confinement counts the margin box, so
     the pinned block is pushed out while the incoming block is still
     96px below it — they never visually collide. */
  margin-bottom:  96px;
  display:        flex;
  flex-direction: column;
  gap:            var(--space-12); /* intra-block rhythm */
  /* Reserve scroll room above/below the first/last block so each one can
     reach viewport-center and trigger its own IntersectionObserver entry. */
  scroll-margin-top: 40vh;
  opacity:        0.22; /* dimmed inactive state — see contrast note below */
  transition:     opacity 0.5s ease;
}

/* Dimmed inactive text stays >=3:1 against --bg-hero (#080C1D): the
   0.45 opacity white measured ~4.3:1 for body copy and ~6:1 for the
   larger title — both clear the relaxed 3:1 floor the brief allows for
   a decorative-dim state (full content is available once active). */
.sticky-projects__block.is-active { opacity: 1; }

.sticky-projects__block-heading {
  display:        flex;
  flex-direction: column;   /* logo ABOVE the project name */
  align-items:    flex-start;
  gap:            var(--space-12);
}

.sticky-projects__block-icon {
  flex-shrink: 0;
  width:       40px;
  height:      48px;
  display:     flex;
  align-items: flex-start;
  justify-content: center;
}
.sticky-projects__block-icon img {
  width:      100%;
  height:     100%;
  object-fit: contain;
  display:    block;
  /* Icons ship as dark marks (built for light project-card backgrounds);
     invert so they read on the section's dark background. */
  filter:     invert(1) brightness(1.6);
}
/* Delta's mark is red — the generic invert turns it cyan. Flatten it
   to pure white instead. */
.sticky-projects__block[data-index="3"] .sticky-projects__block-icon img {
  filter: brightness(0) invert(1);
}

.sticky-projects__block-title-row {
  display:     flex;
  align-items: center;
  flex-wrap:   wrap;
  gap:         var(--space-12);
}

.sticky-projects__block-role-chip {
  display:     inline-flex;
  align-items: center;
  padding:     4px 12px;
  background:  rgba(255, 255, 255, 0.10);
  font-family: var(--font-text);
  font-size:   var(--fs-13);
  font-weight: 500;
  line-height: 20px;
  color:       var(--white);
  white-space: nowrap;
}

.sticky-projects__block-title {
  text-transform: uppercase;
  font-family:    var(--font-headings);
  font-size:      var(--fs-32);
  font-weight:    var(--fw-heading);
  letter-spacing: var(--ls-heading);
  line-height:    40px;
  color:          var(--white);
  margin:         0;
}

.sticky-projects__block-categories {
  display:     flex;
  flex-wrap:   wrap;
  gap:         var(--space-8);
  align-items: center;
  margin-top:  20px;  /* + the block's 12px flex gap = 32px below the link */
  max-width:   440px; /* caps rows at ~3 chips */
}
.sticky-projects__block-categories span {
  display:     inline-flex;
  align-items: center;
  padding:     4px 12px;
  /* Tone-on-tone with the section background: chip = slightly lighter
     navy, type = an even lighter step of the same hue. */
  background:  color-mix(in srgb, var(--white) 9%, var(--bg-hero));
  font-family: var(--font-text);
  font-size:   var(--fs-13);
  font-weight: 500;
  line-height: 20px;
  color:       color-mix(in srgb, var(--white) 62%, var(--bg-hero));
  white-space: nowrap;
}

.sticky-projects__block-desc {
  font-family: var(--font-text);
  font-size:   var(--fs-15);
  line-height: 24px;
  color:       var(--gray-900);
  max-width:   46ch;
  margin:      0;
}

.sticky-projects__block-role {
  font-family: var(--font-text);
  font-size:   var(--fs-13);
  font-weight: 700;
  color:       var(--gray-900);
  margin:      0;
}

.sticky-projects__block-link {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-8);
  width:           fit-content;
  color:           var(--purple-700); /* same accent purple as the vision labels ("01 -My Values") */
  font-family:     var(--font-text);
  font-size:       var(--fs-15);
  font-weight:     500;
  text-decoration: none;
  border-bottom:   1px solid transparent;
  transition:      opacity 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
/* Hover (Vicente 2026-07-28): text + underline shift TOGETHER to the
   lighter purple token — the line always matches the text color. */
.sticky-projects__block-link:hover {
  color:        var(--purple-900);
  border-color: currentColor;
}
.sticky-projects__block-link i     { font-size: 16px; transition: transform 0.2s ease; }
.sticky-projects__block-link:hover i { transform: translateX(4px); }
.sticky-projects__block-link:focus-visible {
  outline:        2px solid var(--white);
  outline-offset: 4px;
}

/* Mobile-only inline image (see breakpoint below); hidden on desktop
   where the sticky card on the right already shows it. */
.sticky-projects__block-mobile-image { display: none; }

/* ── Right: sticky crossfading card ───────────────────────────── */
.sticky-projects__sticky-col {
  grid-column: 2;
  grid-row:    2;
  margin-top:  48px; /* fixed CTA→card gap, travels with the pin */
  height:      fit-content;
}

.sticky-projects__card {
  position:     relative;
  width:        100%;
  /* Full-bleed to the BOTTOM of the viewport (Vicente 2026-07-28):
     card top sits at topbar + pinned header + 48px gap, so exactly
     the remaining viewport height reaches the bottom edge — the old
     640px cap and 48px bottom breathing are gone. */
  height:       calc(100dvh - var(--topbar-h, 0px) - var(--spb-header-h, 210px) - 48px);
  overflow:     hidden;
  box-shadow:   var(--elev-1);
  background:   var(--bg-chip); /* fallback before a face paints in */
}

.sticky-projects__face {
  position:        absolute;
  inset:           0;
  display:         flex;
  align-items:     center;
  justify-content: center;
  padding:         var(--space-40);
  opacity:         0;
  pointer-events:  none;
  transition:      opacity 0.5s ease;
}
.sticky-projects__face.is-active {
  opacity:        1;
  pointer-events:  auto;
  z-index:         2;
}

.sticky-projects__face-img {
  max-width:  100%;
  max-height: 100%;
  object-fit: contain;
  display:    block;
  pointer-events: none;
}

/* Per-project card backgrounds, lifted from each .project-section--N
   treatment on the real home (public/assets/css/pages/home.css) but
   scoped to the sticky card face only — never the page background. */
.sticky-projects__face--1 {
  background: #f3f3f3;
}
/* Housing Help shot scaled up + pushed down (Vicente 2026-07-28) so
   the screenshot's bottom crop of the hand slips BELOW the card edge
   (card has overflow: hidden) instead of showing mid-frame. */
.sticky-projects__face--1 .sticky-projects__face-img {
  transform: scale(1.18) translateY(7%);
}
.sticky-projects__face--veloque {
  /* Blend experiment (Vicente 2026-07-28, option 2): the face's
     backdrop is the PAGE background color (was #000) so the image's
     `lighten` blend swallows its near-black pixels into the page
     navy — the photo's dark field literally becomes the site bg and
     only the lighter subject (rider/bike) survives. If the mid-grays
     pick up an ugly blue cast, fall back to a painted overlay
     gradient (option 1) instead. */
  background: var(--bg-main);
  /* This shot reads better filling the whole card than boxed with the
     shared 40px padding — drop the padding and let the image cover. */
  padding: 0;
}
.sticky-projects__face--veloque .sticky-projects__face-img {
  width:      100%;
  height:     100%;
  max-width:  none;
  max-height: none;
  object-fit: cover;
  mix-blend-mode: lighten;
}
.sticky-projects__face--beloo {
  background: radial-gradient(ellipse at 65% 55%,
                #fe913c 0%, #fe6b36 35%, #ff4630 65%, #ff282b 90%);
}
/* Shot nudged left of the flex-centered spot (Vicente 2026-07-28). */
.sticky-projects__face--beloo .sticky-projects__face-img {
  transform: translateX(-24px);
}
.sticky-projects__face--delta {
  background:
    radial-gradient(ellipse at 72% 50%, rgba(143, 0, 216, 0.20), transparent 60%),
    linear-gradient(135deg, #0B1020 0%, #0E1430 55%, #080C1D 100%);
}

@media (prefers-reduced-motion: reduce) {
  .sticky-projects__block,
  .sticky-projects__face {
    transition: none;
  }
}

/* ── Tablet / phone (<=900px): sticky column collapses, each block
   shows its own image inline instead ─────────────────────────── */
@media (max-width: 900px) {
  .sticky-projects        { padding: 80px 40px; }
  .sticky-projects__pin   { position: static; display: block; }
  .sticky-projects__pin::after { display: none; }
  .sticky-projects__list  { margin-top: 0; }
  .sticky-projects__step  { min-height: 0; }
  .sticky-projects__step + .sticky-projects__step { margin-top: 56px; }
  .sticky-projects__block { position: static; margin-bottom: 0; }
  .sticky-projects__header {
    grid-template-columns: 1fr;
    gap:                   var(--space-24);
    margin-bottom:         56px;
  }
  /* var(--fs-32) resolves 30px at this tier (review fix, Vicente
     2026-07-30 — was a hardcoded 32px). Phone ≤640 rule below still
     overrides to 28px, untouched. */
  .sticky-projects__title { font-size: var(--fs-32); line-height: 40px; }
  .sticky-projects__intro { padding-top: 0; }

  .sticky-projects__body {
    grid-template-columns: 1fr;
    gap:                   0;
  }


  /* No scroll-driven state on mobile — every block reads at full
     opacity; the crossfade behaviour is a desktop-only enhancement. */
  .sticky-projects__block { opacity: 1; }

  .sticky-projects__sticky-col { display: none; }

  .sticky-projects__block-mobile-image {
    display:       block;
    order:         2; /* after heading/categories, before description */
    margin:        var(--space-8) 0;
    overflow:      hidden;
    padding:       var(--space-24);
    display:       flex;
    align-items:   center;
    justify-content: center;
    min-height:    220px;
  }
  .sticky-projects__block-mobile-image img {
    width:      100%;
    max-height: 320px;
    object-fit: contain;
    display:    block;
  }
  .sticky-projects__block-mobile-image--1       { background: #f3f3f3; }
  /* Task 3 (responsive pass): match the desktop face--veloque fusion —
     page-bg backdrop + mix-blend-mode:lighten swallows the shot's dark
     field into the page navy (was #000, a flat box behind the image). */
  .sticky-projects__block-mobile-image--veloque { background: var(--bg-main); }
  .sticky-projects__block-mobile-image--veloque img { mix-blend-mode: lighten; }
  .sticky-projects__block-mobile-image--beloo {
    background: radial-gradient(ellipse at 65% 55%,
                  #fe913c 0%, #fe6b36 35%, #ff4630 65%, #ff282b 90%);
  }
  .sticky-projects__block-mobile-image--delta {
    background:
      radial-gradient(ellipse at 72% 50%, rgba(143, 0, 216, 0.20), transparent 60%),
      linear-gradient(135deg, #0B1020 0%, #0E1430 55%, #080C1D 100%);
  }
}

@media (max-width: 640px) {
  .sticky-projects { padding: 56px 20px; }
  .sticky-projects__title { font-size: 28px; line-height: 34px; }
  .sticky-projects__list  { gap: 48px; }

}

/* ═══ Featured-projects intro, phone AND tablet (Vicente 2026-07-29):
   the desktop intro is a ROW (lead | CTAs, space-between) and the
   lead carries `flex: 0 1 47ch`. Stacked in a column that basis
   becomes HEIGHT (no fit-content → giant gap to the buttons).
   Column: lead fit-content first, CTA stack 32px below, full-width
   buttons. Applies ≤900 so tablet matches phone. ═══ */
@media (max-width: 900px) {
  .sticky-projects__intro {
    flex-direction:  column;
    align-items:     stretch;
    justify-content: flex-start;
    gap:             var(--space-32); /* lead → CTAs */
  }
  .sticky-projects__intro .sticky-projects__lead {
    flex:       0 0 auto;   /* fit-content height, kills the 47ch basis */
    text-align: left;
  }
  .sticky-projects__intro .sticky-projects__cta-row { width: 100%; }
  /* Vicente 2026-07-29: 44px (not 48 — too chunky), 14px/400 text. */
  .sticky-projects__intro .sticky-projects__cta,
  .sticky-projects__intro .sticky-projects__cta-secondary {
    width:       100%;
    height:      44px;
    min-height:  44px;
    font-size:   14px;
    font-weight: 400;
  }
}

/* Task 1 (tablet pass, Vicente 2026-07-30): lead paragraph width 60%,
   TABLET ONLY (641-900) — phone (<=640) keeps the fit-content-height,
   full-width behaviour set above untouched. `.sticky-projects__intro`
   is a stretched column (align-items:stretch, ~line 670), so width is
   a pure cross-axis property here and doesn't fight the lead's
   `flex: 0 0 auto` (that only governs the column's main/vertical axis). */
@media (min-width: 641px) and (max-width: 900px) {
  .sticky-projects__intro .sticky-projects__lead {
    width: 60%;
  }
}

/* Task 2 (responsive pass, hard rule): any side-by-side CTA pair
   stacks vertically ≤640px, full-width, ≥44px tap target. Ungated by
   motion preference — applies to both the reduced-motion static stack
   and the animated pinned frame below. */
@media (max-width: 640px) {
  .sticky-projects__cta-row--end {
    flex-direction: column;
    align-items:    stretch;
    gap:            var(--space-12);
  }
  .sticky-projects__cta-row--end .sticky-projects__cta,
  .sticky-projects__cta-row--end .sticky-projects__cta-secondary {
    width:      100%;
    height:     auto;
    min-height: 48px;
  }
}

/* =============================================================
   Task 1 (responsive pass) — PINNED SWAP, phone + tablet (≤900px)
   Mechanism: pure position:sticky + the existing IntersectionObserver
   (sticky-projects.js) — ZERO JS changes. Everything below is gated
   to `prefers-reduced-motion: no-preference` and placed AFTER the
   ≤900px block above, so that block's static stacked layout becomes
   the reduced-motion base (untouched) while these rules turn it into
   a mini version of the desktop pinned/crossfade frame for everyone
   else. Equal-specificity selectors win by source order.
   ============================================================= */
/* (block retired 2026-07-29 — replaced by whole-unit swap ≤900; see git) */

/* (phone swap-tweaks block removed 2026-07-29 — phone is static now; see git) */

/* TABLET deltas (641–900) — same mechanism, roomier numbers. */
/* (block retired 2026-07-29 — replaced by whole-unit swap ≤900; see git) */

/* ── Topbar right group hide/show (driven by sticky-projects.js):
   language menu + Ask AI + Reach out — hidden over Who I am +
   projects, visible in hero & the rest. ── */
.topbar__right {
  transition: opacity 0.3s ease, visibility 0.3s ease;
}
.topbar__right.is-hidden {
  opacity:        0;
  visibility:     hidden;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .topbar__right { transition: none; }
}

/* ── My Vision (home-b only): highlighted phrases in white + heavier
   weight instead of the purple #c97fff (overrides vision.css). Body-copy
   highlights get +weight for emphasis, but INSIDE titles every word must
   share the exact same weight as the rest of the heading (400) — no
   mixed weights within a single title. */
.vision-purple {
  color:       var(--white);
  font-weight: 600;
}
.vision-slide__heading .vision-purple {
  font-weight: 400;
}

/* ── Home-B only: ALL heading weights down to 400 (every size).
   Tokens cover most headings (hero h1, section h2s, block h3s…);
   the explicit selectors catch headings with hardcoded weights. ── */
:root {
  --fw-display: 400;
  --fw-heading: 400;
}
.vision-slide__heading,
.discover__title,
.about__title,
h1, h2, h3, h4, h5, h6 {
  font-weight: 400;
}

/* ── My Vision section title (home-b only): "01 -My vision" tile
   content extracted from the bento and rewritten as the section
   header — same 2-line clamp discipline and weight rules as the
   rest of the page. ── */
/* 2-column header: big title left, lead paragraph top-aligned right —
   same pattern as the "Featured projects" header above. */
.vision-scroll__header {
  display:               grid;
  grid-template-columns: minmax(440px, 1fr) minmax(0, 1.5fr);
  gap:                   64px;
  align-items:           start;
  margin-bottom:         var(--space-56);
}

.vision-scroll__title {
  font-family:    var(--font-headings);
  font-size:      var(--fs-48);
  font-weight:    400; /* matches the page-wide heading-weight rule below */
  letter-spacing: var(--ls-display);
  line-height:    56px;
  color:          var(--white);
  margin:         0;
}

.vision-scroll__lead {
  font-family:  var(--font-text);
  font-size:    16px;
  font-weight:  var(--fw-body);
  line-height:  26px;
  color:        var(--white); /* uniform white — no highlighted words */
  margin:       0;
  margin-left:  auto; /* block hugs the RIGHT edge of its column; the text inside stays justified */
  padding-top:  var(--space-8); /* baseline-align with the title's cap height */
  /* Width capped so the justified block wraps in ~2 even lines
     (2 × 26px = 52px ≈ the title's 56px). ~160 chars / 2 lines ≈ 80
     chars/line → ≈64ch at 16px (calibrated against Geist's narrow ch,
     same method as the projects lead). */
  max-width:    64ch;
  text-align:      justify; /* justified block (Vicente 2026-07-28) */
  text-align-last: right;   /* the last (shorter) line closes to the right edge too */
}

/* Closing "}" under the bento — mirrors .vision-scroll__title so the
   whole section reads as a ": my root { … }" block. */
.vision-scroll__close {
  font-family:    var(--font-headings);
  font-size:      var(--fs-48);
  font-weight:    400;
  /* "double space" between the ...} characters (Vicente 2026-07-28) —
     done with tracking, not literal spaces: ~2 spaces ≈ 0.55em. */
  letter-spacing: 0.55em;
  line-height:    56px;
  color:          var(--white);
  /* Docked to the BOTTOM-LEFT corner of the last card (Vicente
     2026-07-30). Left: .vision-bento is width:100% of the section's
     padding box and the last tile spans all 4 columns, so the card's
     left edge IS the section's content edge — text-align:left with no
     margin puts the "..." exactly on it (the old right alignment +
     -0.55em margin hung it off the opposite corner).
     Bottom: a negative margin equal to the line box pulls the element
     back over the card, so its last line closes on the card's bottom
     edge instead of floating a full line below the grid. The line box
     is empty at that corner on every tier (04/05 keep their text
     top-left and their artwork right/centre), and the element is
     aria-hidden and non-interactive, so the overlap can't cover or
     block anything. */
  text-align:     left;
  margin-right:   0;
  margin-top:     -56px;    /* = line-height, see above */
}

@media (max-width: 900px) {
  .vision-scroll__close { font-size: 32px; line-height: 40px; margin-top: -40px; }
}
@media (max-width: 640px) {
  .vision-scroll__close { font-size: 28px; line-height: 34px; margin-top: -34px; }
}

@media (max-width: 900px) {
  .vision-scroll__header {
    grid-template-columns: 1fr;
    gap:                   var(--space-24);
  }
  .vision-scroll__lead  {
    padding-top: 0;
    max-width:   none;
    margin-left: 0;
    text-align:  left;
  }
  .vision-scroll__title { font-size: 32px; line-height: 40px; }
}
@media (max-width: 640px) {
  .vision-scroll__title { font-size: 28px; line-height: 34px; }
}

/* ══ Tile 02 (AI native) — home-b layout (Vicente 2026-07-28):
   top row = text block LEFT | terminal RIGHT; the agents graph
   below spans the full tile width. ══ */
/* GRID (Vicente 2026-07-28): row 1 = text (up to "verifies
   itself.") | terminal — the terminal STRETCHES to exactly the text
   block's height. Row 2 = agents svg full width, flush under the
   terminal. Row 3 = the accent line, under the agents. */
.vision-ai__top {
  display:               grid;
  grid-template-columns: minmax(0, 45fr) minmax(0, 55fr);
  grid-template-rows:    auto auto auto;
  /* row-gap 0: the agents svg must TOUCH the terminal's bottom edge
     so the fan edges connect to it. */
  gap:                   0 var(--space-24);
  width:                 100%;
}
.vision-ai__top > .vision-agents {
  grid-row:    2;
  grid-column: 1 / -1;
  margin-top:  0;            /* cancel vision.css -12px pull-up */
}
.vision-ai__text {
  grid-column:    1;
  grid-row:       1;
  display:        flex;
  flex-direction: column;
  gap:            var(--space-12);
}
/* Terminal = normal grid item, default stretch → its height IS the
   text block's height (row 1 is sized by the text column only,
   because the scripted lines are absolutely positioned inside the
   body and contribute NOTHING to layout — no pumping while they
   type/fade, overflow just clips). */
.vision-ai__top > .vision-claude {
  grid-column:    2;
  grid-row:       1;
  min-width:      0;
  height:         auto;
  display:        flex;
  flex-direction: column;
}
.vision-ai__top > .vision-claude .vision-claude__body {
  position:   relative;
  flex:       1 1 auto;
  min-height: 0;
  overflow:   hidden;
}
.vision-ai__top > .vision-claude .vision-term__lines {
  /* abspos mirrors the body's padding (vision.css: 20px 18px 24px) */
  position: absolute;
  inset:    20px 18px 24px;
  overflow: hidden;
}
/* .vision-bento prefix: (0,3,0) — must BEAT the tie with
   `.vision-bento .vision-slide__body` (46ch/balance, later in file). */
.vision-bento .vision-ai__top > .vision-slide__body--accent {
  grid-column: 1 / -1;
  grid-row:    3;
  /* ONE line (Vicente 2026-07-28): lift the shared 46ch measure +
     balance wrap from .vision-bento .vision-slide__body. */
  max-width:   none;
  text-wrap:   initial;
  white-space: nowrap;
  margin:      var(--space-12) 0 0;
  /* Soft purple @50%, baked into the color (Vicente 2026-07-28).
     ⚠ Decorative italics: fails AA at this level — approved. */
  color:       rgba(201, 127, 255, 0.5);
}
@media (max-width: 940px) {
  /* DOM order (text → terminal → agents → accent) is already the
     right mobile order — plain column, no order overrides needed. */
  .vision-ai__top { display: flex; flex-direction: column; }
  .vision-ai__top > .vision-claude { height: 260px; }
  .vision-ai__top > .vision-agents { margin-top: var(--space-12); }
  /* Narrow screens: let the accent wrap rather than overflow. */
  .vision-bento .vision-ai__top > .vision-slide__body--accent { white-space: normal; }
}

/* Square agent nodes, no border; orthogonal edges render crisp
   (all segments are pure H/V, so crispEdges can't distort them). */
.vision-agents__node-bg { stroke: none; }
.vision-agents__edge    { shape-rendering: crispEdges; }
.vision-agents__edge--comm { shape-rendering: auto; } /* keeps arrowheads clean */

/* ── "Business mind" tile pills (home-b only): swap the purple pill
   style for the same tone-on-tone navy chips used on the projects
   section categories — flat corners, no purple. ── */
.vision-pill {
  background: color-mix(in srgb, var(--white) 9%, var(--bg-hero));
  color:      color-mix(in srgb, var(--white) 62%, var(--bg-hero));
  /* corners come from --chip-radius (vision.css) — no local override */
}
.vision-pill:hover {
  background: color-mix(in srgb, var(--white) 14%, var(--bg-hero));
}
/* ============================================================
   My Vision bento — home-b-only overrides
   1) Reorder: values tile now leads (row 1), business tile drops
      to row 2 — DOM was swapped in home-b.html; grid placement
      follows here since vision.css still targets the class names
      (--vision / --values), not DOM order.
   ============================================================ */
.vision-bento__tile--values { grid-column: 1 / 3; grid-row: 1; }
.vision-bento__tile--vision,
.vision-bento__tile--business { grid-column: 1 / 3; grid-row: 2; }

/* Row 3 (Vicente 2026-07-28): 04 background joins the LEFT column
   (under 01 + 03) and 05 flow joins the RIGHT column (under 02 —
   02 keeps its rows-1/2 span, so its height is untouched). Both
   tiles share grid-row 3, so the two columns always close at the
   same total height (grid rows are shared across columns). */
.vision-bento__tile--background { grid-column: 1 / 3; grid-row: 3; }
.vision-bento__tile--flow       { grid-column: 3 / 5; grid-row: 3; }

@media (max-width: 940px) {
  /* vision.css already resets --vision/--values/--ai to auto-flow at
     this breakpoint via the same selector list — re-declare here only
     because specificity/source-order would otherwise let the desktop
     rule above (loaded after vision.css) win inside this query too. */
  .vision-bento__tile--values,
  .vision-bento__tile--vision,
  .vision-bento__tile--business,
  .vision-bento__tile--background,
  .vision-bento__tile--flow {
    grid-column: span 2;
    grid-row:    auto;
  }

  /* Re-stack text over graphic ≤940px — the desktop `row` rule above
     would otherwise beat vision.css's tablet column rule by source
     order. flex: 0 0 auto: a 0 basis collapses the graphic frame in
     column direction (scaled/absolute content → no min-content). */
  .vision-bento__tile--background,
  .vision-bento__tile--flow {
    flex-direction: column;
    align-items:    stretch;
  }
  .vision-bento__tile--background .vision-bento__text,
  .vision-bento__tile--flow       .vision-bento__text,
  .vision-bento__tile--background .vision-bento__graphic-frame,
  .vision-bento__tile--flow       .vision-bento__graphic-frame {
    flex: 0 0 auto;
  }
}

@media (max-width: 640px) {
  .vision-bento__tile--values,
  .vision-bento__tile--vision,
  .vision-bento__tile--business,
  .vision-bento__tile--background,
  .vision-bento__tile--flow {
    grid-column: span 1;
    grid-row:    auto;
  }
}

/* ============================================================
   2) Shrink every bento card — tighter padding/gap rhythm than
      vision.css's default, without clipping the scaled graphics.
   ============================================================ */
.vision-bento { gap: var(--space-16); }

.vision-bento__tile {
  padding: var(--space-24);
  gap:     var(--space-12);
}

.vision-bento__tile--background,
.vision-bento__tile--flow,
.vision-bento__tile--ai {
  padding-bottom: var(--space-24);
}

.vision-bento__tile--background,
.vision-bento__tile--flow {
  gap: var(--space-24);
}

/* Half-width text|graphic split (Vicente 2026-07-28): text gets the
   wider share (~56%), graphic ~44%, and the bento measure cap is
   lifted inside these two tiles so the copy uses its full column.
   Desktop-only: these rules sit LATER in the file than the ≤940px
   re-stack block above, so unscoped they'd win on tablet by source
   order and collapse the frame (percent basis on column height). */
@media (min-width: 941px) {
  /* Free-canvas layout (Vicente 2026-07-28): the text block sits
     top-left in normal flow, like every other vision card; the
     graphic frame is absolutely positioned over the WHOLE tile so
     the artwork isn't boxed by a flex slot — it clips at the card
     edge (tile overflow: hidden), which is explicitly OK for 04's
     rotating corners. */
  .vision-bento__tile--background,
  .vision-bento__tile--flow {
    position:        relative;
    flex-direction:  column;
    align-items:     flex-start;
    justify-content: flex-start;
    overflow:        hidden;
    min-height:      480px;
  }
  .vision-bento__tile--background .vision-bento__text,
  .vision-bento__tile--flow       .vision-bento__text {
    flex:      0 0 auto;
    position:  relative;
    z-index:   1;
    max-width: 46ch;
  }
  .vision-bento__tile--background .vision-bento__graphic-frame,
  .vision-bento__tile--flow       .vision-bento__graphic-frame {
    position: absolute;
    inset:    0;
    width:    auto;
    height:   auto;
    margin:   0;
    overflow: visible; /* clipping now happens at the tile edge */
  }

  /* 04 — big circle anchored center-right. 0.8 → ≈464×416 visual;
     worst rotation frame ≈560px > 480px card, so the squares'
     corners clip top/bottom at the card edge (wanted). */
  .vision-bento__tile--background .vision-bento__graphic-frame {
    justify-content: flex-end;
    align-items:     center;
  }
  .vision-bento__tile--background .vision-slide__circle-wrap {
    transform: scale(0.8);
  }

  /* 05 — the chart uses the FULL card width (Vicente 2026-07-28):
     instead of a fixed-scale 399px box centered with dead space at
     both sides, .vision-flow is pinned edge-to-edge (24px in from
     left/right, 20px off the bottom, ~72% of the card tall) and the
     svg stretches into it (preserveAspectRatio="none" +
     non-scaling-stroke in the markup). */
  .vision-bento__tile--flow .vision-flow {
    position:  absolute;
    left:      24px;
    right:     24px;
    bottom:    20px;
    width:     auto;
    height:    72%;
    transform: none;
  }
}
/* Paragraphs in 04/05 wrap to roughly their heading's width so the
   block reads vertical (Vicente 2026-07-28). 04's title ("A global /
   UX designer.") is narrower than 05's → tighter cap. */
.vision-bento__tile--flow       .vision-bento__text .vision-slide__body {
  max-width: 34ch;
}
.vision-bento__tile--background .vision-bento__text .vision-slide__body {
  max-width: 25ch;
}

/* Flow chart geometry (home-b): the SVG viewBox is 399×340 (vs 255
   in vision.css's sizing), so the flow box matches; the "You are
   here" label tracks the start dot at y=267 (dot − 48px, same offset
   as vision.css's 182 → 134). Desktop size/placement lives in the
   free-canvas media block above; tablet/phone fall back to
   vision.css's stacked frames. */
.vision-bento__tile--flow .vision-flow { height: 340px; }

/* "You are here" tracks the start anchor in PERCENT (the flow box is
   no longer a fixed 399×340 on desktop): dot sits at 78.5% height,
   label floats 48px above it — same offset the px version had. */
.vision-flow__label--start { top: calc(78.5% - 48px); }

/* Square markers (HTML overlays, see home-b.html): centered on the
   curve's endpoints via percent anchors — immune to the svg's
   non-uniform stretch. Animated by vision.js via .vision-flow__dot. */
.vision-flow__sq {
  position: absolute;
  width:    12px;
  height:   12px;
}
.vision-flow__sq--start {
  left:       calc(2.88% - 6px);
  top:        calc(78.5% - 6px);
  background: #c97fff;
}
.vision-flow__sq--end {
  left:       calc(94.2% - 6px);
  top:        calc(3.4% - 6px);
  background: #b855ff;
}

@media (max-width: 940px) {
  .vision-bento__tile { padding: var(--space-16); }
}

@media (max-width: 640px) {
  .vision-bento__tile { padding: var(--space-12) var(--space-16); }
}

/* ============================================================
   5) Bottom fade on every card — flat at the current 6% opacity
      until 64px from the bottom, then fades to 3%.
   ============================================================ */
.vision-bento__tile {
  background: linear-gradient(
    to bottom,
    rgba(170, 172, 241, 0.06) 0,
    rgba(170, 172, 241, 0.06) calc(100% - 64px),
    rgba(170, 172, 241, 0.03) 100%
  );
}

/* ============================================================
   6) My Vision — forma del bloque de texto (Vicente 2026-07-27)
      Problema: en los tiles anchos (04 background · 05 flow) el
      párrafo se estiraba ~90ch hasta el borde derecho y la bandera
      derecha quedaba irregular (línea llena → línea corta).
      Fix en dos partes:
        a) medida acotada (--vision-measure) para que el ancho del
           párrafo case con el del titular y el bloque lea como una
           columna, no como una banda;
        b) `text-wrap: balance` reparte los caracteres entre líneas,
           así el borde derecho en bandera queda parejo en vez de
           dentado. `pretty` va delante como fallback para navegadores
           que soportan pretty pero no balance.
      Nota: sube la especificidad a (0,2,0) para ganar a
      `.vision-bento__tile--business .vision-slide__body { 62ch }`
      de vision.css (empate de especificidad → gana el orden de
      fuente, y home-b.css carga después).
   ============================================================ */
.vision-bento { --vision-measure: 46ch; }

.vision-bento .vision-slide__body {
  max-width:  var(--vision-measure);
  text-wrap:  pretty;
  text-wrap:  balance;
  /* Body copy dropped from #fafafa (vision.css) to the same light
     gray as the project descs (--gray-900 = #C8C8C8) — Vicente
     2026-07-28. ~11:1 on --bg-hero, well past AA. White .vision-purple
     accents now pop against the gray base. */
  color:      var(--gray-900);
}

/* Los titulares llevan <br> manuales, pero balance protege el caso
   en que un idioma/zoom fuerce un salto extra. */
.vision-bento .vision-slide__heading {
  text-wrap: balance;
  /* Home-B only: tile titles down from 40px/48 (vision.css) to 32px/40,
     weight 400 (already enforced by the global heading-weight rule above). */
  font-size:   32px;
  line-height: 40px;
  font-weight: 400;
}

/* La fila de chips (Business mind) se acota a la MISMA medida que el
   párrafo (Vicente 2026-07-27): así el bloque de chips cierra en el
   mismo borde derecho que el texto y el tile entero lee como una sola
   columna. `align-content: flex-start` evita que las filas se separen
   si el tile crece de alto. */
.vision-bento .vision-tags-row {
  max-width:     var(--vision-measure);
  align-content: flex-start;
}

/* En móvil el tile ya es más estrecho que la medida, así que el cap
   deja de morder; balance sigue equilibrando la bandera. */
@media (max-width: 640px) {
  .vision-bento { --vision-measure: none; }
}

/* =============================================================
   .discover 3-column layout + contact CTA card (home-b,
   Vicente 2026-07-28): links LEFT · photo MIDDLE · narrow CTA
   RIGHT. The CTA is an EXACT .discover-card (icon/title+desc/
   arrow) with the Delta contact-CTA gradient. All three columns
   stretch to the same height — the photo's img covers its column
   so the row height is shared 1:1.
   ============================================================= */
.discover__inner {
  grid-template-columns: minmax(340px, 1fr) minmax(280px, 440px) minmax(260px, 360px);
  max-width:   none;
  gap:         48px;
  align-items: stretch; /* equal heights: links block, photo, CTA */
}

/* Explicit placement (Vicente 2026-07-29): the DOM order is now
   mobile-first (photo → cta → content, see home-b.html) so tab order
   matches the phone visual order. Desktop/tablet restore the original
   reading order — content · photo · cta — visually via explicit grid
   coordinates, independent of source order. */
.discover__content { grid-column: 1; grid-row: 1; }
.discover__photo   { grid-column: 2; grid-row: 1; }
.discover-cta       { grid-column: 3; grid-row: 1; }

/* Header row above the columns (title + lead were extracted from
   .discover__content so the three columns align top AND bottom). */
.discover__header {
  position:      relative;
  z-index:       2;
  margin-bottom: var(--space-40);
}
.discover__header .discover__lead { margin-top: var(--space-8); }

/* Links column spreads across the full row height — with the
   header gone, its top card and bottom Resume row now align
   exactly with the photo's and CTA's edges. The card rows GROW
   (flex: 1) so each link soaks up the available height instead
   of leaving it as dead gap (Vicente 2026-07-28); Resume keeps
   its natural height at the bottom. */
.discover__content { justify-content: space-between; }
.discover__primary,
.discover__secondary { flex: 1 1 0; }
.discover__primary  .discover-card,
.discover__secondary .discover-card { height: 100%; }

/* Photo fills its column height regardless of which column is
   tallest — this is what makes all three heights identical. */
.discover__photo img {
  height:     100%;
  object-fit: cover;
}

/* CTA — discover-card format; glassy purple veil (Vicente
   2026-07-28): purple → near-transparent gradient + 15px backdrop
   blur, icon + text stacked in a column, arrow pinned top-right. */
.discover-cta {
  position:        relative;
  flex-direction:  column;
  align-items:     flex-start;
  gap:             var(--space-16);
  background:      linear-gradient(135deg,
                     rgba(143, 0, 216, 0.38) 0%,
                     rgba(143, 0, 216, 0.12) 55%,
                     rgba(143, 0, 216, 0.03) 100%);
  backdrop-filter: blur(15px);
  -webkit-backdrop-filter: blur(15px);
  border:          1px solid var(--purple-border);
}
.discover-cta:hover {
  background:   linear-gradient(135deg,
                  rgba(143, 0, 216, 0.48) 0%,
                  rgba(143, 0, 216, 0.18) 55%,
                  rgba(143, 0, 216, 0.06) 100%);
  border-color: rgba(201, 127, 255, 0.55);
}
.discover-cta .discover-card__title {
  font-family: var(--font-headings);
  font-size:   40px;
  font-weight: 400;
  line-height: 48px;
}
.discover-cta .discover-card__desc {
  font-size:   16px;
  line-height: 24px;
  color:       var(--white);
}
.discover-cta .discover-card__arrow {
  position: absolute;
  top:      20px;
  right:    20px;
}

@media (max-width: 1200px) {
  /* CTA drops below content + photo, full width */
  .discover__inner   { grid-template-columns: minmax(280px, 440px) 1fr; }
  .discover__content { grid-column: 1; grid-row: 1; }
  .discover__photo   { grid-column: 2; grid-row: 1; }
  .discover-cta      { grid-column: 1 / -1; grid-row: 2; }
}
@media (max-width: 768px) {
  .discover__inner   { grid-template-columns: 1fr; }
  .discover__content { grid-column: 1; grid-row: 1; }
  .discover__photo   { grid-column: 1; grid-row: 2; }
  .discover-cta      { grid-column: 1; grid-row: 3; }
}

/* =============================================================
   .discover fade-to-page-bg (home-b, Vicente 2026-07-28)
   The hard edge above "Get to know me" wasn't the pixels — it was
   the section's own two background layers starting at full
   strength at y=0:
     1) .discover's indigo gradient (#0A014C at 0%, home.css)
     2) .pixeled-bg's purple top veil (rgba(79,28,136,.70) at 0%)
   Both are rewritten here so the top of the section IS the page
   background (--bg-main) and only the 3 ramp rows of the grid
   (pixeled-bg--fade-top) build up the purple — the fade lives in
   the section itself now.
   ============================================================= */
.discover {
  background: linear-gradient(180deg,
    var(--bg-main) 0%,
    var(--bg-main) 73.56%,
    #0A014C 100%);
}
.pixeled-bg--fade-top {
  background: linear-gradient(180deg,
    rgba(79, 28, 136, 0.00) 0%,
    rgba(79, 28, 136, 0.00) 66.72%,
    rgba(79, 28, 136, 0.27) 83.65%,
    rgba(79, 28, 136, 0.70) 100%);
}

/* (Pixel-fringe container styles + the hero variant blocks —
   orchestrated entrance, eyebrow role/size, tagline size, vertical
   rhythm — MOVED to pages/home.css on 2026-07-29: the home-b hero
   was promoted to the original home, so both pages share them.) */

/* =============================================================
   Who I am — home-b color pass (Vicente 2026-07-28)
   · Scatter pixels removed from the markup (div deleted —
     scatter-pixels.js is guarded and simply no-ops here).
   · Group labels ("Where it comes from" …): #878DEA — that IS
     the existing --blue-800 token (6.50:1 on the hero bg, AA);
     home.css already uses it, re-stated here so the intent
     survives even if home.css's rule changes.
   · Bullet items: were --purple-900 (#E8C6FD, light purple) —
     now near-white light gray, same tone the hero tagline uses
     (rgba(250,250,250,.85) ≈ 12:1, AA at any size).
   ============================================================= */
/* Group labels, 2nd pass (Vicente): --blue-800 read too dim.
   Now they mirror the My Vision tile labels ("01 -My Values" —
   .vision-slide__label): same mono, 16px/24, weight 400, no
   tracking, no uppercase, #c97fff. One voice for every section
   label on the page. (#c97fff ≈ 6.6:1 on this bg — AA at 16px.) */
.about .about__group-label {
  font-family:    var(--font-mono);
  font-size:      16px;
  font-weight:    400;
  line-height:    24px;
  letter-spacing: 0;
  text-transform: none;
  color:          #c97fff;
}
.about .bullet-list__item  { color: rgba(250, 250, 250, 0.85); }

/* Bullet dots read STRONGER than their text (solid purple-900
   squares) — inverted hierarchy. Now white @35%: present but
   clearly subordinate to the copy. */
.about .bullet-list__dot   { background: rgba(255, 255, 255, 0.10); } /* 35% → 10% (Vicente 2026-07-29) */

/* =============================================================
   HOME-B — "Who I am" two-column layout (Vicente spec, 2026-07-28)
   1) Left-edge alignment: .about__grid on the real /home page centers
      itself (max-width:1200px + margin:0 auto), so its content never
      lines up with .vision-scroll / .sticky-projects below it, which
      both use width:100% + padding:_ 96px (no centering). Overridden
      here to the same padding-based model so the first text pixel of
      "Who I am" sits at the same 96px inset as My Vision / the sticky
      projects deck. Breakpoints mirror vision.css's own responsive
      insets (40px @940, 20px @640) for consistency across the page.
   2) Two-column grid for the heading's content:
      - grid-template-columns: 1fr 380px  (left flexible / right sized
        to its content, inside the 360–420px range requested)
      - column-gap: 120px (reuses the value the old .about__content-row
        already used on this page — no new one-off gap)
      - grid-template-areas place bio (top-left) + ai-widget
        (bottom-left) + facts groups (full-height right column). The
        widget's align-self:end + margin-top:40px pins it to the
        bottom of the left column when the facts column is taller,
        while always keeping ≥40px clearance from the bio even when
        both columns are the same height (fr row absorbs the slack).
   3) ≤940px: grid collapses to a single flex column. DOM order (bio →
      facts → widget, see home-b.html) becomes the visual order as-is —
      matches the requested mobile order with no reordering CSS needed.
   ============================================================= */

.about__grid {
  max-width: none;
  width:     100%;
  margin:    0;
  padding:   0 96px 96px; /* bottom 96px unchanged from home.css's original */
}

.about-b__columns {
  display:              grid;
  /* THREE columns (Vicente 2026-07-28): heading+bio LEFT · fact cards
     MIDDLE (max-content = sized to the widest single-line bullet, so
     no bullet ever wraps to two lines) · AI widget RIGHT, bottom-
     aligned with the cards (align-self: end + shared grid bottom). */
  grid-template-columns: minmax(300px, 1.1fr) max-content minmax(280px, 1fr);
  grid-template-rows:    auto 1fr;
  /* facts start on ROW 2 so the first card's top ("Where it comes
     from") sits level with the bio's first line ("I raced three…") —
     the heading row above them stays empty in the middle column. */
  grid-template-areas:
    "heading .     widget"
    "bio     facts widget";
  column-gap: 64px;
  width:      100%;
}

.about-b__columns .about__heading { grid-area: heading; }

/* Every bullet on ONE line (desktop): pairs with the middle column's
   max-content sizing — the column grows to the longest bullet. */
@media (min-width: 941px) {
  .about-b__columns .bullet-list__item { white-space: nowrap; }
}

.about-b__columns .about__bio {
  grid-area: bio;
  /* Width cap removed (2026-07-28): the bio now spans the full left
     column instead of stopping at a fixed measure. */
  max-width: none;
}

.about-b__col-right {
  grid-area: facts;
}

.about-b__columns .ai-widget {
  grid-area:  widget;
  align-self: end;   /* bottom edge level with the cards' bottom */
  /* Own column now — no bio clearance needed. */
  margin-top: 0;
  width: 100%;
}

.about-b__columns .ai-widget__input { width: 100%; }

/* ── Tablet ≤940px: single column, source order = visual order ──── */
@media (max-width: 940px) {
  .about__grid {
    max-width: none;
    width:     100%;
    margin:    0;
    padding:   0 40px 96px; /* matches .vision-scroll's 40px inset here */
  }

  .about-b__columns {
    display:        flex;
    flex-direction: column;
    gap:            48px; /* matches the old .about__content-row tablet gap */
  }

  .about-b__columns .about__bio { max-width: none; }
  .about-b__columns .ai-widget  { align-self: auto; margin-top: 0; }
}

/* ── Phone ≤640px ─────────────────────────────────────────────── */
@media (max-width: 640px) {
  .about__grid {
    max-width: none;
    width:     100%;
    margin:    0;
    padding:   0 20px 96px; /* matches .vision-scroll's 20px inset here */
  }

  .about-b__columns { gap: 32px; } /* matches the old .about__content-row phone gap */
}

/* =============================================================
   HOME-B — widget input bar height = /ai page (Vicente spec, 2026-07-28)
   ai.css's `.page-ai .ai-widget__bar` (the real full-page AI chat
   input, scoped to /ai) sets min-height: 104px desktop / 52px mobile
   on the bar, and 76px / 36px on the textarea — taller than the bare
   `.ai-widget__bar` default (no explicit min-height; only the
   `--stacked` variant used here sets its own 140px). Overriding to
   those exact values gives the home-b widget the same input-bar scale
   as /ai, on top of the stacked (button-below-text) layout already in
   place — this only changes height/padding, not the stacked structure.
   ============================================================= */
.about-b__columns .ai-widget__bar {
  min-height: 104px;
  padding:    2px 2px 2px 24px;
}

.about-b__columns .ai-widget__input {
  min-height: 76px;
}

@media (max-width: 640px) {
  .about-b__columns .ai-widget__bar {
    min-height: 52px;
    padding:    6px 6px 6px 16px;
  }

  .about-b__columns .ai-widget__input {
    min-height: 36px;
    padding:    8px 8px 8px 0;
  }
}

/* =============================================================
   HOME-B — response area grows upward, input stays pinned at the
   bottom (Vicente spec, 2026-07-28)
   The widget is `display:flex; flex-direction:column` (home.css) and
   sits `align-self:end` inside its grid cell (above). DOM order here
   is response → note → input bar, so the input bar is the last flex
   child and its bottom edge is always the widget's bottom edge — the
   thing that has to line up with the last facts card. As answers
   render, the response box's own height grows, and since the grid
   item is bottom-pinned that growth pushes the box's *top* edge
   upward into the bio→input gap, never moving the input. max-height
   caps it so a long reply can't reach the bio paragraph above;
   overflow-y:auto scrolls instead. No card-chrome change — background/
   border stay whatever home.css already sets.
   ============================================================= */
.about-b__columns .ai-widget__response {
  max-height: min(40vh, 420px);
  overflow-y: auto;
}

/* Second (follow-up) answer, appended below the first inside the same
   response box — see modules/ai.js initWidget's multi-answer path,
   enabled here via data-widget-answers="2". var(--space-12) matches
   the widget's own flex `gap` so the QA pairs read as evenly spaced,
   not cramped. */
.about-b__columns .ai-widget__response-text + .ai-widget__response-text {
  margin-top: var(--space-12);
}

/* =============================================================
   HOME-B — third "Where it comes from" bullet: flag pair instead of
   the square dot (Vicente spec, 2026-07-28)
   Same 12px height + 6px top offset as .bullet-list__dot so the flags
   line up with the text baseline exactly like the dot did; the other
   bullets keep their square dot untouched.
   ============================================================= */
.bullet-list__flags {
  display:     flex;
  align-items: center;
  gap:         4px;
  flex-shrink: 0;
  margin-top:  6px; /* matches .bullet-list__dot's alignment offset */
}

.bullet-list__flags img {
  display: block;
  width:   16px;
  height:  12px;
}

/* =============================================================
   Who I am — sticky pin + full-height spread (Vicente 2026-07-28)
   The section holds the viewport for a beat so the visitor
   actually stops on it: a 180vh stage wraps the section; the
   section is sticky/100vh inside it → pinned for ~80vh of
   scroll. Desktop only (>940px) — on tablet/phone the section
   flows normally (stage collapses to auto).
   Content spreads to use the full viewport height: grid/top-block
   stretch, the widget's existing align-self:end drops it to the
   true bottom, and the facts column distributes its three groups
   evenly. Vicente pre-authorized the chips row getting masked
   briefly on short viewports (.about already has overflow:hidden).
   ============================================================= */
@media (min-width: 941px) {
  /* Pinned scroll HALVED (Vicente 2026-07-28): stage 180vh → 140vh.
     The section is 100vh sticky, so the pinned travel is the excess —
     was 80vh, now 40vh. (90vh would break the pin: stage < viewport.) */
  .about-b__pin-stage { height: 140vh; }

  .about-b__pin-stage .about {
    position:   sticky;
    top:        0;
    height:     100vh;
    box-sizing: border-box;
    /* home.css's 120px top/bottom inside a fixed 100vh eats too much
       of the viewport — tighten so the content gets the room. */
    padding-top:    96px;
    /* Body bottom sits 120px above the scroll cue: cue top ≈ 106px from
       the viewport bottom (bottom 40 + ~66px of label+gap+track), so
       106 + 120 ≈ 226. The body gives up that height on purpose. */
    padding-bottom: 226px;
  }

  /* Stretch the whole chain so the columns can fill the height. */
  .about-b__pin-stage .about__grid      { height: 100%; padding-bottom: 0; }
  .about-b__pin-stage .about__top-block { height: 100%; }
  .about-b__pin-stage .about-b__columns { flex: 1 1 auto; min-height: 0; }

  /* Facts column: distribute the three groups across the full height
     instead of hugging the top. */
  /* First card starts a touch HIGHER so the "Where it comes from"
     label's text top lines up with the bio's first line ("I raced
     three years…") — the card's 16px top padding + label/line-height
     offset ≈ 18px. height compensates so the column BOTTOM (aligned
     with the AI input frame) doesn't move. */
  .about-b__pin-stage .about-b__col-right {
    height:     calc(100% + 18px);
    margin-top: -18px;
  }
  .about-b__pin-stage .about__groups { height: 100%; }
  /* Cards stretch: each of the 3 fact cards takes an equal share of
     the column, so the first card's top edge sits level with the bio's
     first line ("I raced three…") and the last card's bottom edge
     lands level with the AI input frame's bottom — both are simply the
     grid cell's top/bottom, which the bio row and the widget's
     align-self:end already define. */
  .about-b__pin-stage .about__group { flex: 1 1 0; }

  /* Scroll cue — far LEFT (aligned with the section's 96px content
     edge), label and track both left-aligned, pure white (Vicente
     2026-07-28 2ª pasada). */
  .about-b__cue {
    position:       absolute;
    left:           96px;
    /* Bottom edge level with the cards' bottom (Vicente 2026-07-28):
       the pinned .about has padding-bottom: 226px, so the content
       (cards/widget) ends exactly 226px above the section edge. */
    bottom:         226px;
    z-index:        2;
    display:        flex;
    /* Label to the RIGHT of the animated track, both bottom-aligned
       (Vicente 2026-07-28) — DOM is label→track, so row-reverse puts
       the track on the left without touching the markup. */
    flex-direction: row-reverse;
    align-items:    flex-end;
    gap:            var(--space-12);
    gap:            var(--space-12);
    pointer-events: none;
  }
  .about-b__cue .hero__cue-label { color: #ffffff; }
  .about-b__cue .hero__cue-track { background: rgba(255, 255, 255, 0.28); }
  .about-b__cue .hero__cue-run   { background: #ffffff; }
}

/* The cue only earns its place while the section pins (desktop);
   in normal flow (≤940px) it would just be noise. */
@media (max-width: 940px) {
  .about-b__cue { display: none; }
}

/* =============================================================
   Who I am — facts cards, dissolve treatment (Vicente 2026-07-28)
   · Background: --card-surface's rgba(170,172,241,.10) at the top
     fading to fully transparent at the bottom; hover keeps the
     same shape one step brighter (.15 = --card-surface-hover).
   · Border fades WITH it: border-image gradient replaces the flat
     1px --purple-card-border (.11 → 0). (border-image ignores any
     border-radius, fine here — cards are flat-cornered.)
   · Labels stay top-left; the bullet blocks center vertically in
     the remaining card space via margin:auto.
   ============================================================= */
.about .about__group {
  background: linear-gradient(180deg, rgba(170, 172, 241, 0.10) 0%, rgba(170, 172, 241, 0) 100%);
  border:     none;  /* border-image fade retirado (Vicente 2026-07-29) */
}
.about .about__group:hover {
  background: linear-gradient(180deg, rgba(170, 172, 241, 0.15) 0%, rgba(170, 172, 241, 0) 100%);
}
.about .about__group .bullet-list {
  /* Top-aligned under the label (Vicente 2026-07-29 — was centered
     vertically via margin auto/auto). 12px of air after the label. */
  margin-top:    4px;   /* 12 → 4 (Vicente 2026-07-29) */
  margin-bottom: 0;
}

/* =============================================================
   Who I am — bottom pixel separator (Vicente 2026-07-28)
   The symmetric --separator fringe sits absolutely on the
   section's bottom edge (inside its overflow), so during the
   sticky pin it reads as the section's own floor and hands off
   into Featured projects. The scroll cue (z-index 2) floats ON
   TOP of the strip. Rows are shorter than the page fringes
   (min(3.5vw, 52px) vs 6.25vw/90px): it's a divider, not a band.
   ============================================================= */
.about .pixel-fringe--separator {
  position: absolute;
  left:     0;
  right:    0;
  bottom:   0;
  z-index:  1;
}
.about .pixel-fringe--separator .pixel-row {
  height: min(3.5vw, 52px);
}

/* =============================================================
   Featured projects — bottom-only soft mask (Vicente 2026-07-28,
   4ª pasada: side/top masks removed — they didn't sit right).
   Single bottom fade, eased with intermediate stops so the
   dissolve ramps in gently instead of a linear wipe: solid until
   140px from the bottom, then 72% → 40% → 15% → 0. No composite
   needed with one gradient. Applied to the CARD (bg + photo as
   one unit) and the mobile image wrapper.

   Desktop-only for the CARD (fix round, Vicente 2026-07-29): on
   ≤900 the pinned card's bottom edge sits glued to the viewport's
   own bottom edge for the entire pinned/is-ended scroll (sticky-col
   margin-top:auto inside a 100svh pin) — so this mask's transparent
   140px band always coincides with the viewport's bottom 140px.
   Harmless while whatever sits behind is the same dark section bg,
   but at the pinned frame's hand-off the next section's (Vision)
   content scrolls into that exact screen position and bleeds
   through the "transparent" mask straight into view — a double
   exposure of Vision's heading/paragraph over the still-stuck
   project image. The card is short enough on mobile/tablet (40–44svh)
   that the fade isn't needed there anyway — a hard edge reads fine
   against the section's own solid bg. Reproduced + fixed via
   Playwright captures, see scratchpad/handoff/. Mobile image wrapper
   (reduced-motion static layout only) is untouched — different
   context, not part of this bug. */
@media (min-width: 901px) {
  .sticky-projects__card {
    -webkit-mask-image: linear-gradient(to bottom,
      #000 0,
      #000 calc(100% - 140px),
      rgba(0, 0, 0, 0.72) calc(100% - 96px),
      rgba(0, 0, 0, 0.40) calc(100% - 56px),
      rgba(0, 0, 0, 0.15) calc(100% - 24px),
      transparent 100%);
    mask-image: linear-gradient(to bottom,
      #000 0,
      #000 calc(100% - 140px),
      rgba(0, 0, 0, 0.72) calc(100% - 96px),
      rgba(0, 0, 0, 0.40) calc(100% - 56px),
      rgba(0, 0, 0, 0.15) calc(100% - 24px),
      transparent 100%);
  }
}
.sticky-projects__block-mobile-image {
  -webkit-mask-image: linear-gradient(to bottom,
    #000 0,
    #000 calc(100% - 140px),
    rgba(0, 0, 0, 0.72) calc(100% - 96px),
    rgba(0, 0, 0, 0.40) calc(100% - 56px),
    rgba(0, 0, 0, 0.15) calc(100% - 24px),
    transparent 100%);
  mask-image: linear-gradient(to bottom,
    #000 0,
    #000 calc(100% - 140px),
    rgba(0, 0, 0, 0.72) calc(100% - 96px),
    rgba(0, 0, 0, 0.40) calc(100% - 56px),
    rgba(0, 0, 0, 0.15) calc(100% - 24px),
    transparent 100%);
}

/* =============================================================
   "02 -AI native" bento tile — scripted terminal playback
   (Vicente 2026-07-28, home-b only). Replaces the shared narrative
   terminal script from claude-window.js with a "VHS Agentic OS"
   demo: a typed prompt, then an orchestrator response calling out
   to specialist agents. Driven by vision-terminal.js.

   Deliberately scoped to `.vision-term__lines` / `#visionTerminal`
   — never `.vision-claude__lines` — so this never collides with
   claude-window.js, which still owns index.html's Vision terminal.
   `.vision-claude` / `__chrome` / `__body` frame styles come from
   vision.css (read-only, untouched) and are reused as-is.
   ============================================================= */

/* Defensive: if `.vision-claude__lines` (the old abstract-bars /
   narrative-script container) is ever reintroduced inside this
   tile, keep it inert so it can't render behind the scripted
   playback. */
.vision-bento__tile--ai .vision-claude__lines {
  display: none;
}

.vision-term__lines {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-4);
  /* Height stability: the lines are abspos inside the terminal body
     (see .vision-ai__top rules) — they contribute nothing to layout. */
  opacity:        1;
  transition:     opacity 420ms ease; /* FADE_MS in vision-terminal.js */
}

.vision-term__lines.is-fading {
  opacity: 0;
}

.vision-term__line {
  white-space: pre-wrap;
  word-break:  break-word;
}

/* ── Step 1: typed prompt line ────────────────────────────── */
.vision-term__line--prompt {
  color:       #ffffff;
  font-weight: 500;
}

.vision-term__line--prompt.is-typing::after {
  content:        '';
  display:        inline-block;
  width:          7px;
  height:         14px;
  margin-left:    2px;
  vertical-align: -2px;
  background:     currentColor;
  animation:      vision-term-blink 0.85s steps(2) infinite;
}

@keyframes vision-term-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0; }
}

/* ── Step 2: response lines — fade + slide in, not typed ──── */
.vision-term__line--bullet,
.vision-term__line--agent,
.vision-term__line--success {
  opacity:    0;
  transform:  translateY(4px);
  transition: opacity 0.38s ease, transform 0.38s ease;
}

.vision-term__line--bullet.is-visible,
.vision-term__line--agent.is-visible,
.vision-term__line--success.is-visible {
  opacity:   1;
  transform: translateY(0);
}

.vision-term__bullet-mark,
.vision-term__agent-mark {
  color: #c97fff; /* brand purple — consistent literal with vision.css */
}

.vision-term__success-mark {
  color: #6EE7B7; /* green tone — AA on the dark tile background */
}

.vision-term__body {
  color: rgba(250, 250, 250, 0.75);
}

@media (prefers-reduced-motion: reduce) {
  .vision-term__lines,
  .vision-term__lines.is-fading,
  .vision-term__line--bullet,
  .vision-term__line--agent,
  .vision-term__line--success {
    transition: none;
  }
  .vision-term__line--prompt.is-typing::after {
    animation: none;
    opacity:   0.6;
  }
}


/* =============================================================
   Who I am — AI widget CHAT THREAD (Vicente 2026-07-28)
   The widget shows the conversation as chat bubbles above the
   input, reusing the /ai page's .ai-msg components verbatim
   (ported from pages/ai.css — that sheet only loads on /ai).
   Container is widget-sized: capped height + scroll, compact gap.
   ============================================================= */
.ai-widget__thread {
  display:         none;
  flex-direction:  column;
  gap:             20px;
  overflow-y:      auto;
  max-height:      340px;
  margin-bottom:   16px;
  scroll-behavior: smooth;
}
.ai-widget__thread.is-active { display: flex; }
.ai-widget__thread::-webkit-scrollbar       { width: 4px; }
.ai-widget__thread::-webkit-scrollbar-track { background: transparent; }
.ai-widget__thread::-webkit-scrollbar-thumb { background: rgba(155, 135, 245, 0.2); }

/* ── Message row (= ai.css) ── */
.ai-msg {
  display:   flex;
  gap:       10px;
  animation: ai-msg-in 0.3s cubic-bezier(0.2, 0, 0, 1) both;
}
@keyframes ai-msg-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
.ai-msg--user { justify-content: flex-end; }

/* ── Avatars (= ai.css, scaled 44 → 36px for the widget) ── */
.ai-msg__avatar,
.ai-msg__user-avatar {
  width:           36px;
  height:          36px;
  min-width:       36px;
  border:          1px solid rgba(170, 172, 241, 0.11);
  display:         flex;
  align-items:     center;
  justify-content: center;
  flex-shrink:     0;
}
.ai-msg__avatar      { background: rgba(8, 12, 29, 0.95); }
.ai-msg__user-avatar { background: rgba(13, 20, 51, 0.95); }
.ai-msg__avatar svg rect { fill: #FFFFFF; }
.ai-msg__user-avatar svg circle,
.ai-msg__user-avatar svg path { stroke: #FFFFFF; }

/* ── Bubbles (= ai.css) ── */
.ai-msg__bubble {
  background:  rgba(8, 12, 29, 0.95);   /* --bg-main @ 95% */
  border:      1px solid rgba(170, 172, 241, 0.11);
  padding:     14px 16px;               /* 20/24 → compact for the widget */
  font-family: var(--font-mono);
  font-size:   12px;
  font-weight: 400;
  line-height: 16px;
  color:       rgba(255, 255, 255, 0.92);
  white-space: pre-wrap;
  word-break:  break-word;
  max-width:   75%;
}
.ai-msg--ai .ai-msg__bubble   { max-width: 80%; }
.ai-msg--user .ai-msg__bubble { background: rgba(13, 20, 51, 0.95); }

.ai-msg__text {
  font-family: var(--font-mono);
  font-size:   12px;
  font-weight: 400;
  line-height: 20px;
  color:       rgba(255, 255, 255, 0.92);
  white-space: pre-wrap;
  word-break:  break-word;
}
.ai-msg__text--error { color: rgba(255, 110, 110, 0.85); }

.ai-link {
  color:                 rgba(155, 135, 245, 0.9);
  text-decoration:       underline;
  text-underline-offset: 2px;
}
.ai-link:hover { color: #9b87f5; }


/* =============================================================
   Hero — eyebrow as a standalone top line (home-b only,
   Vicente 2026-07-29). Pulled out of .hero__left in the markup:
   · vertical: centered with the topbar AI widget's initial spot
     (topbar is fixed top:48px, the input is 48px tall → its
     center sits 72px from the top; eyebrow line-height is 22px →
     top = 72 − 11 = 61px. No transform: the hero-b-reveal
     entrance animation owns `transform` and would cancel it).
   · horizontal: same content inset as .hero__body / the rest of
     the page's text (max(96px, centered-1200px offset)).
   ============================================================= */
.hero > .hero__eyebrow {
  position:   absolute;
  z-index:    3;
  left:       max(96px, calc((100% - 1200px) / 2));
  /* 16px → 18px (+2, Vicente 2026-07-29); line-height follows 22 → 24,
     so the vertical centering against the topbar widget (center at
     72px) becomes top = 72 − 12 = 60px. */
  font-size:   18px;
  line-height: 24px;
  top:         60px;
  margin-top:  0; /* cancels the in-block rhythm rule from home.css */
}

/* Phone: the fixed topbar is replaced by the mobile nav bar — the
   absolute spot would collide with it. Back into flow, above the h1. */
@media (max-width: 640px) {
  .hero > .hero__eyebrow {
    position: static;
    /* 88px top (Vicente 2026-07-29): the fixed mobile nav spans
       16-72px — 16px margin buried the name under it. */
    margin:   136px 20px 0;  /* 88 + 48 (Vicente 2026-07-29) */
  }
}

/* Tablet (641–900, verified live at 834px): the fixed topbar (globe +
   Ask My AI + Reach out, styles.css .topbar) stays visible all the way
   down to 640px — only .sidebar/.hero layout narrows at ≤900 (see
   styles.css:1046-1058). At that narrower width the absolute eyebrow
   (left: 96px fixed inset) collides with the topbar's right-aligned
   buttons — there isn't enough horizontal room for both. Same static-
   in-flow fix as phone, matching .hero__body's 40px side padding at
   this tier instead of the phone's 20px (Vicente 2026-07-29 fix round).
   ============================================================= */
@media (min-width: 641px) and (max-width: 900px) {
  .hero > .hero__eyebrow {
    position: static;
    margin:   64px 40px 0;  /* clear of the top chrome (Vicente 2026-07-29) */
  }
}


/* Hero primary is now an <a> (was <button>) scrolling to projects —
   kill the anchor underline (Vicente 2026-07-29). */
.hero a.hero__btn { text-decoration: none; }


/* =============================================================
   Hero — thin down arrow under the text block (home-b only,
   Vicente 2026-07-29; ref: "Good work for great people"). Light
   weight, left-aligned with the copy, clear space above; joins
   the entrance stagger as the last beat and bobs gently after.
   ============================================================= */
.hero__down-arrow {
  /* Mirrors the eyebrow (Vicente 2026-07-29): the eyebrow sits 60px
     from the section TOP, so the arrow pins 60px from the section
     BOTTOM — absolute, same left inset as all the hero text. Head
     ("aspas") enlarged: 14px per side (was 10). */
  position:  absolute;
  left:      max(96px, calc((100% - 1200px) / 2));
  bottom:    60px;
  z-index:   3;
  display:   block;
  width:     32px;
  height:    80px;
  margin:    0;
  color:     rgba(250, 250, 250, 0.80); /* = .hero__tagline gray */
  animation: hero-b-reveal 500ms ease-out 350ms both;
}
@media (prefers-reduced-motion: reduce) {
  .hero__down-arrow { animation: none; opacity: 1; transform: none; }
}


/* AI trigger link: sidebar's My AI icon inline before the text
   (Vicente 2026-07-29). The underline stays on the TEXT only —
   the link itself drops it and a nested span carries it. */
.hero__link {
  display:     inline-flex;
  align-items: center;
  gap:         var(--space-8);
}
button.hero__link { text-decoration: none; }
.hero__link .hero__link-text { text-decoration: underline; text-underline-offset: 3px; }
.hero__link-icon { display: block; width: 20px; height: 20px; }

/* =============================================================
   Task 2 (responsive pass) — hero CTA pair stacks ≤640px.
   "See my work" stays a full-width bordered button (already 48px
   tall); "Ask my AI anything" is a quiet text link, not a filled
   pill — kept visually as-is but given a full-width ≥44px row so
   it reads as one stacked list, DOM order = visual order.
   ============================================================= */
@media (max-width: 640px) {
  .hero__ctas {
    flex-direction: column;
    align-items:    stretch;
    gap:            var(--space-16);
  }
  /* Container shrinks to the widest child (the icon + "Ask my AI
     anything" row) and "See my work" stretches to MATCH that width
     (Vicente 2026-07-29) — no more full-bleed button. */
  .hero__ctas     { width: fit-content; }
  .hero__btn      { width: 100%; }
  .hero__cta-wrap { width: 100%; }
  .hero__link {
    width:      100%;
    min-height: 44px;
  }
  /* Down arrow: align with the hero text inset (20px) and keep only
     the RIGHT branch of the head (Vicente 2026-07-29). */
  /* The shaft sits at x=16 inside the 32px SVG canvas; with the left
     branch hidden the VISIBLE line must align at 20px → box at 4px
     (Vicente 2026-07-29, "míralo bien" fix). */
  .hero__down-arrow { left: 4px; }
  .hero__down-arrow-left { display: none; }
}

/* Tablet: no override needed — the base .hero__down-arrow left
   (max(96px, centered-1200px)) already equals the hero text inset at
   this tier (verified live at 834px: arrow 95 = h1 95). */

/* =============================================================
   Task 4 (responsive pass, fix round Vicente 2026-07-29) —
   "Get to know me" PHONE order, WCAG 2.4.3 fix: title/lead
   (.discover__header, untouched) → photo → "Let's talk" CTA →
   remaining discover-card links. DOM order in home-b.html now
   IS this phone order (photo → cta → content), so tab order
   matches reading order — no CSS `order` hack needed here.
   Desktop/tablet (≥641px, above) restore the original content ·
   photo · cta reading order via explicit grid placement instead.
   ============================================================= */
@media (max-width: 640px) {
  .discover__inner   { grid-template-columns: 1fr; }
  .discover__photo   { grid-column: 1; grid-row: 1; }
  .discover-cta       { grid-column: 1; grid-row: 2; }
  .discover__content { grid-column: 1; grid-row: 3; }
}

/* =============================================================
   Task 5 (responsive pass, superseded 2026-07-30 for 641-900 — see
   the tablet-bento block right below) — My Vision bento, 901-940px
   ONLY now (the narrow open-issue sliver between the tablet bento
   below and vision.css's own 940 breakpoint: sidebar still desktop
   here, collapsed layout — left as the existing 1-col fallback,
   untouched). 641-900 no longer matches this query — it gets a real
   2-col bento instead (below).
   ============================================================= */
@media (min-width: 901px) and (max-width: 940px) {
  .vision-bento { grid-template-columns: 1fr; }
}

/* =============================================================
   Tablet bento, 641-900px (Vicente 2026-07-30) — real 2-column
   bento instead of the 1-col stack above. Column count itself
   already comes from vision.css's own ≤940 breakpoint
   (grid-template-columns: repeat(2, 1fr)) — untouched here, still
   active in this range since the 1-col override above no longer
   reaches down to 900.
   Arrangement (mirrors desktop's dense flow, adapted to 2 cols):
     col 1, row 1 → 01 Values
     col 1, row 2 → 03 Business
     col 2, rows 1-2 (tall) → 02 Agentic systems
     both cols, row 3 → 04 Background (full width)
     both cols, row 4 → 05 Flow (full width)
   02 stays legible at ~half width because its internal layout
   already stacks text → terminal → agents in a column at ≤940 (see
   the "Tile 02 (AI native)" block above) — a half-width column just
   means that stack is narrower, not squeezed sideways; the terminal
   (font-mono, word-break, width:100%) and the agents svg
   (width:100%, scalable viewBox) both reflow cleanly. Tested at
   768/834: terminal text and agent nodes stay crisp, no distortion.
   Selectors match home-b's own ≤940 stacking rule above in
   specificity (0,1,0) — this block wins on source order (loads
   later in the file), same mechanism the rest of this stylesheet
   already relies on throughout. */
@media (min-width: 641px) and (max-width: 900px) {
  .vision-bento__tile--values {
    grid-column: 1;
    grid-row:    1;
  }
  .vision-bento__tile--vision,
  .vision-bento__tile--business {
    grid-column: 1;
    grid-row:    2;
  }
  .vision-bento__tile--ai {
    grid-column: 2;
    grid-row:    1 / 3;
  }
  .vision-bento__tile--background {
    grid-column: 1 / 3;
    grid-row:    3;
  }
  .vision-bento__tile--flow {
    grid-column: 1 / 3;
    grid-row:    4;
  }
}


/* ═══ Phone = plain scroll (Vicente 2026-07-29) ═══
   sticky-projects.js still toggles .is-pinned/.is-ended on scroll;
   these resets (last in file — win every equal-specificity tie) make
   those states inert ≤640 so nothing dims, hides or swaps. */
@media (max-width: 900px) {
  #stickyProjects.is-pinned .sticky-projects__title,
  #stickyProjects.is-pinned .sticky-projects__lead,
  #stickyProjects.is-ended  .sticky-projects__title,
  #stickyProjects.is-ended  .sticky-projects__lead {
    opacity:    1;
    visibility: visible;
  }
  #stickyProjects.is-pinned .sticky-projects__intro .sticky-projects__cta-row,
  #stickyProjects.is-ended  .sticky-projects__intro .sticky-projects__cta-row {
    opacity:        1;
    pointer-events: auto;
  }
  /* Header CTAs live under the lead on phone — the end-of-scroll
     duplicate row is desktop/tablet choreography only. */
  .sticky-projects__cta-row--end { display: none; }
}


/* Short label only exists on phone (default hidden everywhere). */
.sticky-projects__cta-label-short { display: none; }

/* (fixed CTA dock retired 2026-07-29 — the row now lives inside the sticky stage; see git) */


/* ═══ Phone project swap v4 (Vicente 2026-07-29) — PHONE + TABLET ═══
   Scroll-controlled sticky stage (built by sticky-projects.js on
   <=900 + motion OK — widened from <=640, Vicente 2026-07-30 tablet
   port): .sticky-projects__stage is sticky and viewport tall, holding
   the four units stacked in the same frame + the CTA row at its
   bottom. The empty steps in flow are the scroll control: each one
   consumed = crossfade to the next unit. Sticky = smooth natural
   entry and exit. Desktop/reduced-motion: untouched (the stage div is
   never created there — every rule below except the header one is
   stage-scoped, so the <=900 static fallback block above keeps
   serving reduced-motion users unchanged). Tablet proportion deltas
   live in the 641-900 block right after this one. */
@media (max-width: 900px) {
  .sticky-projects__stage {
    position:   sticky;
    /* Full viewport (Vicente 2026-07-29): the navbar hides while the
       stage is consumed (body.sp-immersive below), so the stage owns
       the whole screen — no topbar reserve. */
    top:        0;
    /* 100svh CONSTANT (Vicente 2026-07-29, 2ª vuelta): dvh made the
       text→image distance breathe with the browser bar. svh keeps
       EVERY internal distance fixed; the viewport slack shows up as a
       page-bg band under the whole unit when the chrome collapses —
       the approved trade-off ("mejor por debajo del todo"). */
    height:     100svh;
    margin:     0 -20px;            /* full-bleed over the section's 20px sides */
    background: var(--bg-hero);
    overflow:   hidden;
    z-index:    5;
  }
  /* 60svh = 40% less scroll per project (Vicente 2026-07-29). */
  .sticky-projects__stage ~ .sticky-projects__step { height: 60svh; min-height: 0; }
  /* Delta fix (Vicente 2026-07-29): with 60svh steps the sticky range
     ended BEFORE the last step could activate — P4 swapped mid ride-out
     and never fully showed. Extra travel on the last step restores its
     activation margin + a dwell comparable to the other projects. */
  .sticky-projects__stage ~ .sticky-projects__step:last-child { height: 140svh; } /* Delta dwell ampliado, 2ª vuelta (Vicente 2026-07-29) */
  /* P1 dwell fix (Vicente 2026-07-29): the stage's own in-flow height
     (100svh) was counting as extra Housing-Help time — P2 arrived at
     ~1280px of scroll vs ~506 for the others. Pull the driver train up
     under the stage so every project gets a comparable dwell. */
  .sticky-projects__stage + .sticky-projects__step { margin-top: -90svh; }
  /* Less dead space between the section header and the stage.
     :has() guard added (Vicente 2026-07-30): only when the stage
     exists — reduced-motion (no stage) keeps the static fallback's
     56px header margin. */
  .sticky-projects:has(.sticky-projects__stage) .sticky-projects__header { margin-bottom: 16px; }
  .sticky-projects__stage ~ .sticky-projects__step + .sticky-projects__step { margin-top: 0; }

  .sticky-projects__stage .sticky-projects__block {
    position:       absolute;
    inset:          0;
    display:        flex;
    flex-direction: column;
    box-sizing:     border-box;
    /* FIXED paddings, no env() (Vicente 2026-07-29): safe-area-inset
       changes value when the browser bar collapses — it was the last
       source of the image+CTA cluster breathing. Constant 96px keeps
       the buttons clear of the home indicator in every state, even if
       they do not kiss the physical edge. */
    padding:        72px 20px 96px;
    opacity:        0;
    visibility:     hidden;
    pointer-events: none;
    transition:     opacity 0.9s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.9s;
  }
  .sticky-projects__stage .sticky-projects__block.is-active {
    opacity:        1;
    visibility:     visible;
    pointer-events: auto;
    transition:     opacity 0.9s cubic-bezier(0.4, 0, 0.2, 1);
  }

  /* Text budget so the unit always fits the frame. */
  .sticky-projects__stage .sticky-projects__block-desc {
    display:            -webkit-box;
    -webkit-box-orient:  vertical;
    -webkit-line-clamp:  4;  /* 3 → 4 (Vicente 2026-07-30): Beloo's desc was cut mid-word */
    overflow:            hidden;
  }

  /* Phone card polish (Vicente task #5): logo + title share one line,
     tighter 8px vertical rhythm, persistent underline on the link
     (touch has no hover to reveal it). */
  .sticky-projects__stage .sticky-projects__block { gap: var(--space-8); }
  .sticky-projects__stage .sticky-projects__block-heading {
    flex-direction: row;
    align-items:    center;
    gap:            var(--space-12);
  }
  .sticky-projects__stage .sticky-projects__block-icon {
    width:  32px;
    height: 32px;
  }
  /* Vicente 2026-07-29: 24px total between the Discover link and the
     typology chips (8px block gap + 16px margin). Role chip is moved
     into this rail by JS; ONE non-wrapping row, slides laterally,
     full-bleed to the phone edges with 16px fade masks inward. */
  .sticky-projects__stage .sticky-projects__block-categories {
    margin-top:      16px;
    display:         flex;
    flex-wrap:       nowrap;
    overflow-x:      auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin-left:     -20px;   /* reach the stage (=viewport) edges */
    margin-right:    -20px;
    padding:         0 20px;
    -webkit-mask-image: linear-gradient(to right,
                       transparent 0, #000 16px,
                       #000 calc(100% - 16px), transparent 100%);
    mask-image:      linear-gradient(to right,
                       transparent 0, #000 16px,
                       #000 calc(100% - 16px), transparent 100%);
  }
  .sticky-projects__stage .sticky-projects__block-categories::-webkit-scrollbar { display: none; }
  .sticky-projects__stage .sticky-projects__block-categories > * { flex: 0 0 auto; }
  /* Role chip in the rail (Vicente 2026-07-29): pure white type,
     bg 10%→13%, and a vertical separator to the first typology chip
     with 4px of air on each side (rail gap 8 + 1px margin → chip
     |4px| 1px line |4px| chip). */
  .sticky-projects__stage .sticky-projects__block-role-chip {
    margin:       0 1px 0 0;
    position:     relative;
    color:        #FFFFFF;
    background:   rgba(255, 255, 255, 0.13);
  }
  .sticky-projects__stage .sticky-projects__block-role-chip::after {
    content:    "";
    position:   absolute;
    right:      -5px;             /* 4px gap + the 1px line itself */
    top:        4px;
    bottom:     4px;
    width:      1px;
    background: rgba(255, 255, 255, 0.25);
  }
  .sticky-projects__stage .sticky-projects__block-link {
    border-color: currentColor;
  }

  /* Text never shrinks — when the unit is taller than the frame the
     IMAGE gives way (below), so the 3-line desc / link / categories
     always render whole instead of clipping mid-line. */
  .sticky-projects__stage .sticky-projects__block > * { flex: 0 0 auto; }

  /* Image: bottom of the frame, full-bleed to the stage edges. It is
     the only flexible child — grows to fill whatever height the text
     leaves free, shrinks first when space is tight. */
  .sticky-projects__stage .sticky-projects__block-mobile-image {
    order:      99;
    margin:     auto -20px -20px;  /* -20px: banda 20px más abajo (Vicente 2026-07-29) */
    /* SAME height for all four projects (Vicente 2026-07-29): fixed
       band instead of flexible leftover. */
    flex:       0 0 auto;
    height:     46svh;
    min-height: 0;
    padding:    var(--space-16);
  }
  .sticky-projects__stage .sticky-projects__block-mobile-image img {
    width:      100%;
    height:     100%;
    max-height: none;
    object-fit: contain;
  }

  /* ── CTA row docked INSIDE the stage bottom (navbar feel, but it
     enters/leaves WITH the stage — no fixed element, no observer). ── */
  .sticky-projects__stage .sticky-projects__cta-row {
    position:        absolute;
    left:            0;
    right:           0;
    bottom:          0;
    z-index:         10;
    display:         flex;
    flex-direction:  row;
    gap:             var(--space-12);
    padding:         12px 20px 12px;  /* fixed — no env(), see block note */
    background:      color-mix(in srgb, var(--bg-main) 92%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    opacity:         1;
    pointer-events:  auto;
  }
  .sticky-projects__stage .sticky-projects__cta,
  .sticky-projects__stage .sticky-projects__cta-secondary {
    flex:        1 1 0;
    width:       auto;
    height:      44px;
    min-height:  44px;
    font-size:   14px;
    font-weight: 400;
    /* 360px-wide phones: each button gets ~154px — the labels must
       stay on one line inside the 44px bar. */
    white-space:   nowrap;
    padding-left:  var(--space-8);
    padding-right: var(--space-8);
  }
  /* "All projects": fixed light gray + dark type — overrides every
     [data-active-face] tint (equal specificity, later in source). */
  .sticky-projects__stage .sticky-projects__cta {
    background:   var(--gray-900);
    border-color: var(--gray-900);
    color:        var(--gray-400);
  }
  /* Short label replaces the long swap pair. */
  .sticky-projects__stage .sticky-projects__cta-secondary .sticky-projects__cta-swap { display: none; }
  .sticky-projects__stage .sticky-projects__cta-secondary .sticky-projects__cta-label-short { display: inline; }
}


/* ═══ Tablet deltas for the sticky stage (Vicente 2026-07-30) ═══
   641-900 inherits the whole phone stage mechanism above (same 100svh
   constant, fixed paddings, no env()/dvh — non-negotiable geometry)
   and only re-proportions it: full-bleed over the 40px section
   padding, roomier frame paddings, taller image band, mid-tier type,
   and a fit-content CTA row instead of the phone's 50/50 split. */
@media (min-width: 641px) and (max-width: 900px) {
  /* Section side padding is 40px at this tier (static block ~line 590)
     — the stage must reach the real viewport edges. */
  .sticky-projects__stage { margin: 0 calc(-1 * var(--space-40)); }

  /* Roomier frame: 88/104px vertical are FIXED (same no-env() rule as
     the phone's 72/96 — no token at this size, constant by design);
     sides align content with the section's 40px inset. */
  .sticky-projects__stage .sticky-projects__block {
    padding: 88px var(--space-40) 104px;
  }

  /* Type: between phone (24px) and desktop (32px). */
  .sticky-projects__stage .sticky-projects__block-title {
    font-size:   var(--fs-28);
    line-height: 34px;
  }
  .sticky-projects__stage .sticky-projects__block-icon {
    width:  40px;
    height: 40px;
  }
  /* Comfortable measure: the base 46ch cap already holds, just make
     it explicit so tablet lines never run edge to edge. */
  .sticky-projects__stage .sticky-projects__block-desc { max-width: 46ch; }

  /* Chip rail: full-bleed to the 40px edges, same 16px fade masks.
     max-width: none lifts the base 440px cap (~line 423) — it was
     winning here and left ~300px of dead space on the right plus a
     mid-word truncation (review fix, Vicente 2026-07-30). */
  .sticky-projects__stage .sticky-projects__block-categories {
    max-width:    none;
    margin-left:  calc(-1 * var(--space-40));
    margin-right: calc(-1 * var(--space-40));
    padding:      0 var(--space-40);
  }
  /* The rail can still overflow at some widths (768px) — it is
     keyboard-focusable (tabindex set by sticky-projects.js) so arrow
     keys can scroll it. Same white ring as the site's dark-surface
     focus pattern (styles.css ~33-45). Review fix, Vicente 2026-07-30. */
  .sticky-projects__stage .sticky-projects__block-categories:focus-visible {
    outline:        2px solid var(--white);
    outline-offset: 3px;
  }

  /* Image band: taller than phone (46svh) — tablet has the vertical
     room; full-bleed to the 40px edges, same 20px bottom drop. */
  .sticky-projects__stage .sticky-projects__block-mobile-image {
    height:  50svh;
    margin:  auto calc(-1 * var(--space-40)) -20px;
    padding: var(--space-24);
  }

  /* ── Task 1: CTA row, tablet — "All projects" + "Ask me about this
     project" side by side, both fit-content, left-aligned (overrides
     the phone stage's 50/50 flex split, later in source). ── */
  .sticky-projects__stage .sticky-projects__cta-row {
    justify-content: flex-start;
    gap:             var(--space-12);
    padding:         12px var(--space-40); /* fixed vertical — no env() */
  }
  .sticky-projects__stage .sticky-projects__cta,
  .sticky-projects__stage .sticky-projects__cta-secondary {
    flex:          0 0 auto;
    width:         fit-content;
    padding-left:  var(--space-24);
    padding-right: var(--space-24);
  }
  /* Tablet has the width: restore the full "Ask me about this
     project" swap pair, retire the phone-only short label. */
  .sticky-projects__stage .sticky-projects__cta-secondary .sticky-projects__cta-swap { display: inline-block; }
  .sticky-projects__stage .sticky-projects__cta-secondary .sticky-projects__cta-label-short { display: none; }

  /* Reduced-motion fallback (no stage — CTAs stay in the intro): same
     Task-1 row, fit-content, left-aligned. Overrides the <=900 intro
     stack (~line 664) at equal specificity, later in source. */
  .sticky-projects__intro .sticky-projects__cta-row {
    flex-direction: row;
    align-items:    center;
    width:          fit-content;
  }
  .sticky-projects__intro .sticky-projects__cta,
  .sticky-projects__intro .sticky-projects__cta-secondary {
    width: fit-content;
  }

  /* ── Task 1: NO per-project tint on tablet — id-weighted override
     beats every [data-active-face="n"] rule (~line 280) in both the
     stage and the reduced-motion intro contexts. Phone treatment:
     gray-900 bg, gray-400 type (8.1:1, AA — ratio corrected from the
     earlier ~10.6:1 claim, Vicente 2026-07-30). ── */
  #stickyProjects[data-active-face] .sticky-projects__cta {
    background:   var(--gray-900);
    border-color: var(--gray-900);
    color:        var(--gray-400);
  }

  /* ── Delta face, tablet band integration (review fix, Vicente
     2026-07-30): the 4th mockup broke the band language — hard
     vertical edges and no scale, unlike Housing/Beloo (+25%) and
     Veloque (+20%). Modest +18% grown from the bottom edge (fills the
     dead zone between chips and image at 834) + lateral fade so the
     screenshot's edges dissolve into its ::before backdrop. Doubled
     class (0,3,1) outweighs the shared ≤900 img mask (~line 2590,
     0,2,1 but LATER in source — the modifier alone ties, it doesn't
     add), so BOTH fades live here: lateral × the same bottom
     dissolve, intersected. Lateral stops are PERCENTAGES — the
     contain-fit content sits inset ~10-15% inside the img box, so
     px-from-the-box-edge stops never reach its visible edges. ── */
  .sticky-projects__stage .sticky-projects__block-mobile-image.sticky-projects__block-mobile-image--delta img {
    transform:        scale(1.25);
    transform-origin: center bottom;
    -webkit-mask-image:
      linear-gradient(to right,
        transparent 9%, rgba(0,0,0,0.45) 17%, #000 30%,
        #000 70%, rgba(0,0,0,0.45) 83%, transparent 91%),
      linear-gradient(to bottom,
        #000 0, #000 calc(100% - 80px), rgba(0,0,0,0.72) calc(100% - 56px),
        rgba(0,0,0,0.40) calc(100% - 32px), rgba(0,0,0,0.15) calc(100% - 14px),
        transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image:
      linear-gradient(to right,
        transparent 9%, rgba(0,0,0,0.45) 17%, #000 30%,
        #000 70%, rgba(0,0,0,0.45) 83%, transparent 91%),
      linear-gradient(to bottom,
        #000 0, #000 calc(100% - 80px), rgba(0,0,0,0.72) calc(100% - 56px),
        rgba(0,0,0,0.40) calc(100% - 32px), rgba(0,0,0,0.15) calc(100% - 14px),
        transparent 100%);
    mask-composite: intersect;
  }

  /* ═══ Task 2 (experiment, Vicente 2026-07-30): typology chips →
     BENTO MOSAIC, tablet only. sticky-projects.js (tabletBento branch)
     wraps heading+desc+link into a new .sticky-projects__block-textcol
     and pairs it with the untouched .sticky-projects__block-categories
     inside a new .sticky-projects__block-row. Grid give the two
     columns equal height for free (align-items:stretch — the row
     track height is set by the taller column, textcol, and the chip
     column stretches to match it, no JS measuring needed). ═══ */
  .sticky-projects__stage .sticky-projects__block-row {
    display:             grid;
    grid-template-columns: 1fr 1fr;
    align-items:         stretch;
    gap:                 var(--space-16);
  }
  .sticky-projects__stage .sticky-projects__block-textcol {
    display:        flex;
    flex-direction: column;
    gap:            var(--space-8);
    min-width:      0;
  }
  /* Cancel the horizontal-rail treatment (nowrap scroll, full-bleed
     margins, fade masks — ~line 2257 & 2404): the chips now live
     inside the row's right column instead of a slider. */
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-categories {
    max-width:           none;
    margin:              0;
    padding:             0;
    height:              100%;
    display:             flex;
    flex-wrap:           wrap;
    align-content:       stretch;
    overflow:            visible;
    -webkit-mask-image:  none;
    mask-image:          none;
  }
  /* Hole-free mosaic: each chip grows to fill its row's leftover
     width, and align-content:stretch (above) makes every wrapped row
     share the column's height evenly — chip counts differ per project
     but the grid always packs solid, no gaps. */
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-categories > * {
    flex:            1 1 auto;
    justify-content: center;
    text-align:      center;
    white-space:     normal;
  }
  /* The rail's vertical divider after the role chip (~line 2286) reads
     as a lone hairline once chips wrap into tiles — drop it here. */
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-role-chip {
    margin: 0;
  }
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-role-chip::after {
    display: none;
  }
}


/* ═══ Phone type ramp pass (Vicente 2026-07-29) ═══ */
@media (max-width: 640px) {
  /* Project titles (Featured projects cards): 28 → 24px. */
  .sticky-projects__block-title { font-size: 24px; line-height: 30px; }
  /* My-root card headings: 32 (desktop size leaking) → 24px. */
  .vision-bento .vision-slide__heading { font-size: 24px; line-height: 30px; }
  /* ": my root {" lead ("Great experiences and products…") → 14px. */
  .vision-scroll__lead { font-size: 14px; line-height: 21px; }
}


/* ═══ Immersive mode (Vicente 2026-07-29) — PHONE + TABLET ═══
   body.sp-immersive is set by sticky-projects.js while the project
   steps are being consumed: slide the floating mobile navbar out the
   top so the pinned projects get the full viewport. Widened <=640 →
   <=900 (Vicente 2026-07-30 tablet port — the mobile-nav now serves
   the whole <=900 tier, see styles.css). */
@media (max-width: 900px) {
  .mobile-nav {
    /* visibility 0s (no delay) on THIS side → instant reappear on
       exit; the hide-side rule below delays it 0.35s so the fade-out
       still plays. Review fix WCAG 2.4.3/2.4.7, Vicente 2026-07-30. */
    transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease,
                visibility 0s;
  }
  body.sp-immersive .mobile-nav {
    transform:      translateY(-88px);  /* top 16 + bar 56 + margin */
    opacity:        0;
    /* Out of the tab order + a11y tree while hidden (opacity alone
       left it focusable). Delayed so the 0.35s fade-out completes. */
    visibility:     hidden;
    transition:     transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease,
                    visibility 0s linear 0.35s;
    pointer-events: none;
  }
}


/* ═══ Extended image backdrops (Vicente 2026-07-29) — PHONE ONLY ═══
   The per-project background band stretches UP to just under the
   "Discover the project" link, fading in at its top (same eased fade
   language as the desktop card's bottom mask). One seamless surface:
   the container's own bg is nulled and a single ::before paints the
   WHOLE extended area (no gradient seams), behind the image.
   Widened <=640 → <=900 (Vicente 2026-07-30): the tablet stage uses
   the same band language; every rule is stage-scoped so the
   reduced-motion static layout is untouched. */
@media (max-width: 900px) {
  .sticky-projects__stage .sticky-projects__block-mobile-image {
    position:   relative;
    background: none;           /* modifier bgs move to the ::before */
    overflow:   visible;
    /* The global bottom-fade mask (~line 1752) clips element+children
       to the container box — it decapitated the ::before extension
       (Playwright bisect 2026-07-29). Neutralized here; the bottom
       fade lives on the img and the ::before instead. */
    -webkit-mask-image: none;
    mask-image:         none;
  }
  /* Photo keeps its bottom dissolve. */
  .sticky-projects__stage .sticky-projects__block-mobile-image img {
    -webkit-mask-image: linear-gradient(to bottom,
      #000 0, #000 calc(100% - 80px), rgba(0,0,0,0.72) calc(100% - 56px),
      rgba(0,0,0,0.40) calc(100% - 32px), rgba(0,0,0,0.15) calc(100% - 14px),
      transparent 100%);
    mask-image: linear-gradient(to bottom,
      #000 0, #000 calc(100% - 80px), rgba(0,0,0,0.72) calc(100% - 56px),
      rgba(0,0,0,0.40) calc(100% - 32px), rgba(0,0,0,0.15) calc(100% - 14px),
      transparent 100%);
  }
  .sticky-projects__stage .sticky-projects__block-mobile-image::before {
    content:  "";
    position: absolute;
    left:     0;
    right:    0;
    bottom:   0;
    top:      -152px;           /* reaches ~8px below the Discover link */
    z-index:  -1;               /* behind the mockup image */
    /* Fade IN at the top + the same eased fade OUT at the bottom the
       card language already uses. */
    -webkit-mask-image: linear-gradient(to bottom,
      transparent 0, transparent 40px, rgba(0,0,0,0.15) 72px,
      rgba(0,0,0,0.40) 104px, rgba(0,0,0,0.72) 136px, #000 176px,
      #000 calc(100% - 80px), rgba(0,0,0,0.72) calc(100% - 56px),
      rgba(0,0,0,0.40) calc(100% - 32px), rgba(0,0,0,0.15) calc(100% - 14px),
      transparent 100%);
    mask-image: linear-gradient(to bottom,
      transparent 0, transparent 40px, rgba(0,0,0,0.15) 72px,
      rgba(0,0,0,0.40) 104px, rgba(0,0,0,0.72) 136px, #000 176px,
      #000 calc(100% - 80px), rgba(0,0,0,0.72) calc(100% - 56px),
      rgba(0,0,0,0.40) calc(100% - 32px), rgba(0,0,0,0.15) calc(100% - 14px),
      transparent 100%);
  }
  /* Per-project surfaces (mirrors the <=900 static modifiers). */
  /* Housing Help (Vicente 2026-07-29): bluish base with greenish
     patches — closer to the page navy AND the mockup's green UI, so
     the band's color jump softens. */
  .sticky-projects__stage .sticky-projects__block-mobile-image--1::before {
    background:
      radial-gradient(ellipse at 62% 40%, rgba(56, 110, 88, 0.50), transparent 62%),
      radial-gradient(ellipse at 22% 78%, rgba(46, 122, 96, 0.32), transparent 55%),
      linear-gradient(160deg, #0E1630 0%, #13223D 45%, #0B1326 100%);
  }
  /* (Vicente 2026-07-29: images must NOT take the top fade — only the
     ::before backdrops do. The --1 img top-fade rule was removed; all
     imgs fall back to the shared bottom-only dissolve above.) */
  .sticky-projects__stage .sticky-projects__block-mobile-image--veloque::before { background: var(--bg-main); }
  /* Veloque mockup +20% (Vicente 2026-07-29): scaled from the bottom
     edge so it grows upward into the extended backdrop and keeps its
     seat above the CTA bar. */
  /* Housing Help +25% (Vicente 2026-07-29). */
  .sticky-projects__stage .sticky-projects__block-mobile-image--1 img {
    transform:        scale(1.25);
    transform-origin: center bottom;
  }
  /* Beloo +25%, nudged left (Vicente 2026-07-29). */
  .sticky-projects__stage .sticky-projects__block-mobile-image--beloo img {
    transform:        scale(1.25) translateX(-16px);
    transform-origin: center bottom;
  }
  .sticky-projects__stage .sticky-projects__block-mobile-image--veloque img {
    transform:        scale(1.2);
    transform-origin: center bottom;
  }
  .sticky-projects__stage .sticky-projects__block-mobile-image--beloo::before {
    background: radial-gradient(ellipse at 65% 62%,
                  #fe913c 0%, #fe6b36 35%, #ff4630 65%, #ff282b 90%);
  }
  .sticky-projects__stage .sticky-projects__block-mobile-image--delta::before {
    background:
      radial-gradient(ellipse at 72% 58%, rgba(143, 0, 216, 0.20), transparent 60%),
      linear-gradient(135deg, #0B1020 0%, #0E1430 55%, #080C1D 100%);
  }
}


/* ═══ Justified paragraphs (Vicente 2026-07-29) — PHONE ONLY ═══
   Project descriptions + the section lead go from ragged-right to
   justified. `text-wrap: initial` lifts the pretty/balance wrapping
   (they fight justification). */
@media (max-width: 640px) {
  .sticky-projects__stage .sticky-projects__block-desc,
  .sticky-projects__intro .sticky-projects__lead {
    text-align: justify;
    text-wrap:  initial;
  }
  /* Tighter leading on the project descs to win height (Vicente 2026-07-29). */
  .sticky-projects__stage .sticky-projects__block-desc { line-height: 1.35; }
}


/* ═══ Tagline shape lock (Vicente 2026-07-29) — PHONE ONLY ═══
   The 4-line wrap (ref screenshot 5.18pm) must be IDENTICAL on every
   phone width. Font scales with the AVAILABLE width (100vw - 40px of
   side padding), calibrated at 390px/14px → same chars per line, same
   breaks, from SE (≥345px) to Pro Max. Line-height rides along
   (22/14 = 1.571). Clamps keep degenerate sizes out. */
@media (max-width: 640px) {
  .hero__tagline {
    font-size:   clamp(11px, calc((100vw - 40px) * 14 / 350), 17px);
    line-height: 1.571;
  }
  /* Constant hero height (Vicente 2026-07-29): home.css:943 uses
     100dvh — the hero breathed with the browser bar. svh = smallest
     viewport, fixed; the slack shows below the fold instead. */
  .hero { min-height: 100svh; }

  /* (0,2,0) — beats home.css:1756 `.hero .hero__tagline { 16px }`. */
  .hero .hero__tagline { margin-top: 0; }  /* gap h1→tagline = 0 (Vicente 2026-07-29) */
  /* Text block (h1 + tagline + CTAs) 24px up (Vicente 2026-07-29).
     -48 real: the flex centering splits the shift in half. */
  /* SUPERSEDED 2026-07-30 by the identity-chips block at the END of
     this file: the eyebrow it compensated for no longer exists. */
  .hero__body { margin-top: -160px; }
}


/* ═══ Section titles +8px & header row (Vicente 2026-07-29) — PHONE ═══ */
@media (max-width: 640px) {
  /* "Featured projects": 28 → 36px, forced to TWO lines (min-content),
     with the "A selection…" lead to its RIGHT — one row. */
  .sticky-projects__header {
    display:        flex;
    flex-direction: column;  /* row → column (Vicente 2026-07-30) */
    align-items:    stretch;
    gap:            var(--space-16);
  }
  .sticky-projects__title {
    font-size:   36px;
    line-height: 40px;  /* one line again (Vicente 2026-07-30) */
    flex:        0 0 auto;
  }
  .sticky-projects__intro { flex: 1 1 auto; min-width: 0; }
  /* ": my root {" 28 → 36px. */
  .vision-scroll__title { font-size: 36px; line-height: 40px; }

  /* Body-paragraph leading pass (match the project short descs, 1.35):
     home sections' class-styled bodies — the site-wide inheritance rule
     lives in styles.css. */
  .sticky-projects__intro .sticky-projects__lead,
  .vision-scroll__lead,
  .vision-bento .vision-slide__body,
  .about__bio,
  .discover__lead,
  .hero__tagline { line-height: 1.35; }
}


/* ═══ Who I am facts cards, phone (Vicente 2026-07-29) ═══
   No square dots; the ES/US flags render INLINE right after each
   country name (the leading flags block is desktop's treatment). */
/* Inline flags at EVERY tier (Vicente 2026-07-29) — the leading
   flags block was removed from the markup; the row now carries a
   standard dot like its siblings. */
.bullet-list__flag-inline {
  display:        inline-block;
  vertical-align: -1px;
  margin-left:    6px;
}


/* Who I am facts cards (Vicente 2026-07-29): bullet copy switches to
   Geist like every other body — the card LABELS keep their mono +
   purple voice (rule above, .about .about__group-label). */
.about .bullet-list__item {
  font-family: var(--font-text);
  font-weight: 400;
}


/* Who I am bullet dots (Vicente 2026-07-29): −40% size (12 → 7px) and
   10% opacity, every tier — phone included (its removal was reverted).
   margin-top re-centers the smaller square on the 24px line. */
.bullet-list__dot {
  width:      7px;
  height:     7px;
  margin-top: 8px;
  background: rgba(250, 250, 250, 0.10);
}


/* Who I am bio (Vicente 2026-07-29): light gray like every other body
   (same --gray-900 used by the vision/bento copy — ~11:1 on this bg). */
.about-b__columns .about__bio { color: var(--gray-900); }


/* Who I am phone: title → bio gap 32 → 16px (Vicente 2026-07-29).
   The flex column gap stays 32 for the rest (bio→facts→widget); the
   bio just pulls itself 16px closer to the heading. */
@media (max-width: 640px) {
  .about-b__columns .about__bio { margin-top: -32px; }  /* col gap 32 → 0 */
}
@media (min-width: 641px) and (max-width: 940px) {
  .about-b__columns .about__bio { margin-top: -48px; }  /* col gap 48 → 0 */
}
/* 641-900 overrides the -48px cancel above with its own value (grid
   row-gap var(--space-40) instead of the flex gap 48px) — see the
   "Who I am — tablet two-column layout" block near the end of this
   file, which redefines .about-b__columns as a 2-col grid at this
   tier only. 901-940 keeps the -48px rule untouched. */


/* Featured projects header: title ↔ "A selection…" gap = 16px on
   phone (row, was already 16) AND tablet (column, was 24) — Vicente
   2026-07-29, mirrors the Who I am title→bio treatment. */
@media (min-width: 641px) and (max-width: 900px) {
  .sticky-projects__header { gap: 16px; }
}


/* ═══ Hero arrow scroll-cue animation (Vicente 2026-07-29) ═══
   Mirrors the Who-I-am desktop cue (hero-cue-run): the shaft dims to
   a track and a bright segment travels down it on a 2.2s loop with
   soft fades — same easing, same rhythm. */
.hero__down-arrow { color: rgba(250, 250, 250, 0.45); } /* track (was .80) */
.hero__down-arrow-run {
  animation: hero-arrow-run 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Overshoot fix (Vicente 2026-07-29): segment final position changed from 78px to 60px — prevents the bright segment from overshooting past the arrow tip. Fade completes before segment reaches y=78. */
@keyframes hero-arrow-run {
  0%   { transform: translateY(-14px); opacity: 0; }
  25%  { opacity: 1; }
  75%  { opacity: 1; }
  100% { transform: translateY(60px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__down-arrow-run { animation: none; opacity: 0; }
  .hero__down-arrow { color: rgba(250, 250, 250, 0.80); } /* full-strength static arrow */
}

/* Who I am facts — bullet list gap (Vicente 2026-07-29): reduce from
   8px (base home.css) to 4px for tighter vertical spacing. */
.about .bullet-list { gap: 4px; }


/* Project card phone: title → short-desc gap RESTORED to the block's
   tokenized rhythm (Vicente 2026-07-30 — the 0px experiment crowded
   the clamped desc). The gap is the block's own flex gap,
   var(--space-8); no per-element override needed. */


/* My-root tiles, phone (Vicente 2026-07-29): tighter heading→body gap
   (tile flex gap 12 stays for the rest); business tile desc→chips 32px. */
@media (max-width: 640px) {
  .vision-bento .vision-slide__heading + .vision-slide__body { margin-top: -8px; }  /* 12 → 4 */
  .vision-bento__tile--business .vision-tags-row { margin-top: 44px; }  /* medido: offset heredado -24 → 44 da 32px reales */
}


/* ═══ Featured-projects header pixel backdrop (Vicente 2026-07-30) ═══
   Phone + tablet: the pixel rows move BEHIND the title+lead as a
   highlight backdrop (stretched to the header's box, full-bleed to
   the viewport sides) and the classic divider strip at Who I am's
   floor is hidden. Desktop keeps the original separator untouched. */
.pixel-fringe--projects-head { display: none; }
@media (max-width: 900px) {
  .about .pixel-fringe--separator { display: none; }  /* no divider role here */
  .sticky-projects__header { position: relative; }
  .sticky-projects__header > :not(.pixel-fringe--projects-head) {
    position: relative;
    z-index:  1;
  }
  .pixel-fringe--projects-head {
    display:        flex;
    flex-direction: column;
    position:       absolute;
    top:            -16px;
    bottom:         -16px;
    left:           -40px;   /* section side padding (tablet) */
    right:          -40px;
    z-index:        0;
    width:          auto;  /* kills the base width:100% — over-constrained
                              abspos was ignoring right:-20 (40px gap) */
    pointer-events: none;
  }
  .pixel-fringe--projects-head .pixel-row {
    display: flex;
    flex:    1 1 0;
    height:  auto;    /* stretch: rows share the header's height */
  }
  .pixel-fringe--projects-head .bg-pixel { flex: 1 1 0; height: 100%; }
}
@media (max-width: 640px) {
  .pixel-fringe--projects-head { left: -20px; right: -20px; }  /* phone padding */
}


/* ═══ Who I am — tablet two-column layout, 641-900 ONLY (Vicente 2026-07-30) ═══
   Below 941px .about-b__columns is a single flex column (rule ~1470,
   still governs 901-940 untouched). At 641-900 specifically we carve
   out a real 2-column grid instead: bio+facts stack on the LEFT
   (2fr) and the AI widget sticks on the RIGHT (1fr), reusing the same
   .about-b__columns/.about__heading/.about__bio/.about-b__col-right/
   .ai-widget elements — no new markup, no new component (compose-first).
   Source order in this file is what wins the cascade (same-specificity
   selectors as the ≤940 block above), so this must stay AFTER it. */
@media (min-width: 641px) and (max-width: 900px) {
  /* `overflow: hidden` on .about (home.css) still establishes a
     scroll-container per spec even though .about never actually
     scrolls — that traps position:sticky on any descendant (the
     widget below) and makes it move in lockstep with the page
     instead of sticking (verified via Playwright bisect, 2026-07-30).
     `clip` reads identically (nothing here relies on programmatic
     scrollTop) but does NOT establish that scroll container, freeing
     the widget's sticky to resolve against the real page scroller. */
  .about { overflow: clip; }

  .about-b__columns {
    display:               grid;
    /* minmax floor added (Vicente 2026-07-30, review fix): at 768px the
       plain 1fr resolved to ~203px — narrow enough that the widget's
       caption ("Answers here. After two…") wrapped to 2 lines and the
       bar (min-height 104px, see override below) read as a squarish
       purple slab. 230px keeps the caption on one line at this tier's
       narrowest widths; bio+facts (2fr) still hold comfortably down to
       641px (verified via Playwright). */
    grid-template-columns: 2fr minmax(230px, 1fr); /* bio+facts : widget */
    grid-template-rows:    auto auto auto;
    column-gap:            var(--space-40); /* matches this tier's section inset */
    row-gap:               var(--space-40);
  }

  /* Left column stack: heading → bio → facts, top to bottom. */
  .about-b__columns .about__heading  { grid-area: 1 / 1 / 2 / 2; }
  .about-b__columns .about__bio      { grid-area: 2 / 1 / 3 / 2; }
  .about-b__col-right                { grid-area: 3 / 1 / 4 / 2; }

  /* heading→bio gap cancelled to 0 (same treatment as the ≤940/≤640
     rules above, re-based on THIS tier's row-gap token instead of the
     old 48px flex gap — supersedes the -48px rule a few lines up). */
  .about-b__columns .about__bio { margin-top: calc(-1 * var(--space-40)); }

  /* Right column: AI widget spans all 3 rows and stays put while the
     section scrolls — sticky, offset below the phone-style mobile-nav
     top bar (fixed top:16px + 56px bar height, see styles.css ~1116-
     1131) plus one more space-16 of breathing room. 16+56+16 = 88px,
     which not by accident matches .main's own 88px padding-top for
     non-hero pages at this same ≤900 tier (styles.css ~1112). */
  .about-b__columns .ai-widget {
    grid-area:  1 / 2 / 4 / 3;
    align-self: start;   /* was `auto` (≤940 rule) — needed for sticky to travel */
    margin-top: 0;
    width:      100%;
    position:   sticky;
    top:        calc(var(--space-16) + var(--space-56) + var(--space-16));
  }

  /* Bar min-height gated down from the ungated desktop value (104px,
     rule ~1503 above) — that height was tuned for the wide desktop
     widget and, combined with the narrow column at this tier, made the
     bar read as a squarish slab. Re-based on the phone value (~1513)
     since this tier's widget column is closer in width to phone than
     to desktop. */
  .about-b__columns .ai-widget__bar {
    min-height: 52px;
    padding:    6px 6px 6px 16px;
  }

  .about-b__columns .ai-widget__input {
    min-height: 36px;
    padding:    8px 8px 8px 0;
  }
}


/* ═══ Who I am → Featured projects gap, tablet 641-900 (Vicente 2026-07-30) ═══
   Task 3: three separate paddings stack up into the visual gap between
   the sections (.about bottom + .about__grid bottom + .sticky-projects
   top). Measured empirically via Playwright (stage bottom → title top):
   104px stock → 48px here, ≈46% ("roughly half"), all three unified to
   the same var(--space-40) token for a tidy, consistent rhythm. Only
   the bottom/top sides touch; left/right insets and the
   .sticky-projects__stage CTA/stage rules (~2366-2454) are untouched. */
@media (min-width: 641px) and (max-width: 900px) {
  .about                { padding-bottom: var(--space-40); }  /* 120 → 40 */
  .about__grid           { padding-bottom: var(--space-40); }  /* 96 → 40 */
  .sticky-projects       { padding-top:    var(--space-40); }  /* 80 → 40 */
}


/* ═══ my-root tile labels read as CSS custom properties (Vicente 2026-07-30) ═══
   "01 -My Values" → "--01-my-values:". The text node in index.html is
   already the kebab-case, lowercase middle part; only the `--` prefix
   and the trailing `:` are added here, as pseudo-elements. Why this
   split:
     · it is per-ELEMENT, not per-string, so the phone renumbering keeps
       working untouched — the dual .vision-slide__label--stack /
       --lateral spans each carry their own number and each gets its own
       `--` and `:` (see the lateral block below);
     · screen readers announce the token, not a run of punctuation;
     · text-transform: lowercase is a belt-and-braces guard so a future
       "05 -My Flow" style edit still renders as a token.
   Scoped to .vision-scroll so no other section's labels are touched.
   Colour/typography stay on .vision-slide__label (vision.css ~157):
   mono, 16/24, --purple-700. The pseudo-elements inherit all of it, so
   the `--` and `:` are the same purple as the number. */
.vision-scroll .vision-slide__label {
  text-transform: lowercase;
}
.vision-scroll .vision-slide__label::before { content: "--"; }
.vision-scroll .vision-slide__label::after  { content: ":";  }


/* ═══ Phone lateral my-root (Vicente 2026-07-30) ═══
   PHONE ≤640 + motion-ok ONLY: the ": my root {" section becomes a
   sequence of equal-height horizontal slides driven by vertical
   scroll — DOM is built at runtime by vision-lateral.js (house
   sticky-stage pattern, mirrors sticky-projects.js):
     section.vision-lateral-on
       .vlat (tall scroll driver)
         .vlat__stage (sticky, 100svh constant — NEVER dvh/env():
                       browser-bar changes made env() bounce)
           #visionTrack.vision-bento = the flex rail (keeps every
                       `.vision-bento …` descendant selector alive)
             .vlat__slide × 6 (title · 01+02 merged · Agentic ·
                       Background · Flow · "...}")
   Addendum (Vicente 2026-07-30): the title slide is TOP-anchored and
   narrower than the viewport so the next card peeks in from the right;
   Values + Business share ONE slide stacked in a column, which shifts
   the phone numbering (Business → 02, Agentic → 03) via the dual label
   spans below. Every slide is exactly the same height.
   Reduced-motion / >640: .vision-lateral-on is never added — none of
   these rules apply, the vertical stack renders untouched.
   Every rule is scoped under .vision-lateral-on inside the ≤640
   query: zero effect on desktop/tablet or other agents' bands. */

/* Dual numbering labels (markup: index.html ~750 / ~913). The lateral
   variant is hidden EVERYWHERE by default — desktop, tablet and also
   reduced-motion phone, where the vertical stack keeps DOM order and
   therefore the original 02-Agentic / 03-Business numbering. Only
   .vision-lateral-on swaps them (rule inside the ≤640 query below).
   display:none keeps the hidden copy out of the a11y tree too, so the
   visible text is never announced twice. */
.vision-slide__label--lateral { display: none; }

@media (max-width: 640px) {
  .vision-scroll.vision-lateral-on { padding: 0; }

  /* Slide geometry, single source of truth. The nav block matches
     styles.css ~1116 (.mobile-nav top 16 + 56 bar) + 16 breathing —
     the same 88px .main uses — so every card is exactly
     100svh − (88 + 16) tall and the rail never sits under the navbar. */
  .vision-lateral-on {
    --vlat-pad-top:    calc(var(--space-16) + var(--space-56) + var(--space-16));
    --vlat-pad-bottom: var(--space-16);
    --vlat-peek:       var(--space-56);  /* of the next card, on slide 1 */
  }

  .vision-lateral-on .vlat        { position: relative; }
  .vision-lateral-on .vlat__stage {
    position: sticky;
    top:      0;
    height:   100svh;   /* constant geometry — see block comment */
    overflow: hidden;
  }

  /* The track becomes the rail: (0,2,0) beats .vision-bento's grid. */
  .vision-lateral-on .vision-bento {
    display:     flex;
    gap:         0;
    height:      100%;
    width:       max-content;
    will-change: transform;
  }

  /* Equal-height slides: every slide is exactly the stage box. */
  .vision-lateral-on .vlat__slide {
    flex:            0 0 100vw;
    width:           100vw;
    height:          100%;
    box-sizing:      border-box;
    padding:         var(--vlat-pad-top) var(--space-16) var(--vlat-pad-bottom);
    display:         flex;
    flex-direction:  column;
    justify-content: flex-start;
    min-width:       0;
    overflow:        hidden;
  }

  /* Cards fill the whole slide; their CONTENT is TOP-aligned
     (Vicente 2026-07-30). Optical centring inside a 100svh box left
     150-250px of dead space above the label on the shorter cards (04,
     05) and every slide started its content at a different Y. Padding
     is unified here too, so the label baseline is literally the same
     number on all of them: slide pad-top (88) + 12. */
  .vision-lateral-on .vlat__slide > .vision-bento__tile {
    width:           100%;
    height:          100%;
    min-height:      0;
    justify-content: flex-start;
    padding:         var(--space-12) var(--space-16);
  }
  /* The title slide's header is not a tile — same 12px so the h2 sits
     on that shared baseline as well. */
  .vision-lateral-on .vlat__slide--title .vision-scroll__header {
    padding-top: var(--space-12);
  }

  /* ── Slide 1 — title: anchored TOP, narrower than the viewport so
     the next card peeks from the right (scroll invitation). The rail
     is positioned from each slide's own offsetLeft in
     vision-lateral.js, so a narrower first slide does NOT desync the
     ones after it. ── */
  .vision-lateral-on .vlat__slide--title {
    flex-basis: calc(100vw - var(--vlat-peek));
    width:      calc(100vw - var(--vlat-peek));
  }
  .vision-lateral-on .vision-scroll__header { margin-bottom: 0; }
  /* The ≤900 block (~849) switches the lead to text-align:left but leaves
     `text-align-last: right` from the desktop justified treatment in place;
     on a narrow top-anchored slide that stray right-closed last line reads
     like a bug. Reset it here only — the ≤900 tablet band is another
     workstream's and is left exactly as it was. */
  .vision-lateral-on .vision-scroll__lead { text-align-last: auto; }

  /* ── Slide 2 — merged "01 My Values" + "02 Business mind", stacked
     in one column. Both cards split the slide height evenly; type is
     compacted so the pair fits without scroll. ── */
  .vision-lateral-on .vlat__slide--merged { gap: var(--space-12); }
  /* The PAIR must total exactly one slide, so every slide reads as the
     same block (Vicente 2026-07-30). flex-basis auto + grow keeps each
     card's own content height as its share and splits only the LEFTOVER
     room between them — the earlier `flex: 0 0 auto` ended wherever the
     content ended (pair ~200px short of a slide), and a flat `1 1 0`
     even split pushed 02 down to the slide's 50% mark and left a void
     mid-slide. Content stays top-aligned inside each card
     (justify-content: flex-start), so the shared label baseline set by
     the .vlat__slide > tile padding above is unchanged. */
  .vision-lateral-on .vlat__slide--merged > .vision-bento__tile {
    height:          auto;
    flex:            1 1 auto;
    min-height:      0;
    justify-content: flex-start;  /* shared top baseline (2026-07-30) */
  }
  /* padding: inherited from the unified .vlat__slide > tile rule above. */
  /* Type relaxed back towards the other slides' scale (Vicente
     2026-07-30): the 20/13/14px compaction existed because both cards
     split the slide height 50/50; hugging their content freed ~250px,
     so the pair can read at the same 24px heading / 14px body rhythm
     as slides 03-05 and still leave slack at the bottom. */
  .vision-lateral-on .vision-bento .vlat__slide--merged .vision-slide__heading {
    font-size:   24px;
    line-height: 30px;
  }
  .vision-lateral-on .vision-bento .vlat__slide--merged .vision-slide__body {
    font-size:   14px;
    line-height: 1.4;
  }
  /* The 44px desc→chips offset of the vertical layout (~2924) is a
     whole card row in half a slide. */
  .vision-lateral-on .vlat__slide--merged .vision-bento__tile--business .vision-tags-row {
    margin-top: var(--space-12);
    gap:        var(--space-4);
  }
  .vision-lateral-on .vlat__slide--merged .vision-pill {
    height:    26px;
    font-size: 11px;
    padding:   0 var(--space-8);
  }

  /* Phone numbering follows the merge: 01 Values · 02 Business ·
     03 Agentic. Swap of the dual label spans (see note above). */
  .vision-lateral-on .vision-slide__label--stack   { display: none; }
  .vision-lateral-on .vision-slide__label--lateral { display: inline; }

  /* ── Last slide — closing "...}": BOTTOM-LEFT of the slide (Vicente
     2026-07-30), matching the desktop treatment. Its left edge lands on
     the same x as every card's left edge: the slide's own
     var(--space-16) inset is the card box edge, so the close needs no
     extra padding of its own. justify-content: flex-end pushes it to
     the bottom of the column; the slide's var(--vlat-pad-bottom) is the
     only gap under it. ── */
  .vision-lateral-on .vlat__slide--close {
    justify-content: flex-end;
    align-items:     flex-start;
  }
  .vision-lateral-on .vision-scroll__close {
    margin:       0;
    text-align:   left;
    font-size:    36px;
    line-height:  40px;
  }

  /* ── Slide 3 — 03 Agentic systems: the one WIDE slide (Vicente
     2026-07-30). It carries the terminal AND the agents graph, which
     on a 390px slide collapsed to 141px-wide columns (terminal type at
     10px, graph 59px tall — neither readable). The rail is a
     horizontal scroller, so this slide is allowed to exceed the
     viewport: it still lands flush-left like the others and its
     overhang is revealed as the scroll continues — vision-lateral.js
     maps scroll onto rail DISTANCE, so the extra width automatically
     buys proportionally more scroll instead of speeding the slide up.
     Internal layout mirrors the desktop tile: text | terminal in row 1,
     agents graph full-width in row 2, accent line in row 3. Overrides
     the ≤940 flex-column rule (home-b.css ~932) by later source order +
     higher specificity. ── */
  .vision-lateral-on { --vlat-wide-extra: 170px; }
  .vision-lateral-on .vlat__slide--agentic {
    flex-basis: calc(100vw + var(--vlat-wide-extra));
    width:      calc(100vw + var(--vlat-wide-extra));
  }

  .vision-lateral-on .vision-ai__top {
    display:               grid;
    grid-template-columns: minmax(0, 40fr) minmax(0, 60fr);
    grid-template-rows:    auto auto auto;
    /* row-gap 0: the agents graph must TOUCH the terminal's bottom edge
       so the fan edges read as springing from it (desktop behaviour). */
    gap:                   0 var(--space-16);
    align-items:           stretch;
  }
  .vision-lateral-on .vision-ai__text {
    grid-column: 1;
    grid-row:    1;
    gap:         var(--space-8);
  }
  /* height:auto → the terminal stretches to the text block's height,
     exactly like desktop (beats the ≤940 260px rule: 2 classes + later). */
  .vision-lateral-on .vision-ai__top > .vision-claude {
    grid-column: 2;
    grid-row:    1;
    height:      auto;
  }
  .vision-lateral-on .vision-ai__top > .vision-agents {
    grid-column: 1 / -1;
    grid-row:    2;
    margin-top:  0;
    width:       100%;
    height:      auto;   /* width-driven: 400×168 viewBox scales up with the slide */
  }
  .vision-lateral-on .vision-bento .vision-ai__top > .vision-slide__body--accent {
    grid-column: 1 / -1;
    grid-row:    3;
    white-space: normal;
    font-size:   12px;
    line-height: 1.4;
    margin-top:  var(--space-12);
  }
  .vision-lateral-on .vision-bento .vision-ai__top .vision-slide__heading {
    font-size:   22px;
    line-height: 28px;
  }
  .vision-lateral-on .vision-ai__top .vision-slide__body {
    font-size:   13px;
    line-height: 1.4;
  }
  /* Terminal type back to a legible size now that its column is ~2×
     wider than on the old viewport-width slide. */
  .vision-lateral-on .vision-ai__top > .vision-claude .vision-claude__body {
    min-height:  0;
    font-size:   12px;
    line-height: 18px;
  }
  .vision-lateral-on .vision-ai__top > .vision-claude .vision-term__lines {
    inset: 14px 14px 16px;   /* tighter than the desktop 20/18/24 */
  }
  .vision-lateral-on .vision-claude__chrome { padding: 8px 12px; }

  /* ── Slides 4/5 — the graphic takes the free space UNDER the text
     (Vicente 2026-07-30): with content top-aligned all the leftover
     room is at the bottom, so the frame fills it and the artwork is
     centred inside at a much larger scale. Scales are capped by the
     card interior width (390 − 32 slide − 32 tile = 326px), not by the
     available height: 04's wrap is authored 580×520 → 0.55 ≈ 319×286;
     05's chart 399×340 → 0.80 ≈ 319×272. (0,4,0) beats vision.css's
     ≤640 rules (~786). ── */
  .vision-lateral-on .vlat__slide > .vision-bento__tile--background .vision-bento__graphic-frame,
  .vision-lateral-on .vlat__slide > .vision-bento__tile--flow       .vision-bento__graphic-frame {
    flex:       1 1 auto;
    height:     auto;
    min-height: 0;
    margin-top: 0;
  }
  /* The circle's floating tags stick ~10px outside the 580×520 wrap
     box, so at 0.55 the artwork measures 340px against a 326px card
     interior and "Graphic Design" clipped. The frame (and only the
     frame) bleeds out over the card's 16px side padding → 358px of
     room, 9px clear on each side. */
  .vision-lateral-on .vlat__slide > .vision-bento__tile--background .vision-bento__graphic-frame,
  .vision-lateral-on .vlat__slide > .vision-bento__tile--flow       .vision-bento__graphic-frame {
    margin-inline: calc(-1 * var(--space-16));
  }
  .vision-lateral-on .vision-bento__tile--background .vision-slide__circle-wrap { transform: scale(0.55); }
  /* 0.75, not 0.8: the chart's "Our future together" pill sits ~23px
     right of the .vision-flow box, which at 0.8 landed exactly on the
     frame edge. */
  .vision-lateral-on .vision-bento__tile--flow       .vision-flow               { transform: scale(0.75); }

  /* Heading→body tightening from the vertical layout reads cramped
     inside full-height slides — restore the tile's own 12px rhythm.
     (0,3,0) + later source beats the -8px rule at ~2837. */
  .vision-lateral-on .vision-bento .vision-slide__heading + .vision-slide__body {
    margin-top: 0;
  }
}


/* ═══ Tablet 641-900 — cards 02 / 04 / 05 refinements (Vicente 2026-07-30) ═══
   Appended last on purpose: the ≤940 rules earlier in this file (and in
   vision.css) re-stack 04/05 into a column and zero the 02 sub-gap; these
   tablet-scoped rules restore a desktop-like composition at this tier
   only. Phone (≤640) and desktop (>900) are untouched. */
@media (min-width: 641px) and (max-width: 900px) {

  /* ── 04 background · 05 flow: text LEFT, graphic RIGHT ──────────
     Both tiles span the full bento width at this tier (~688px @768,
     ~754px @834), which is enough for a side-by-side split. Text is
     top-aligned (its label reads as the tile header), the graphic
     centres itself against it. gap = var(--space-24), one step below
     the desktop var(--space-40) to buy the graphic column width. */
  .vision-bento__tile--background,
  .vision-bento__tile--flow {
    flex-direction: row;
    align-items:    flex-start;
    gap:            var(--space-24);
  }
  .vision-bento__tile--background .vision-bento__text,
  .vision-bento__tile--flow       .vision-bento__text {
    flex:      1 1 0;
    min-width: 0;
  }
  .vision-bento__tile--background .vision-bento__graphic-frame,
  .vision-bento__tile--flow       .vision-bento__graphic-frame {
    flex:       0 0 48%;
    width:      auto;
    margin-top: 0;
    align-self: center;
  }

  /* 04 — scale is set by the TAG cluster, not the 580×520 wrap: the
     absolutely-positioned tags stick out past the wrap on both sides
     (636×494 union at scale 1). At 0.46 that's 293×227, which clears
     the ~315px (@768) / ~347px (@834) graphic column and the 260px
     frame height with margin to spare — 0.5 (the ≤940 value) clipped
     "Graphic Design" and "Industrial Design" at 768. */
  .vision-bento__tile--background .vision-bento__graphic-frame { height: 260px; }
  .vision-bento__tile--background .vision-slide__circle-wrap   { transform: scale(0.46); }

  /* 05 — same trick as desktop: instead of transform-scaling the
     399×340 box (which overflowed the frame and clipped the "We in
     the future" pill), the chart is pinned inside the frame and the
     svg stretches into it (preserveAspectRatio="none" +
     non-scaling-stroke in the markup). The insets are exactly the
     labels' negative offsets plus margin, so nothing is clipped:
     top 48 > the end pill's -42, right 30 > its right:-24, left 8 >
     the start pill's -4. Square markers are percent-anchored, so
     they track the stretched curve automatically. */
  .vision-bento__tile--flow .vision-bento__graphic-frame { height: 236px; }
  .vision-bento__tile--flow .vision-flow {
    position:  absolute;
    left:      8px;
    right:     30px;
    top:       48px;
    bottom:    8px;
    width:     auto;
    height:    auto;
    transform: none;
  }

  /* ── 02 Agentic systems: the tile's sub-grid collapses to a flex
     column at ≤940 with no gap, so the terminal butted straight into
     the copy. One var(--space-24) step of separation — applied as a
     margin on the terminal, NOT as a flex gap, so the agents svg
     keeps its own (smaller) var(--space-12) offset and the group
     hierarchy stays: text → terminal (24) > terminal → agents (12),
     which belong together visually (the fan edges point at it). */
  .vision-ai__top > .vision-claude { margin-top: var(--space-24); }
}

/* ═══ Phone (≤640px): single-line headings for tiles 04 & 05
   (Vicente 2026-07-30)
   Task: hide manual <br> tags and adjust font-size to ensure
   "A global UX designer." and "Strategy first, design follows."
   each render on exactly ONE line. ═══ */
@media (max-width: 640px) {
  .vision-bento__tile--background .vision-slide__heading br,
  .vision-bento__tile--flow .vision-slide__heading br {
    display: none;
  }
  
  /* Reduce heading size to fit on one line. Start at 24px, which leaves
     room for the text to wrap naturally if zoom or language forces it. */
  .vision-bento__tile--background .vision-slide__heading,
  .vision-bento__tile--flow .vision-slide__heading {
    font-size: 24px;
    line-height: 32px;
  }
}


/* ═══ Chips bento — EXACT height of the desc + link group
   (Vicente 2026-07-30) — TABLET ONLY (641-900px) ═══
   Before this pass the bento column stretched over the WHOLE text
   column (heading included), so its top edge sat ~40px above the
   short description. sticky-projects.js now wraps desc + link in
   .sticky-projects__block-descgroup; here .block-textcol becomes
   `display: contents`, which promotes the heading and that group to
   direct children of the 2x2 row grid. The chips then sit in
   column 2 / row 2 — the SAME track as the desc group — so their box
   is its box by construction: top = top of the description,
   bottom = bottom of "Discover the project". No JS measuring, no
   subgrid (subgrid mis-sized row 1 by 4px when the heading overflowed
   its track — measured at 768px).
   Row gap --space-8 (heading → desc rhythm, unchanged); column gap
   --space-16, so text ↔ bento separation > the intra-group rhythm. */
@media (min-width: 641px) and (max-width: 900px) {
  .sticky-projects__stage .sticky-projects__block-row {
    grid-template-rows: auto auto;
    gap:                var(--space-8) var(--space-16);
  }
  /* Box removed: heading + desc-group become row-grid items. */
  .sticky-projects__stage .sticky-projects__block-textcol {
    display: contents;
  }
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-heading {
    grid-column: 1;
    grid-row:    1;
    min-width:   0;
  }
  .sticky-projects__stage .sticky-projects__block-descgroup {
    grid-column:    1;
    grid-row:       2;
    display:        flex;
    flex-direction: column;
    gap:            var(--space-8);
    min-width:      0;
  }
  /* If a project's chips ever need more height than its copy, the
     shared row track grows: pin the link to the track's bottom edge
     so both boxes keep ending on the same line. */
  .sticky-projects__stage .sticky-projects__block-descgroup .sticky-projects__block-link {
    margin-top: auto;
  }
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-categories {
    grid-column:   2;
    grid-row:      2;
    align-self:    stretch;
    height:        100%;
    /* Tight mosaic: zero vertical gutter so the wrapped lines tile
       edge to edge; the horizontal --space-8 gutter stays. */
    row-gap:       var(--space-8);
    column-gap:    var(--space-8);
    align-items:   stretch;   /* base sets `center` → chips floated
                                 mid-line and left holes above/below */
    align-content: stretch;   /* lines share the group height evenly */
  }
  /* Each chip fills its line; the label re-centres inside the now
     taller tile. */
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-categories > * {
    display:     flex;
    align-items: center;
    align-self:  stretch;
  }
}

/* ══════════════════════════════════════════════════════════════
   A11Y FIXES — WCAG 2.2 AA (a11y pass, 2026-07-30)
   Appended as one block on purpose: keep it here, do not fold
   these rules into the layout blocks above.
   ══════════════════════════════════════════════════════════════ */

/* 2.5.8 Target Size (Min) — "Discover the project" measured 156x22
   on phone and 166x24 on tablet; 24x24 is the floor and the inline
   exception does not apply (block-level link, not inside a sentence).
   The block's own gap absorbs the 8px, so vertical rhythm holds. */
@media (max-width: 900px) {
  .sticky-projects__block-link {
    padding-block: 4px;
  }
}

/* 1.4.3 Contrast — "Our future together": #F5E8FC on the brand purple
   #B855FF is 3.08:1 at 14px/500, below the 4.5:1 floor. Dark ink on the
   unchanged brand purple gives 5.35:1. Fails at every breakpoint, so
   this is deliberately outside any media query. */
.vision-flow__label--end {
  color: #080C1D;
}


/* 2.5.8 desktop completion (Vicente 2026-07-30): the same discover link
   measured 23.5px at 1440 — half a pixel under the 24px floor. The
   phone/tablet padding above is scoped ≤900, so desktop needs its own. */
@media (min-width: 901px) {
  .sticky-projects__block-link { padding-block: 1px; }
}

/* ══════════════════════════════════════════════════════════════
   Chip background opacities — tablet only (641-900px) ═══
   (Vicente 2026-07-30)
   
   Regular typology chips: 4% opacity white on bg-hero.
   Role/category chip (first): 8% opacity white on bg-hero.
   ══════════════════════════════════════════════════════════════ */
@media (min-width: 641px) and (max-width: 900px) {
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-categories span {
    background: color-mix(in srgb, var(--white) 4%, var(--bg-hero));
  }
  .sticky-projects__stage .sticky-projects__block-row .sticky-projects__block-categories > span:first-child {
    background: color-mix(in srgb, var(--white) 8%, var(--bg-hero));
  }
}


/* ══════════════════════════════════════════════════════════════
   TASK 1 — Featured-projects header sits closer to the first
   project title (Vicente 2026-07-30) · PHONE ≤640 + TABLET 641-900

   Measured gap = header margin-bottom + the stage frame's own
   padding-top (the stage background is --bg-hero, i.e. the page
   colour, so that padding reads as plain empty space between the
   lead and "Housing Help"). Both levers move, nothing else:
   the stage stays 100svh, its paddings stay FIXED px with no
   env()/dvh, the image band stays anchored to the frame bottom.
   The :has() guard is kept so reduced-motion (no stage built by
   sticky-projects.js) keeps the 56px static-fallback rhythm.

   Phone   89px → 48px  (16 + 72  →  8 + 40)
   Tablet 107px → 64px  (16 + 88  →  8 + 56)
   ══════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .sticky-projects:has(.sticky-projects__stage) .sticky-projects__header {
    margin-bottom: var(--space-8);
  }
  .sticky-projects__stage .sticky-projects__block {
    padding-top: var(--space-40);
  }
}
@media (min-width: 641px) and (max-width: 900px) {
  .sticky-projects__stage .sticky-projects__block {
    padding-top: var(--space-56);
  }
}


/* ══════════════════════════════════════════════════════════════
   TASK 2 — Who I am AI widget behaves like DESKTOP on TABLET
   (641-900, Vicente 2026-07-30). Extends the two-column block at
   ~line 2988; every selector below matches one there and wins by
   source order, so this must stay after it.

   a) Room to read as a chat. The ≥941 rule still supplied
      `grid-template-areas` (a THREE-column template) at this tier,
      even though the 641-900 block re-declares only two tracks —
      the leftover implicit third column ate a full 40px column-gap
      and pinned the widget to its 230px minmax floor. Clearing the
      areas returns that gap to the tracks; the widget column then
      runs 240px @641 → 275px @834 → 300px @900, i.e. the desktop
      widget's own 280-312px range at the top of the tier. The
      .ai-widget__thread / .ai-msg bubble components (~line 1911)
      are ungated, so the chat renders here with no extra rules:
      the thread grows to its 340px cap, then scrolls.

   b) The behaviour note moves BELOW the composer. .ai-widget is a
      flex column, so this is pure `order` — the markup (thread →
      note → bar) is untouched and desktop keeps the note on top.

   c) Widget TOP flush with the bio's first line. The widget used
      to start at grid row 1 (the "Who I am" heading row); it now
      starts at row 2 and repeats the bio's own -40px row-gap
      cancel (see ~line 3022), landing both boxes on the same y.
   ══════════════════════════════════════════════════════════════ */
@media (min-width: 641px) and (max-width: 900px) {
  .about-b__columns {
    grid-template-areas:   none;
    grid-template-columns: minmax(0, 1.6fr) minmax(240px, 1fr);
  }

  .about-b__columns .ai-widget {
    grid-area:  2 / 2 / 4 / 3;
    margin-top: calc(-1 * var(--space-40));
  }

  .about-b__columns .ai-widget__bar  { order: 1; }
  .about-b__columns .ai-widget__note { order: 2; }
}


/* ═══ Who I am facts: ONE merged card on phone (Vicente 2026-07-30) ═══
   The three groups (comes from / sharpened / is now) stop being three
   separate surfaces and become a single card: the container takes the
   card treatment, the children go transparent. Labels, bullets, dots
   and gaps keep their own rhythm. */
@media (max-width: 640px) {
  .about .about__groups {
    background: linear-gradient(180deg,
                  rgba(170, 172, 241, 0.10) 0%,
                  rgba(170, 172, 241, 0) 100%);
    padding: 2px;  /* Vicente 2026-07-30 (el intento previo con var(--space-20) no existía → declaración inválida = 0) */
    gap:     var(--space-24);   /* separation between groups inside the card */
  }
  .about .about__group,
  .about .about__group:hover {
    background: none;
    border:     none;
    padding:    0;
  }
}


/* ═══ Tablet CTA bar: double width, centred (Vicente 2026-07-30) ═══
   The row was fit-content and left-aligned; the pair now reads as the
   section's action bar. `--sp-cta-w` doubles each button's natural
   width via min-width and the row centres itself in the stage. */
@media (min-width: 641px) and (max-width: 900px) {
  .sticky-projects__stage .sticky-projects__cta-row {
    justify-content: center;
  }
  .sticky-projects__stage .sticky-projects__cta,
  .sticky-projects__stage .sticky-projects__cta-secondary {
    min-width: 296px;   /* ~2x the fit-content width measured at 834 */
    justify-content: center;
  }
}


/* ═══ Desktop fixes (Vicente 2026-07-30) ═══ */
@media (min-width: 941px) {
  /* 1 · Hero → Who I am: the pinned section carried 96px of top padding
     on top of the pixel-fringe band, so the title floated ~380px below
     the hero. Halved; the fringe effect above is untouched (it lives on
     .pixel-fringe, not on this padding). */
  .about-b__pin-stage .about { padding-top: var(--space-40); }
  /* The remaining distance is the hero-out pixel fringe (270px at 1440).
     Vicente wants the fringe effect untouched, so instead of shrinking
     it the section slides UP over its faintest rows: the pixels still
     render, the perceived gap halves. */
  .about-b__pin-stage { margin-top: -140px; }

  /* 2 · Closing "...}" sat INSIDE the last card: the -56px margin
     (its own line-height) pulled it up over the bento. It belongs
     BELOW the grid, flush with its left edge. */
  .vision-scroll__close {
    margin-top: var(--space-16);
  }
}


/* ═══ Housing Help surface unified across tiers (Vicente 2026-07-30) ═══
   Phone got the bluish base with greenish patches (it kills the hard
   colour jump against the page). Desktop's sticky card face and the
   tablet unit were still the flat #f3f3f3 studio grey: same recipe
   here, so the project reads identically at every width. */
.sticky-projects__face--1,
.sticky-projects__block-mobile-image--1 {
  background:
    radial-gradient(ellipse at 62% 40%, rgba(56, 110, 88, 0.50), transparent 62%),
    radial-gradient(ellipse at 22% 78%, rgba(46, 122, 96, 0.32), transparent 55%),
    linear-gradient(160deg, #0E1630 0%, #13223D 45%, #0B1326 100%);
}


/* (Beloo desktop top fade retired 2026-07-30: Vicente did not like it.
   The hard top edge is the intended look on desktop cards.) */

/* ═══ Veloque shot +15%, overflow allowed (Vicente 2026-07-30) ═══
   The laptop mock reads small inside the card; scaled up and the card
   is allowed to spill for this face only (the shot's dark field blends
   with the page anyway thanks to the lighten blend). */
@media (min-width: 901px) {
  /* Overflow RETIRED (Vicente 2026-07-30): outside the card the
     mix-blend-mode: lighten has no backdrop to blend into, so the
     shot's black field showed as a hard rectangle. The image keeps
     the +15% scale but is clipped to the card again. */
  .sticky-projects__face--veloque .sticky-projects__face-img {
    transform: scale(1.15);
    transform-origin: center center;
  }
}


/* =============================================================
   HERO — IDENTITY CHIPS (Vicente 2026-07-30)
   Ported from the /hero-lab experiment, approved as-is. The old
   .hero__eyebrow line (absolute at the hero top on desktop, in
   flow on phone/tablet) is GONE from index.html; its content now
   lives in two chips inside the text block. Its rules earlier in
   this file (and in home.css) are dead but harmless — left in
   place so docs/hero-phone-v1.css still reads as a full backup.
   Entrance: animations.js targets .hero__chip, and the CSS
   fallback below gives them the eyebrow's old 70ms beat, so the
   run h1 → chips → tagline → CTAs stays uninterrupted.
   ============================================================= */

/* Desktop + tablet: both chips share one row, between h1 and tagline. */
.hero__chips {
  display:   flex;
  flex-wrap: wrap;
  gap:       var(--space-8);       /* peer gap, both chips equal */
  margin:    var(--space-16) 0 0;  /* h1 → chips */
  padding:   0;
}

.hero__chip {
  display:         inline-block;
  padding:         var(--space-8) var(--space-12);
  border:          0;                  /* explicit: no border */
  border-radius:   var(--chip-radius);
  backdrop-filter: blur(15px);
  -webkit-backdrop-filter: blur(15px);
  font-family:     var(--font-text);   /* Geist, not the eyebrow's mono */
  font-size:       var(--fs-13);
  font-weight:     var(--fw-body);
  letter-spacing:  var(--ls-body);
  line-height:     1.35;               /* same body leading as the tagline */
}

/* Name chip: white fill at 8%, white label. Over --bg-hero (#080C1D)
   the fill composites to ≈#1C1F2F → #FFFFFF on it is far past AA. */
.hero__chip--name {
  background: rgba(255, 255, 255, 0.08);
  color:      #FFFFFF;
}

/* Role chip: brand light purple (--purple-700, #c97fff) at 8% with the
   same purple as the label — ≈7:1 on --bg-hero; the 8% tint barely
   moves the backdrop. */
.hero__chip--role {
  background: rgba(201, 127, 255, 0.08);
  color:      var(--purple-700);
}

/* Entrance — the eyebrow's slot in the stagger, inherited verbatim. */
.hero .hero__chip {
  animation:       hero-b-reveal 500ms ease-out both;
  animation-delay: 70ms;
}
@media (prefers-reduced-motion: reduce) {
  .hero .hero__chip { animation: none; opacity: 1; transform: none; }
}


/* ═══ Phone (≤640) ═══════════════════════════════════════════
   One chip per row, and the name chip lifts ABOVE the headline.
   Two chips on one row wrapped into a stack of pills instead of
   reading as a byline; `display: contents` dissolves the wrapper
   so both chips become direct flex items of .hero__left, and
   order:-1 raises the name. Final order:
   [Vicente Hernaiz] → h1 → [role] → tagline → CTAs.            */
@media (max-width: 640px) {

  /* Vertical re-seating. The eyebrow used to sit IN FLOW here
     (margin 136px 20px 0), and .hero__body was pulled -160px to
     land the h1 at its calibrated y. With the eyebrow removed and
     the chips added, that offset overshoots: the hero is
     flex-centered, so a margin delta of X moves the block X/2.
     -12px is the value validated on /hero-lab — it keeps a ~40px
     gap between the CTAs and the down arrow (the binding
     constraint at the bottom) and lets the taller text block
     breathe upward. */
  .hero__body { margin-top: -12px; }

  .hero__chips { display: contents; }

  .hero__chip--name {
    order:         -1;
    align-self:    flex-start;
    margin-bottom: var(--space-12);
    /* Only this chip keeps the +2pt: short, alone above the headline,
       reads better bigger. The role chip stays at 13 or it wraps. */
    font-size:     15px;
  }

  .hero__chip--role {
    align-self: flex-start;
    /* 16 above; the -12 below cancels part of the tagline's 24 top
       margin → 12px chip→tagline, the gap Vicente set on /hero-lab. */
    margin:     var(--space-16) 0 -12px;
  }

  /* home-b.css zeroes this earlier for the old h1+tagline pairing;
     the chips are the element above it now. Group separation
     (chips → tagline) stays larger than the intra-group gaps. */
  .hero .hero__tagline { margin-top: var(--space-24); }

  /* CTAs side by side: the bordered button hugs its text and the quiet
     AI link rides next to it, so the pair reads as one line instead of
     a stacked list. Both keep a 44px tap height. */
  .hero__ctas {
    flex-direction: row;
    align-items:    center;
    flex-wrap:      wrap;
    gap:            var(--space-16);
  }
  .hero__ctas .hero__btn {
    width:      auto;
    flex:       0 0 auto;
    min-height: 44px;
  }
  .hero__ctas .hero__cta-wrap { width: auto; flex: 0 1 auto; }
  .hero__ctas .hero__link {
    width:      auto;
    min-height: 44px;
  }
}


/* Hero primary button border at 40% (Vicente 2026-07-30): home.css:176
   uses solid white, too loud next to the new chips. */
.hero__btn { border-color: rgba(255, 255, 255, 0.40); }


/* Tablet: kill the black band above the hero (Vicente 2026-07-30).
   .main carries 88px of top padding for the floating mobile nav
   (styles.css ≤900). Phone cancels it with .hero{margin-top:-88px}
   (home.css:943, ≤640 only), so at 641-900 the hero started 88px
   down and the page bg showed as a band. Same cancellation here. */
@media (min-width: 641px) and (max-width: 900px) {
  .hero { margin-top: -88px; min-height: 100svh; }
}


/* Tablet: down-arrow shaft aligned with the hero text (Vicente
   2026-07-30). The text inset dropped to 40px when the sidebar rail
   was replaced by the mobile nav, but the arrow kept the base
   max(96px, …) inset, leaving the shaft at x=112. The shaft sits at
   x=16 inside the 32px viewBox, so the box goes to 24 for the LINE
   to land on 40. */
@media (min-width: 641px) and (max-width: 900px) {
  .hero__down-arrow { left: 24px; }
}


/* Tablet: "...}" BELOW the grid, not over card 05 (Vicente 2026-07-30).
   The ≤900 rule pulled it up by its own line-height (-40px) to sit
   inside the last card; desktop was already fixed, tablet was not. */
@media (min-width: 641px) and (max-width: 900px) {
  .vision-scroll__close { margin-top: var(--space-16); }
}


/* Veloque: dejar ver el overflow del mock escalado (Vicente 2026-07-30).
   No había overflow:hidden en la cadena; quien recortaba era la
   mask-image del CARD (el fundido inferior), porque una máscara recorta
   siempre a la caja del elemento y a sus hijos. Con Veloque activo la
   retiramos: su foto ya se funde con la página por el mix-blend-mode
   lighten sobre --bg-main, así que el borde inferior duro no se nota. */
/* (The mask removal for Veloque is gone too: with the card clipping
   again, the bottom dissolve is back for every face. Vicente
   2026-07-30.) */
