.hero {
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding: 140px 24px 80px;
}

.hero__grid {
  width: 100%;
  max-width: 1560px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: 48px;
  align-items: center;
}

@media (min-width: 1024px) {
  .hero__grid {
    grid-template-columns: 0.85fr 1.3fr;
    gap: 56px;
  }
}

/* Left column: title / description / CTA */
.hero__intro {
  text-align: center;
  transition: opacity 0.7s ease-out, transform 0.7s ease-out;
  opacity: 0;
  transform: translateY(20px);
}

@media (min-width: 1024px) {
  .hero__intro {
    text-align: left;
  }
}

.hero.is-animated .hero__intro {
  opacity: 1;
  transform: translateY(0);
}

.hero__title {
  font-family: var(--font-satoshi);
  font-weight: 600;
  font-size: 28px;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: #fff;
  /* width:fit-content shrinks this block box down to its longest line's own
     width — text-align:center then only centers the *shorter* line inside
     that narrower box, not the whole title inside the actual viewport.
     margin:auto is what centers the box itself (below, at 1024px+ this
     flips to margin:0 to sit left, matching .hero__intro's own alignment). */
  margin: 0 auto;
  width: fit-content;
  max-width: 100%;
  text-align: center;
}

.hero__title strong,
.hero__title-highlight {
  display: inline-block;
  margin-top: -0.08em;
  background: linear-gradient(90deg, #3f7bf8 0%, #a866f0 50%, #f86bb8 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero__description {
  margin-top: 20px;
  font-family: var(--font-satoshi);
  font-size: 18px;
  color: #d4d4e0;
  max-width: 540px;
}

/* A block (not the old plain <br/>) so a small margin-top can actually
   apply between it and the line above — a very light bit of breathing
   room, not a full paragraph break. */
.hero__tagline {
  display: block;
  margin-top: 10px;
}

@media (min-width: 1024px) {
  .hero__description {
    margin-left: 0;
    margin-right: 0;
  }
}

.hero__description {
  margin-left: auto;
  margin-right: auto;
}

@media (min-width: 1024px) {
  .hero__description {
    margin-left: 0;
  }
}

.hero__cta {
  margin-top: 32px;
  text-align: center;
}

/* Doubled-up selector (not just .hero__cta-btn) so this reliably beats
   .btn's own fixed `width: 234px` — same specificity as a bare class
   selector, so which one wins would otherwise depend on load order alone
   (button.css loads after hero.css in this page's extra_css list, so the
   fixed width was silently winning the whole time regardless of the
   padding set here — confirmed directly, the box never actually shrank
   until this selector was made more specific). */
.btn.hero__cta-btn {
  position: relative;
  overflow: hidden;
  width: auto;
  /* A modest trim from the original fixed 234px, not the drastic ~170px
     the first attempt at "auto" landed on once its width:auto actually
     started working (see the specificity note above) — 44px of padding
     lands close to the original width while still being a real reduction. */
  padding: 0 44px;
}

/* Stars behind the label — absolutely positioned so they never affect the
   flex layout of the label/rocket, and painted first (before them in
   source order) so they always sit behind the text, never over it. */
.hero__cta-btn__stars {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Plain, fixed-looking dots (no twinkle/scale pulse) that stream along the
   SAME diagonal axis as the rocket, but travelling the opposite way
   (north-east -> south-west) — background rushing past in reverse is what
   actually reads as forward motion (the same parallax idea as a scrolling
   star field behind a spaceship, or this project's own page-transition
   starfield falling as its rocket climbs). Each star loops the identical
   path on the same 2.4s cycle, just evenly staggered in time (0.6s apart,
   a quarter-cycle each on 4 stars) — with only 4 dots that's what makes it
   read as one continuous stream rather than 4 things blinking in sync.
   Fades in/out right at the loop seam so the instant reset is never seen
   mid-opacity. */
.hero__cta-btn__star {
  position: absolute;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  animation: hero-cta-star-stream 2.4s linear infinite;
}

/* Spread across the whole button (corners and edges included), not
   clustered along one diagonal band through the middle — six now instead
   of four, positions picked to cover top/bottom and left/right fairly
   evenly. Delays evenly split across the 2.4s cycle (0.4s apart, a sixth
   each). */
.hero__cta-btn__star--1 {
  top: 18%;
  left: 8%;
  animation-delay: 0s;
}

.hero__cta-btn__star--2 {
  top: 78%;
  left: 18%;
  width: 2px;
  height: 2px;
  animation-delay: 0.4s;
}

.hero__cta-btn__star--3 {
  top: 15%;
  left: 48%;
  animation-delay: 0.8s;
}

.hero__cta-btn__star--4 {
  top: 68%;
  left: 58%;
  width: 2px;
  height: 2px;
  animation-delay: 1.2s;
}

.hero__cta-btn__star--5 {
  top: 28%;
  left: 88%;
  animation-delay: 1.6s;
}

.hero__cta-btn__star--6 {
  top: 82%;
  left: 38%;
  width: 2px;
  height: 2px;
  animation-delay: 2s;
}

@keyframes hero-cta-star-stream {
  0% {
    translate: 24px -16px;
    opacity: 0;
  }
  12% {
    opacity: 0.85;
  }
  85% {
    opacity: 0.85;
  }
  100% {
    translate: -24px 16px;
    opacity: 0;
  }
}

.hero__cta-btn__label {
  position: relative;
}

/* Rocket + smoke group — a fixed box so the smoke (positioned absolute
   within it) always lines up with the rocket's own tail regardless of the
   surrounding flex layout. */
.hero__cta-btn__rocket-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

/* Nose pointed north-east (rotate(45deg) from the SVG's own straight-up
   drawing). Bigger swing and a scale pulse now (was a barely-there 2px
   nudge) plus a shorter cycle — reads as a real, energetic thrust/climb
   instead of a gentle sway. */
.hero__cta-btn__rocket {
  position: relative;
  z-index: 1;
  animation: hero-cta-rocket-fly 1.5s ease-in-out infinite;
}

@keyframes hero-cta-rocket-fly {
  0%,
  100% {
    transform: rotate(45deg) translate(0, 0) scale(1);
  }
  50% {
    transform: rotate(38deg) translate(3.5px, -3.5px) scale(1.14);
  }
}

/* Soft puffs drifting off south-west (opposite the rocket's own north-east
   heading), fading and expanding on a loop — positioned at the tail end of
   the un-rotated SVG (bottom-left of the icon box, where the flame ends up
   once rotated). */
.hero__cta-btn__smoke {
  position: absolute;
  bottom: 3px;
  left: 3px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  filter: blur(2px);
  opacity: 0;
  animation: hero-cta-smoke-puff 2.4s ease-out infinite;
}

.hero__cta-btn__smoke--2 {
  width: 5px;
  height: 5px;
  animation-delay: 0.8s;
}

@keyframes hero-cta-smoke-puff {
  0% {
    opacity: 0;
    transform: translate(0, 0) scale(0.4);
  }
  15% {
    opacity: 0.5;
  }
  100% {
    opacity: 0;
    transform: translate(-7px, 7px) scale(1.7);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero__cta-btn__star,
  .hero__cta-btn__rocket,
  .hero__cta-btn__smoke {
    animation: none !important;
  }

  .hero__cta-btn__rocket {
    transform: rotate(45deg);
  }

  /* The stars are only ever visible through the streaming animation's own
     opacity keyframes (resting opacity is 0) — without a static fallback
     here, turning that animation off per above would make every star
     simply vanish instead of just holding still. */
  .hero__cta-btn__star {
    opacity: 0.6;
    translate: 0 0;
  }
}

@media (min-width: 640px) {
  .hero__title {
    font-size: 34px;
  }
}

@media (min-width: 1024px) {
  .hero__title {
    font-size: 40px;
    margin: 0;
  }
}

/* Right column: chat card */
.hero-chat {
  width: 100%;
  /* Explicit (rather than relying on the transform below also creating a
     containing block) — the settings gear anchor is absolutely positioned
     against this card. */
  position: relative;
  /* Redesign: anthracite with a very faint violet undertone, matte rather
     than glowing — the card is the main structure, not a glowing object.
     No large violet glow around the whole card anymore. */
  background: linear-gradient(145deg, rgba(37, 36, 49, 0.96), rgba(29, 27, 40, 0.96));
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 24px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.035);
  /* Bottom padding grew a little (12px -> 20px) to make room for the
     architecture link now living in this same strip, below the composer/
     turnstile — everything above it (composer height, etc.) is
     unaffected, this is purely extra breathing room at the card's own
     bottom edge. */
  padding: 18px 20px 20px;
  transition: opacity 0.7s ease-out 0.15s, transform 0.7s ease-out 0.15s;
  opacity: 0;
  transform: translateY(20px);
}

@media (min-width: 1024px) {
  .hero-chat {
    padding: 25px 28px 24px;
  }
}

.hero.is-animated .hero-chat {
  opacity: 1;
  transform: translateY(0);
}

.hero-chat__header {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  margin-bottom: 18px;
  /* Real bug found via testing: the tooltip below used to be positioned
     relative to .hero-chat__info-anchor itself, i.e. wherever the "i"
     button happens to land — and the live badge to its left is long
     enough (uppercase monospace, letter-spacing) that on a narrow phone
     it pushes the button most of the way across the card, so a
     left-anchored tooltip starting there ran way past the right edge of
     the screen. Anchoring to the header's own left edge instead (a
     position every card width already keeps on-screen, independent of
     how long the badge text renders) keeps the tooltip in a stable,
     always-safe spot regardless of screen size. */
  position: relative;
}

/* Small standalone "i" info button next to the live badge — separate from
   the settings gear (top-right, no tooltip of its own any more). */
.hero-chat__info-anchor {
  display: inline-flex;
}

.hero-chat__info-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  color: rgba(255, 255, 255, 0.45);
  background: transparent;
  border: none;
  cursor: pointer;
  transition: color 0.2s ease;
}

.hero-chat__info-btn:hover {
  color: rgba(255, 255, 255, 0.85);
}

.hero-chat__info-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
  border-radius: 50%;
}

.hero-chat__info-btn svg {
  width: 17px;
  height: 17px;
}

@media (min-width: 1024px) {
  .hero-chat__header {
    margin-bottom: 25px;
  }
}

.hero-chat__label {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: monospace;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(46, 204, 113, 0.12);
  border: 1px solid rgba(46, 204, 113, 0.3);
  padding: 5px 12px;
  border-radius: 9999px;
}

.hero-chat__status-dot {
  position: relative;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #2ecc71;
  box-shadow: 0 0 6px #2ecc71;
}

.hero-chat__status-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #2ecc71;
  animation: status-dot-halo 1.6s ease-out infinite;
}

@keyframes status-dot-halo {
  0% {
    transform: scale(1);
    opacity: 0.7;
  }
  100% {
    transform: scale(3.2);
    opacity: 0;
  }
}

/* Stabilization pass §1 — compact intro block between the badge and the
   image. Left-aligned, deliberately less imposing than the page's own H1
   (.hero__title), one line on desktop where it fits, never a second heavy
   card. */
.hero-chat__intro {
  margin: 10px 0 14px;
  text-align: left;
}

.hero-chat__intro-title {
  margin: 0;
  font-family: var(--font-satoshi);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.3;
  color: #eef1f8;
  /* hero-heading-rotator.js crossfades textContent changes on this element
     itself — a plain opacity dip-and-recover on the whole node, simplest
     thing that reads as an intentional swap rather than a jump cut. */
  transition: opacity 0.35s ease;
}

.hero-chat__intro-title.is-swapping {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__intro-title {
    transition: none;
  }
}

@media (min-width: 640px) {
  .hero-chat__intro-title {
    font-size: 19px;
    white-space: nowrap; /* one line on desktop where it fits, per the brief */
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

@media (min-width: 1024px) {
  .hero-chat__intro-title {
    font-size: 20px;
  }
}

/* Info bubble on the small "i" button next to the live badge — folds in
   what used to be the always-visible intro description + LinkedIn
   microtext, shown on hover/focus only now that the intro block above is
   just the rotating title. Not a strict ARIA tooltip (it holds a real
   clickable link), so it's plain hover/focus-within CSS rather than
   JS-timed show/hide. Opens toward the right (left:0) since its anchor
   sits near the card's own left edge — anchoring right:0 here would push
   most of the bubble off-card. */
.hero-chat__info-tip {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  /* Real bug found via testing: 78vw is a fraction of the whole viewport,
     not of this card — on a narrow phone, minus the anchor's own offset
     from the card's left edge, that could still push the tooltip's right
     edge past the viewport. calc(100vw - Npx) guarantees a fixed margin
     on both sides regardless of screen size instead, same technique
     already used for the settings panel (hero-chat-settings.js). */
  width: min(280px, calc(100vw - 48px));
  max-width: calc(100vw - 48px);
  padding: 12px 14px;
  border-radius: 12px;
  background: rgba(18, 16, 28, 0.97);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
  pointer-events: none;
  z-index: 20;
}

.hero-chat__info-anchor:hover .hero-chat__info-tip,
.hero-chat__info-anchor:focus-within .hero-chat__info-tip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  pointer-events: auto;
}

.hero-chat__info-tip p {
  margin: 0;
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  line-height: 1.5;
  color: rgba(238, 241, 248, 0.82);
}

.hero-chat__info-tip p + p {
  margin-top: 8px;
  font-size: 11px;
  color: rgba(238, 241, 248, 0.5);
}

.hero-chat__info-tip a {
  color: #a78bfa;
  text-decoration: none;
  border-bottom: 1px solid rgba(167, 139, 250, 0.35);
  transition: color 0.15s ease, border-color 0.15s ease;
}

.hero-chat__info-tip a:hover {
  color: #d9b8ff;
  border-color: rgba(217, 184, 255, 0.6);
}

.hero-chat__info-tip a:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__info-tip {
    transition: none;
  }
}

.hero-chat__image {
  width: 100%;
  aspect-ratio: 1672 / 941;
  border-radius: 14px;
  overflow: hidden;
  margin-bottom: 16px;
  position: relative;
  background: linear-gradient(160deg, #1c1c28, #0d0d13);
  border: 1px solid rgba(255, 255, 255, 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero-chat__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: none;
}

.hero-chat__image.has-photo img {
  display: block;
}

.hero-chat__image.has-photo .hero-chat__image-placeholder {
  display: none;
}

.hero-chat__image-placeholder {
  font-family: monospace;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.35);
}

/* Positioned over the left-hand vertical monitor in /images/hero-desk.webp — a
   purely decorative looping terminal (pure CSS, no JS) while idle. Doubles as
   the screensaver half of hero-screen-controller.js's idle/active pair (see
   its data-chat-screensaver attribute in index.njk): hidden the instant a
   real operation starts, restored once idle again — the two <pre> blocks
   in the markup are identical so translateY(-50%) loops seamlessly whenever
   it's visible. */
.hero-chat__terminal.is-hidden {
  display: none;
}

.hero-chat__terminal {
  /* Measured directly off the photo's pixels (luminance edge-detection on the
     screen's near-black interior vs. its bezel), not eyeballed: the box below
     is exactly the inner screen, stopping short of the bezel/"PAGTIC" label.
     Position/size/tilt/mask are the physical monitor calibration — never
     touched by the MLOps-pipeline log content refresh below. */
  position: absolute;
  top: 23.1%;
  left: 13.3%;
  width: 16.4%;
  height: 47.3%;
  overflow: hidden;
  border-radius: 2px;
  padding: 10px 8px 10px 13px;
  /* Every descendant font-size below is sized in cqw (this element's own
     width), not vw (the full viewport) — the hero content is capped at
     max-width:1560px (.hero__grid), so past that breakpoint this box
     stops growing while vw keeps climbing, pinning every clamp() at its
     ceiling and making text/buttons look oversized relative to a screen
     that's no longer actually getting any bigger. cqw tracks this box's
     real rendered width instead, so sizing stays proportionally correct
     whether the box is capped on an ultrawide monitor or shrunk on a
     phone. */
  container-type: inline-size;
  perspective: 420px;
  perspective-origin: 50% 50%;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
  /* MLOps log palette (custom properties) + base type — merged in from the
     terminal-log.css content refresh rather than left as a second
     duplicate `.hero-chat__terminal { }` block. */
  --terminal-fg: #dce7ff;
  --terminal-muted: #74819a;
  --terminal-cyan: #5ee7ff;
  --terminal-blue: #60a5fa;
  --terminal-violet: #b994ff;
  --terminal-pink: #ff72d2;
  --terminal-green: #63f59a;
  --terminal-yellow: #ffd166;
  --terminal-orange: #ff9f43;
  --terminal-red: #ff6b81;
  color: var(--terminal-fg);
  font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", Consolas, monospace;
  font-variant-ligatures: none;
  text-rendering: geometricPrecision;
}

.hero-chat__terminal-tilt {
  /* Matches the monitor's slight turn in the photo: its left edge is closer to
     the viewer than its right edge, so the left side of the text should render
     larger/closer and the right side smaller/farther. */
  transform: rotateY(10deg);
  transform-style: preserve-3d;
}

/* The tilt layer is now the shared stage for all five states (ready,
   running, summary, confetti, live) — each a position:absolute overlay
   filling it, only one (plus the confetti layer, briefly) visible at a
   time via the native `hidden` attribute + a fade transition. Needs an
   explicit height (the box above only has padding, no fixed content
   height) for `inset:0` on those children to actually mean "fill the
   screen", not "as tall as whatever happens to overflow". */
.hero-chat__terminal-tilt {
  position: relative;
  height: 100%;
}

/* Shared fade transition for every full-screen state panel. Panels start
   invisible in CSS; index.njk marks the initial `ready` panel `is-in`
   directly in the markup so it's the one thing guaranteed visible even if
   JS is slow to boot (or absent — see .hero-chat__terminal-noscript). */
.hero-chat__terminal-panel {
  position: absolute;
  inset: 0;
  flex-direction: column;
  opacity: 0;
  transform: translateY(5px);
  transition: opacity 0.36s ease, transform 0.36s ease;
  pointer-events: none;
}

/* An unconditional `display: flex` above would win over the UA stylesheet's
   `[hidden] { display: none }` (author rules beat UA rules regardless of
   specificity), silently keeping every panel in the render and
   accessibility tree at all times. Scoping display to :not([hidden]) lets
   the native attribute actually remove inactive panels — .is-in/.is-out
   still drive the opacity/transform transition on top of that. */
.hero-chat__terminal-panel:not([hidden]) {
  display: flex;
}

.hero-chat__terminal-panel.is-in {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.hero-chat__terminal-panel.is-out {
  opacity: 0;
  transform: translateY(-5px);
  pointer-events: none;
}

/* --- READY -------------------------------------------------------------
   Deliberately down to two elements — a title and a button — so the small
   screen reads instantly instead of competing for attention with a badge/
   tagline/stepper. Title and button fade in independently (see their own
   `animation` below) with the button trailing ~350ms behind the title, a
   pure-CSS choreography that still runs with JS absent/slow to boot. */
/* Explicit direction after seeing it live ("je veux la même inclinaison"
   [as the log text]): was centered, both axes — the exact same reason
   .hero-chat__terminal-boot itself already isn't (see that rule's own
   comment) applies here too, just never fixed for this panel. Dead
   center sits right on the tilt's rotation origin
   (perspective-origin: 50% 50%), where rotateY barely displaces
   anything, so this read as flat, floating text instead of something
   embedded in the tilted screen like the log lines are.

   Left-anchored horizontally (align-items), which is what actually
   produces the tilt — rotateY is a rotation around a VERTICAL axis, so
   the visible skew comes from horizontal distance to that center
   pivot, not vertical. Left/top-anchoring BOTH axes (the first pass
   here) over-corrected per explicit follow-up feedback ("au milieu de
   l'écran, pas tout en haut") — justify-content stays centered so it
   sits vertically in the middle of the screen, tilt and all. */
.hero-chat__terminal-ready {
  justify-content: center;
  align-items: flex-start;
  gap: clamp(10px, 6%, 20px);
  text-align: left;
  /* Symmetric top/bottom now (was 10%/0, left over from the first,
     top-anchored pass) — with justify-content: center restored, unequal
     padding would have pulled the visual center off from the box's own
     true middle. Left/right stay asymmetric on purpose (12%/8%): a
     little less on the right keeps the button from crowding that
     edge once it's no longer centered. */
  padding: 10% 8% 10% 12%;
}

.hero-chat__terminal-title {
  margin: 0;
  max-width: 15ch;
  font-size: clamp(7.6px, 10.9cqw, 11px);
  font-weight: 800;
  line-height: 1.4;
  color: #f2f5ff;
  text-shadow: 0 0 4px rgb(255 255 255 / 10%);
  opacity: 0;
  animation: terminal-ready-fade 520ms ease-out 60ms forwards;
}

/* The launch button — sober and premium rather than a loud gradient: a
   dark tinted fill with a fine coral border, no permanent glow/pulse/
   shine. Green is deliberately never used here: it's reserved for
   successful validations later in the experience. Trails the title in by
   ~350ms (see the animation-delay below) so the two reveal in sequence. */
.hero-chat__terminal-launch {
  position: relative;
  /* Flex items whose text has to wrap otherwise take the FULL available
     cross-axis width regardless of align-items:center on the parent — the
     browser's shrink-to-fit formula falls back to "available width"
     whenever the one-line (max-content) width doesn't fit, which is
     always true here once the label wraps to two lines. width:fit-content
     runs the real fit-content algorithm instead, sizing the button to its
     own wrapped text; max-width is just a hard safety cap. */
  width: fit-content;
  max-width: 17ch;
  margin: 0;
  padding: 0.65em 0.7em;
  border: 1px solid rgb(255 91 111 / 72%);
  border-radius: 10px;
  font-family: inherit;
  font-size: clamp(7.2px, 9.2cqw, 9px);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: #ff6577;
  background: rgb(38 28 40 / 72%);
  cursor: pointer;
  transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
  opacity: 0;
  animation: terminal-ready-fade 480ms ease-out 400ms forwards;
}

.hero-chat__terminal-launch:hover {
  transform: translateY(-1px);
  border-color: rgb(255 128 145 / 90%);
  background: rgb(58 30 38 / 78%);
  box-shadow: 0 0 10px 1px rgb(255 91 111 / 22%);
}

.hero-chat__terminal-launch:active,
.hero-chat__terminal-launch.is-pressed {
  transform: translateY(0) scale(0.98);
}

.hero-chat__terminal-launch.is-pressed {
  animation: terminal-ready-fade 480ms ease-out 400ms forwards, terminal-launch-flash 380ms ease-out;
}

.hero-chat__terminal-launch:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgb(255 91 111 / 35%);
}

.hero-chat__terminal-launch:disabled {
  cursor: default;
}

/* The 7.6-9px legibility floors on the title/button above are sized for a
   normal desktop/tablet terminal box — on a real phone the box itself
   (a fixed % of the hero image) can render under 60px wide, where those
   floors no longer fit at all and the title/button clip against
   .hero-chat__terminal's own overflow:hidden. Queries the terminal's own
   rendered width (not the viewport) since that's what actually
   determines whether content fits here. Placed after the base rules on
   purpose — @container doesn't out-rank equal-specificity rules that
   simply come later in the file, so this has to sit last to win. */
@container (max-width: 90px) {
  .hero-chat__terminal-ready {
    gap: clamp(3px, 5%, 8px);
  }

  .hero-chat__terminal-title {
    max-width: 12ch;
    font-size: clamp(4.6px, 10.9cqw, 11px);
  }

  .hero-chat__terminal-launch {
    font-size: clamp(4.4px, 9.2cqw, 9px);
    padding: 0.5em 0.5em;
  }
}

.hero-chat__terminal-boot {
  /* Left/top instead of centered: a short centered line sits near the
     tilt's rotation origin (perspective-origin: 50% 50%), where the
     rotateY distortion barely shows — it reads as flat, floating text
     rather than something embedded in the screen. Anchoring it where a
     real log line starts gives it the same visible "inside the tilted
     glass" depth as everything else. */
  align-items: flex-start;
  justify-content: flex-start;
  margin: 0;
  padding-top: 0.3em;
  font-size: clamp(6.5px, 8.3cqw, 8px);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--terminal-cyan);
  text-shadow: 0 0 8px rgb(94 231 255 / 45%);
  /* §22: honors the real \n between "INITIALIZING" and "PIPELINE..." (see
     index.njk) as an actual line break — never collapsed like normal
     whitespace, and never dependent on the panel's own width the way
     ordinary wrapping would be. */
  white-space: pre-line;
}

.hero-chat__terminal-boot--typing::after {
  content: "▋";
  display: inline-block;
  margin-left: 0.08em;
  color: var(--terminal-cyan);
  text-shadow: 0 0 7px rgb(94 231 255 / 75%);
  animation: terminal-cursor-blink 720ms steps(1, end) infinite;
}

.hero-chat__terminal-noscript {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 6%;
  margin: 0;
  padding: 0.4em 0.5em;
  border-radius: 4px;
  background: rgb(10 7 22 / 65%);
  font-size: clamp(5.6px, 6.8cqw, 6.8px);
  line-height: 1.35;
  color: var(--terminal-muted);
  text-align: center;
}

/* Visually hidden but still announced — the aria-live status line, never
   shown as text (the visible states already carry the same information). */
.hero-chat__terminal-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

/* --- RUNNING / global progress bar ---------------------------------------
   A thin strip running the full width of the true screen, flush against
   its bottom edge — negative insets cancel .hero-chat__terminal's own
   padding (10px 8px 10px 13px) to reach that edge exactly, rather than
   floating as a pill inset from it. Outside the scrolling track entirely
   (a sibling panel, not a child of .hero-chat__terminal-track) so the log
   auto-scroll never carries it along or covers it. Tied to the pipeline's
   real per-line progress (PipelineRun.onProgress), never an independent
   timer, and only ever shown while the pipeline is actually running (see
   the :not([hidden]) rule below — an unconditional `display` here would
   otherwise win over the `hidden` attribute, same class of bug already
   fixed once for .hero-chat__terminal-panel). */
.hero-chat__terminal-progress {
  /* Deliberately NOT breaking out of .hero-chat__terminal's own padding
     via negative insets any more — that padding is exactly the safe zone
     already measured/calibrated against the real monitor photo (see the
     comment on .hero-chat__terminal above), and extending past it on the
     real image didn't line up with the tilted screen's actual edge the
     way it did in a flat/untilted approximation. Spanning the full width
     of that already-safe inner box keeps it correctly aligned. */
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  height: 12px;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 0;
  background: #050409;
  border-top: 1px solid rgb(255 255 255 / 10%);
  transition: opacity 220ms ease;
}

.hero-chat__terminal-progress:not([hidden]) {
  display: flex;
}

.hero-chat__terminal-progress.is-out {
  opacity: 0;
}

.hero-chat__terminal-progress-fill {
  position: absolute;
  inset: 0;
  width: 0%;
  /* Same green as [OK]/validation lines (--terminal-green), not a
     separate custom shade — one established "success" green across the
     whole terminal instead of two similar-but-different greens. */
  background: var(--terminal-green);
  transition: width 320ms ease;
}

.hero-chat__terminal-progress-label {
  position: relative;
  z-index: 1;
  font-size: clamp(5.6px, 6.8cqw, 6.8px);
  /* Matches .log-cmd exactly (the "$ terraform plan ..." command text) —
     same near-white, same weight, same soft glow — instead of a separate
     pure #fff + dark-outline treatment. */
  font-weight: 650;
  letter-spacing: 0.03em;
  color: #f4f7ff;
  text-shadow: 0 0 4px rgb(255 255 255 / 18%);
}

.hero-chat__terminal-track {
  overflow: hidden;
}

.hero-chat__terminal-track > pre {
  transform: translate3d(0, 0, 0);
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.hero-chat__terminal-track.is-jump-scrolling > pre {
  transition: none !important;
}

.terminal-live-line {
  display: block;
  min-height: 1.46em;
  opacity: 0;
  transform: translateY(0.18em);
  animation: terminal-line-arrival 180ms ease-out forwards;
}

.terminal-live-line--blank {
  user-select: none;
}

.terminal-live-line--refresh {
  animation: terminal-progress-refresh 160ms ease-out forwards;
}

.terminal-live-line--typing::after {
  content: "▋";
  display: inline-block;
  margin-left: 0.08em;
  color: var(--terminal-green);
  text-shadow: 0 0 7px rgb(99 245 154 / 75%);
  animation: terminal-cursor-blink 720ms steps(1, end) infinite;
}

.terminal-live-line--milestone {
  animation:
    terminal-line-arrival 180ms ease-out forwards,
    terminal-success-pulse 1.25s ease-in-out 160ms 2;
}

/* One persistent, in-place epoch block instead of ten stacked ones: created
   once on the first epoch, then every field is updated by reference on
   each subsequent epoch (see PipelineRun.renderEpochBlock in
   terminal-live.js) — the block itself never re-appends or disappears.
   Reuses the existing terminal-progress-refresh keyframe (already used for
   in-place progress-bar updates) for the "light flash" on each update. */
.terminal-epoch-block-line {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

.terminal-epoch-header {
  display: block;
  color: var(--terminal-yellow);
  font-weight: 750;
  text-shadow: 0 0 5px rgb(255 209 102 / 28%);
}

.terminal-epoch-bar {
  display: block;
  color: #a9b8ff;
}

.terminal-epoch-metrics {
  display: flex;
  flex-direction: column;
  gap: 0.1em;
}

.terminal-epoch-row {
  display: flex;
}

.terminal-epoch-label {
  min-width: 14ch;
  flex-shrink: 0;
  color: var(--terminal-muted);
}

.terminal-epoch-value {
  color: #74f1ff;
  font-weight: 600;
}

/* --- SUMMARY ------------------------------------------------------------ */
.hero-chat__terminal-summary {
  justify-content: center;
  gap: clamp(1.5px, 0.6%, 3px);
}

.hero-chat__terminal-summary-line {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.4em;
  font-size: clamp(6px, 7.6cqw, 7.4px);
  line-height: 1.35;
  color: var(--terminal-fg);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.hero-chat__terminal-summary-line.is-in {
  opacity: 1;
  transform: translateY(0);
}

.hero-chat__terminal-summary-check {
  color: var(--terminal-green);
  text-shadow: 0 0 6px rgb(99 245 154 / 55%);
}

.hero-chat__terminal-summary-final {
  margin: 0.5em 0 0;
  font-size: clamp(7.5px, 10cqw, 9.5px);
  font-weight: 900;
  letter-spacing: 0.02em;
  color: #86ffb0;
  text-shadow:
    0 0 4px rgb(134 255 176 / 82%),
    0 0 12px rgb(48 255 128 / 45%);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.hero-chat__terminal-summary-final.is-in {
  opacity: 1;
  transform: translateY(0);
  animation: terminal-success-pulse 1.3s ease-in-out 0.35s 2;
}

/* --- CONFETTI ------------------------------------------------------------ */
/* Strictly clipped to the terminal's own box (inherited overflow:hidden)
   and inert to pointer events — it only ever sits on top of the summary
   screen, briefly, never blocking the launch button or anything else. */
.hero-chat__terminal-confetti {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 3;
}

.hero-chat__terminal-confetti-piece {
  position: absolute;
  top: -8%;
  left: var(--x, 50%);
  width: 4px;
  height: 4px;
  background: currentColor;
  opacity: 0;
  transform: translate3d(0, 0, 0) rotate(0deg);
}

.hero-chat__terminal-confetti.is-active .hero-chat__terminal-confetti-piece {
  animation: terminal-confetti-fall var(--duration, 1.7s) ease-in var(--delay, 0s) forwards;
}

.hero-chat__terminal-confetti-piece--rect {
  width: 5px;
  height: 3px;
}

.hero-chat__terminal-confetti-piece--dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
}

.hero-chat__terminal-confetti-piece--line {
  width: 6px;
  height: 1.4px;
}

/* --- PRODUCTION LIVE dashboard -------------------------------------------- */
.hero-chat__terminal-live {
  gap: clamp(3px, 1%, 6px);
}

.hero-chat__terminal-live-status {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.4em;
  font-size: clamp(6.2px, 8cqw, 7.6px);
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--terminal-green);
  text-shadow: 0 0 6px rgb(99 245 154 / 35%);
}

.hero-chat__terminal-live-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--terminal-green);
  box-shadow: 0 0 6px 1px rgb(99 245 154 / 75%);
  animation: terminal-dot-pulse 3s ease-in-out infinite;
}

.hero-chat__terminal-live-metrics {
  display: grid;
  gap: 0.15em 0.5em;
  font-size: clamp(5.6px, 7.1cqw, 6.8px);
}

.hero-chat__terminal-live-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5em;
}

.hero-chat__terminal-live-row dt {
  margin: 0;
  color: var(--terminal-muted);
}

.hero-chat__terminal-live-row dd {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.35em;
  font-weight: 700;
  color: var(--terminal-fg);
  font-variant-numeric: tabular-nums;
}

.hero-chat__terminal-live-bar {
  display: inline-block;
  width: 22px;
  height: 3px;
  border-radius: 2px;
  background: rgb(255 255 255 / 12%);
  overflow: hidden;
}

.hero-chat__terminal-live-bar > span {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--terminal-cyan), var(--terminal-violet));
  transition: width 1.1s ease;
}

.hero-chat__terminal-live-feed {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* flex:1 reserves the full remaining height upfront so reaching
     MAX_FEED_LINES never shifts the layout — but justify-content:flex-end
     then anchored whatever few lines exist to the BOTTOM of that
     reserved (and, right after going live, much taller-than-needed) box,
     leaving a large empty gap directly under the metrics. flex-start
     keeps new lines appearing right where content actually is; any
     unused space ends up below the feed instead, which reads as normal
     empty room rather than a gap in the middle of the dashboard. */
  justify-content: flex-start;
  gap: 0.1em;
  padding-top: 0.2em;
  border-top: 1px solid rgb(255 255 255 / 8%);
}

.hero-chat__terminal-live-feed .hero-chat__terminal-live-line {
  margin: 0;
  font-size: clamp(5.2px, 6.4cqw, 6.2px);
  line-height: 1.4;
  color: var(--terminal-muted);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-word;
  opacity: 0;
  animation: terminal-line-arrival 260ms ease-out forwards;
}

@keyframes terminal-dot-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.55;
    transform: scale(0.82);
  }
}

@keyframes terminal-ready-fade {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes terminal-launch-flash {
  0% {
    border-color: rgb(255 91 111 / 72%);
    box-shadow: none;
  }
  35% {
    border-color: rgb(255 150 163 / 95%);
    box-shadow: 0 0 0 3px rgb(255 101 119 / 35%);
  }
  100% {
    border-color: rgb(255 91 111 / 72%);
    box-shadow: none;
  }
}

@keyframes terminal-confetti-fall {
  0% {
    opacity: 1;
    transform: translate3d(0, 0, 0) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate3d(var(--drift, 0), var(--fall, 100%), 0) rotate(var(--rotate, 180deg));
  }
}

@keyframes terminal-line-arrival {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes terminal-progress-refresh {
  from {
    opacity: 0.48;
    filter: brightness(1.7);
  }
  to {
    opacity: 1;
    filter: brightness(1);
  }
}

@keyframes terminal-cursor-blink {
  0%,
  52% {
    opacity: 1;
  }
  53%,
  100% {
    opacity: 0;
  }
}

@keyframes terminal-success-pulse {
  0%,
  100% {
    filter: brightness(1);
  }
  50% {
    filter: brightness(1.35);
  }
}

.hero-chat__terminal pre {
  margin: 0;
  font-size: clamp(6px, 8.5cqw, 8.5px);
  line-height: 1.46;
  letter-spacing: 0.005em;
  color: var(--terminal-fg);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-word;
}

/* MLOps pipeline log palette — violet for stage headers, pink for the
   model, yellow for epochs, cyan/teal for metrics, orange for AWS/
   Terraform, blue-violet for RAG, bright green for validations and
   confetti success lines, yellow for the few controlled warnings.
   Unscoped (not `.hero-chat__terminal .log-*`) on purpose so this palette
   actually wins the cascade here without needing `!important`; nothing
   else on the page uses these class names. */
.log-cmd {
  color: #f4f7ff;
  font-weight: 650;
  text-shadow: 0 0 4px rgb(255 255 255 / 18%);
}

.log-cmd::first-letter {
  color: var(--terminal-green);
}

.log-datetime,
.log-dim {
  color: var(--terminal-muted);
}

.log-stage {
  color: var(--terminal-violet);
  font-weight: 800;
  text-shadow: 0 0 5px rgb(185 148 255 / 40%);
}

.log-info {
  color: var(--terminal-blue);
}

.log-data {
  color: var(--terminal-cyan);
}

.log-model {
  color: var(--terminal-pink);
  text-shadow: 0 0 6px rgb(255 114 210 / 30%);
}

.log-epoch {
  color: var(--terminal-yellow);
  font-weight: 750;
  text-shadow: 0 0 5px rgb(255 209 102 / 28%);
}

.log-metric {
  color: #74f1ff;
  font-weight: 600;
}

.log-progress {
  color: #a9b8ff;
}

.log-rag {
  color: #9d8cff;
}

.log-aws {
  color: var(--terminal-orange);
}

.log-security {
  color: #ff8ca1;
}

.log-ok {
  color: var(--terminal-green);
  text-shadow: 0 0 5px rgb(99 245 154 / 25%);
}

.log-success {
  color: #86ffb0;
  font-weight: 900;
  letter-spacing: 0.025em;
  text-shadow:
    0 0 4px rgb(134 255 176 / 82%),
    0 0 11px rgb(48 255 128 / 45%),
    0 0 20px rgb(48 255 128 / 20%);
}

.log-warn {
  color: var(--terminal-yellow);
}

/* Kept available (per the pipeline content having no genuine failure —
   only controlled warnings, each followed by a successful resolution) but
   nothing in the current terminalLog block uses this class. */
.log-err {
  color: var(--terminal-red);
  text-shadow: 0 0 6px rgb(255 107 129 / 34%);
}

/* Evite une densité excessive sur les petits écrans sans toucher au cadrage. */
@media (max-width: 760px) {
  .hero-chat__terminal pre {
    line-height: 1.42;
  }

  /* The 10/8/10/13px padding calibrated for desktop eats a much bigger
     share of a phone's tiny terminal box (that box is a fixed percentage
     of the hero image, so it shrinks a lot more than the padding does) —
     cut it roughly in half to reclaim real reading width/height. The
     progress bar spans this same padded box's full width on every size
     (see .hero-chat__terminal-progress above), so no matching override is
     needed here any more. */
  .hero-chat__terminal {
    padding: 5px 4px 5px 7px;
  }
}

/* terminal-live.js itself already reads prefers-reduced-motion and reacts
   at the logic level — no per-character typing, much shorter pauses, no
   confetti call, transitionIn/Out durations passed as 0 — the experience
   keeps working end to end, just compressed. This is the CSS-only
   backstop for everything that's a pure decorative loop rather than
   something JS explicitly times: the ready screen's title/button fade-in,
   the live dashboard's status dot, and the panel fade transitions all
   collapse to instant/static. */
@media (prefers-reduced-motion: reduce) {
  .hero-chat__terminal-panel {
    transition-duration: 0.001ms !important;
  }

  .hero-chat__terminal-title,
  .hero-chat__terminal-launch {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .hero-chat__terminal-live-dot,
  .terminal-live-line,
  .terminal-live-line--milestone,
  .terminal-live-line--typing::after,
  .hero-chat__terminal-summary-final.is-in {
    animation: none !important;
  }

  .hero-chat__terminal-track > pre,
  .hero-chat__terminal-progress {
    transition: none !important;
  }
}

/* Avatar stage — the screen's permanent occupant, present from first paint
   (never toggled off). Box re-measured directly off the photo's pixels
   (luminance edge-detection, cross-checked at several rows/columns to rule
   out local artifacts). Image is 1672x941. All 4 edges pulled in by 1px
   from the raw measured transition (each was letting content land 1 real
   pixel past the true glass edge): left edge x=539px (32.237%), right edge
   x=1217px, top edge y=235px (24.973%), bottom edge x=623px (41.233% tall). */
.hero-chat__avatar-stage {
  position: absolute;
  top: 24.973%;
  left: 32.237%;
  width: 40.550%;
  height: 41.233%;
  overflow: hidden;
  border-radius: 2px;
  /* Transparent until the CRT reveal (.is-on, toggled by hero-avatar.js
     alongside the canvas's own is-ready, or by hero-chat.js's safety
     backstop on a load failure) — before that moment this whole screen is
     empty, not just the avatar itself. */
  background: transparent;
}

/* The sunset wallpaper backdrop lives on a ::before rather than directly
   on the stage's own background, so its appearance can be delayed to
   0.85s (once the vertical opening has actually started) instead of
   popping in the instant .is-on lands — a plain `background` swap can't
   be delayed/transitioned smoothly for a gradient, but opacity can.
   Placed first in paint order (a pseudo-element on the stage itself, the
   canvas is a later sibling in the DOM) so it sits behind the avatar,
   never over it. */
.hero-chat__avatar-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  /* Same sunset wallpaper as the IAcine avatar project's own index.css
     body background (linear-gradient(19deg, #FF7E5F, #feb47b, #6a3093)) —
     the canvas itself renders with a transparent clear, so this is what
     actually shows behind/around the avatar, exactly like the reference. */
  background: linear-gradient(19deg, #ff7e5f 0%, #feb47b 50%, #6a3093 100%);
  opacity: 0;
  transition: opacity 0.3s ease 0.85s;
}

.hero-chat__avatar-stage.is-on::before {
  opacity: 1;
}

/* The perspective floor grid used to be printed here as a flat, tilted
   CSS overlay — explicitly rejected ("le sol en quadrillage est juste
   print en dur... il doit vraiment être un sol sur lequel mon avatar est
   posé"): it never moved with the camera and went blank looking up from
   below. Replaced by a real Three.js floor plane in hero-avatar.js
   (createFloorGrid) that lives IN the scene, orbits/tilts with the
   camera exactly like the avatar itself, and renders correctly from any
   angle. Nothing left to style here. */

@media (prefers-reduced-motion: reduce) {
  .hero-chat__avatar-stage::before {
    transition: none;
  }
}

/* Positioned over the LEFT-hand monitor in /images/hero-desk.webp — real
   status/log messages (mic permission, document scan, errors — never a
   settled reply, that's spoken by the avatar and otherwise only ever
   visible by explicitly opening the discussion panel) live here
   exclusively; the right-hand screen (.hero-chat__avatar-stage) is
   avatar-only, nothing else ever renders there. Same box AND the same
   perspective/mask treatment as .hero-chat__terminal just below — they're
   the same physical screen area (idle decorative loop vs a real
   active/terminal operation), so the real text tilts in 3D exactly like
   the decorative terraform-style log it replaces while visible. */
.hero-chat__screen {
  position: absolute;
  top: 23.1%;
  left: 13.3%;
  width: 16.4%;
  height: 47.3%;
  background: transparent;
  border-radius: 2px;
  padding: 10px 8px 10px 13px;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
  perspective: 420px;
  perspective-origin: 50% 50%;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-weight: 500;
  font-size: clamp(8px, 0.75vw, 11px);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: #eef1f8;
  text-shadow: 0 0 14px rgba(120, 170, 255, 0.25);
  display: none;
}

.hero-chat__screen::-webkit-scrollbar {
  display: none;
}

.hero-chat__screen.is-active {
  display: block;
}

.hero-chat__screen-track {
  display: block;
  white-space: pre-wrap;
  /* Same tilt as .hero-chat__terminal-tilt — left edge closer to the
     viewer, right edge farther, matching the monitor's own turn in the
     photo. */
  transform: rotateY(10deg);
  transform-style: preserve-3d;
}

/* Avatar 3D — the right-hand screen's ONLY occupant, permanently (idles
   from first paint, never gated behind any operation, never shares its
   space with any text — every status/log message lives on the left
   screen instead, see .hero-chat__screen above). Starts fully collapsed
   behind a horizontal clip (a CRT tube's electron beam has nothing to
   show yet) and is only revealed once hero-avatar.js adds .is-ready —
   that happens after the GLB has loaded, the mixer has evaluated a first
   real Idle pose, and the very first renderer.render() call has already
   run, specifically so nobody ever sees a T-pose/A-pose flash while any
   of that is still in flight. The reveal is intentionally NOT an opacity
   fade: per explicit design direction this must read as a CRT power-on
   (point → horizontal line → vertical opening), so the image itself has
   to be exposed by a growing clip-path window, in lockstep with the
   decorative dot/line overlay below — a fondu would show the whole frame
   softly appearing everywhere at once, which is exactly the look this
   replaces. */
.hero-chat__avatar-canvas {
  display: block;
  width: 100%;
  height: 100%;
  clip-path: inset(50% 0 50% 0);
  /* Hints the browser to promote this to its own compositor layer ahead
     of time — a simple inset() clip-path is eligible for compositor-
     thread animation in modern engines, unlike an animated
     background-color, which stays on the main thread no matter what. */
  will-change: clip-path;
}

.hero-chat__avatar-canvas.is-ready {
  animation: hero-avatar-crt-reveal 1450ms linear forwards;
}

/* Once the reveal has genuinely finished, hero-avatar.js swaps is-ready
   for is-open (on the animation's own "animationend" event) and the
   canvas is pinned fully visible via a plain, non-animated clip-path
   instead of an animation hold. This matters because the canvas is later
   physically moved (appendChild, never cloned) into the expanded modal's
   own stage and back — a CSS animation with fill-mode:forwards can
   restart from its 0% keyframe when its element leaves and re-enters the
   render tree (e.g. a display:none ancestor toggling visible), which
   would replay the whole CRT boot sequence on every modal open. A static
   rule has no timeline to restart. */
.hero-chat__avatar-canvas.is-open {
  animation: none;
  clip-path: inset(0 0 0 0);
}

/* Holds fully collapsed through the dot/line-forming phases (0 → 58.6%
   = 0.85s), then opens vertically from the centerline out to full height
   over the next 350ms (→ 82.8% = 1.2s) — the same window the decorative
   line below finishes its own vertical stretch in, so the glowing edge
   and the actual reveal edge stay visually the same line. Holds open for
   the trailing flash/scanlines phase. linear (not eased) so it tracks
   the overlay's own linear keyframe stops exactly. */
@keyframes hero-avatar-crt-reveal {
  0%, 58.621% {
    clip-path: inset(50% 0 50% 0);
  }
  82.759% {
    clip-path: inset(0 0 0 0);
  }
  100% {
    clip-path: inset(0 0 0 0);
  }
}

/* CRT power-on overlay: transparent and static while the ~12MB avatar
   GLB fetches/decodes behind it — the stage's own sunset wallpaper is
   itself gated behind the same .is-on moment (see .hero-chat__avatar-stage
   below), so there's nothing but the screen's dark base underneath at
   rest. The moment .is-on is added (hero-avatar.js, right alongside the
   canvas's own is-ready — a direct class toggle rather than a CSS
   :has() selector: measured that :has()-driven animations can lag behind
   a class change until something else forces a style recalc, which in a
   genuinely idle tab could mean the animation never actually starts), it
   plays a single-shot sequence: a bright point ignites at the exact
   center, stretches into a horizontal line, that line opens vertically
   to fill the screen (synchronized with the canvas's own clip-path
   reveal above so the glow rides the growing edge), then a brief flash
   and a few scanlines settle before everything fades out, leaving the
   avatar on its own. A safety timeout in hero-chat.js also forces
   .is-on after a few seconds regardless of load success, so a failed
   GLB fetch/WebGL init can never leave the reveal stuck mid-way either —
   the avatar is already fully posed and rendered underneath before it
   starts on the happy path; nothing ever waits on this animation, it's
   purely decorative on top of an avatar already there. */
.hero-chat__avatar-crt {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

/* The center point: small, bright, softly blue-white halo. Only
   transform + opacity are animated (box-shadow is declared once, static,
   and simply carried along by the transform) — measured that animating a
   *changing* paint property like background-color or a live box-shadow
   forces a real repaint every frame, which the avatar's own WebGL render
   loop (already busy every frame with mixer/morph updates) starves
   badly: a nominally ~650ms animation was actually taking ~5s to finish
   in an earlier version built that way. */
.hero-chat__avatar-crt-dot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: #eef4ff;
  box-shadow: 0 0 10px 3px rgb(220 232 255 / 90%), 0 0 22px 8px rgb(190 210 255 / 45%);
  opacity: 0;
  transform: scale(0.5);
}

/* The line: a soft-edged bluish-white bar, born as a near-invisible
   sliver at the exact center of the dot's position, that first stretches
   horizontally (0.55–0.85s) then opens vertically from its own center
   (0.85–1.2s) — the same window the canvas's clip-path finishes opening
   in, so this glow always sits right on the growing edge of the reveal,
   never ahead of or behind it. */
.hero-chat__avatar-crt-line {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, #cfe0ff 12%, #fff 50%, #cfe0ff 88%, transparent 100%);
  box-shadow: 0 0 14px 3px rgb(210 225 255 / 70%);
  opacity: 0;
  transform: scale(0.02, 0.006);
}

/* Trailing micro-flash + scanlines (1.2–1.45s): a very brief, soft
   brightening and a few faint horizontal lines, both opacity-only so
   neither competes with the avatar's own render loop for paint time. No
   mix-blend-mode either — treated as the same compositor-safety risk as
   an animated background-color. */
.hero-chat__avatar-crt-flash {
  position: absolute;
  inset: 0;
  background: rgb(234 242 255 / 35%);
  opacity: 0;
}

.hero-chat__avatar-crt-scanlines {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(to bottom, rgb(255 255 255 / 12%) 0, rgb(255 255 255 / 12%) 1px, transparent 1px, transparent 3px);
  opacity: 0;
}

.hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-dot {
  animation: hero-avatar-crt-dot 1450ms linear forwards;
}

.hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-line {
  animation: hero-avatar-crt-line 1450ms linear forwards;
}

.hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-flash {
  animation: hero-avatar-crt-flash 1450ms linear forwards;
}

.hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-scanlines {
  animation: hero-avatar-crt-scanlines 1450ms linear forwards;
}

/* 0–0.35s: nothing. 0.35–0.55s: the point ignites and holds. ~0.55s: it
   hands off to the line and disappears (kept a hair past the line's own
   start so there's no visible gap between the two). */
@keyframes hero-avatar-crt-dot {
  0%, 24.138% {
    opacity: 0;
    transform: scale(0.5);
  }
  37.931% {
    opacity: 1;
    transform: scale(1);
  }
  40% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

/* Picks up right as the dot fades (~0.55s): a hairline sliver fades in,
   stretches horizontally to full width by 0.85s, then opens vertically
   to full height by 1.2s, holds briefly as the flash/scanlines phase
   runs, then fades out to leave the avatar clean. */
@keyframes hero-avatar-crt-line {
  0%, 37.931% {
    opacity: 0;
    transform: scale(0.02, 0.006);
  }
  40% {
    opacity: 1;
    transform: scale(0.02, 0.006);
  }
  58.621% {
    opacity: 1;
    transform: scale(1, 0.006);
  }
  82.759% {
    opacity: 1;
    transform: scale(1, 1);
  }
  88% {
    opacity: 0;
    transform: scale(1, 1);
  }
  100% {
    opacity: 0;
    transform: scale(1, 1);
  }
}

/* A soft, brief brightening right as the vertical opening finishes. */
@keyframes hero-avatar-crt-flash {
  0%, 82.759% {
    opacity: 0;
  }
  88% {
    opacity: 0.5;
  }
  100% {
    opacity: 0;
  }
}

/* A few scanlines settle in a touch after the flash, then fade. */
@keyframes hero-avatar-crt-scanlines {
  0%, 82.759% {
    opacity: 0;
  }
  86% {
    opacity: 0.5;
  }
  100% {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__avatar-canvas {
    clip-path: none;
  }

  .hero-chat__avatar-canvas.is-ready {
    animation: none;
  }

  .hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-dot,
  .hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-line,
  .hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-flash,
  .hero-chat__avatar-stage.is-on .hero-chat__avatar-crt-scanlines {
    animation: none;
    opacity: 0;
  }
}

/* Kept: reused by .hero-chat__screen-step-dot--active below — the
   dedicated .hero-chat__cursor (fake typewriter) and .hero-chat__loading
   (pre-screen-controller loading dots) rules that used to sit here were
   both genuinely dead (nothing references them any more — the typewriter
   was removed per §8's "ne simule pas un faux streaming", and the loading
   dots were replaced by the screen controller's operation states long
   before this pass) and have been removed rather than left as unused CSS. */
@keyframes hero-chat-loading-bounce {
  0%, 80%, 100% {
    opacity: 0.35;
    transform: scale(0.7);
  }
  40% {
    opacity: 1;
    transform: scale(1.1);
  }
}

/* ==========================================================================
   Unified screen states (addendum §1-§4) — every non-idle status the
   chatbot has (document scan, voice, model tier, memory, errors) renders
   here, through hero-screen-controller.js, replacing the old
   "loading dots only" behaviour. Same LLMOps-terminal language as the
   track's own base styles (JetBrains Mono, blue glow) — see .hero-chat__screen.
   ========================================================================== */

.hero-chat__sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.hero-chat__screen-state {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  animation: hero-screen-state-in 0.25s ease-out;
}

@keyframes hero-screen-state-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

.hero-chat__screen-icon {
  flex-shrink: 0;
  color: #78aaff;
  filter: drop-shadow(0 0 6px rgba(120, 170, 255, 0.5));
}

.hero-chat__screen-state--success .hero-chat__screen-icon {
  color: #3ddc84;
  filter: drop-shadow(0 0 6px rgba(61, 220, 132, 0.5));
}

.hero-chat__screen-state--error .hero-chat__screen-icon {
  color: #ff6b6b;
  filter: drop-shadow(0 0 6px rgba(255, 107, 107, 0.5));
}

.hero-chat__screen-state--warning .hero-chat__screen-icon {
  color: #ffc36b;
  filter: drop-shadow(0 0 6px rgba(255, 195, 107, 0.5));
}

/* Stabilization pass §8 — the "generation in progress" orbit: a ring, a
   pulsing center dot, and 3 (Normal) or 4 (Thinking, denser/more violet
   per the brief) segments circling it, plus a soft breathing halo behind
   the whole thing. Real, technical, LLMOps-terminal in spirit — never a
   childish spinner, never a percentage. */
.hero-chat__screen-icon--orbit {
  position: relative;
  width: 26px;
  height: 26px;
  color: #78aaff;
  filter: none;
}

.hero-chat__screen-icon--orbit::before {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(120, 170, 255, 0.35), transparent 70%);
  animation: hero-orbit-breathe 2.4s ease-in-out infinite;
}

.hero-chat__screen-icon--orbit-thinking::before {
  background: radial-gradient(circle, rgba(184, 107, 248, 0.4), transparent 70%);
}

.hero-chat__orbit-ring {
  position: relative;
  width: 26px;
  height: 26px;
  display: block;
  animation: hero-orbit-spin 2.6s linear infinite;
}

.hero-chat__screen-icon--orbit-thinking .hero-chat__orbit-ring {
  animation-duration: 1.9s; /* "un peu plus complexe" — a touch livelier for Thinking */
}

.hero-chat__orbit-track {
  stroke: rgba(120, 170, 255, 0.22);
}

.hero-chat__screen-icon--orbit-thinking .hero-chat__orbit-track {
  stroke: rgba(184, 107, 248, 0.25);
}

.hero-chat__orbit-core {
  fill: #78aaff;
  animation: hero-orbit-pulse 1.3s ease-in-out infinite;
}

.hero-chat__screen-icon--orbit-thinking .hero-chat__orbit-core {
  fill: #b86bf8;
}

.hero-chat__orbit-seg {
  fill: #78aaff;
  opacity: 0.85;
}

.hero-chat__screen-icon--orbit-thinking .hero-chat__orbit-seg {
  fill: #b86bf8;
}

.hero-chat__orbit-seg--2 { opacity: 0.6; }
.hero-chat__orbit-seg--3 { opacity: 0.35; }
.hero-chat__orbit-seg--4 { opacity: 0.2; }

/* A faint scanline sweeping through the detail text while generating —
   subtle, technical, never obscuring the actual phase label. */
.hero-chat__screen-state--generating .hero-chat__screen-detail {
  position: relative;
  overflow: hidden;
}

.hero-chat__screen-state--generating .hero-chat__screen-detail::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(120, 170, 255, 0.18), transparent);
  animation: hero-orbit-scanline 2.2s ease-in-out infinite;
}

@keyframes hero-orbit-spin {
  to { transform: rotate(360deg); }
}

@keyframes hero-orbit-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.6; transform: scale(0.82); }
}

@keyframes hero-orbit-breathe {
  0%, 100% { opacity: 0.5; transform: scale(0.92); }
  50% { opacity: 1; transform: scale(1.08); }
}

@keyframes hero-orbit-scanline {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__screen-icon--orbit::before,
  .hero-chat__orbit-ring,
  .hero-chat__orbit-core,
  .hero-chat__screen-state--generating .hero-chat__screen-detail::after {
    animation: none;
  }
}

/* Stabilization pass §8 — the real, complete reply, revealed as one
   honest block (a quick fade+settle, not a fake per-character typing
   animation — see hero-screen-controller.js's renderReply()). */
.hero-chat__reply-text {
  margin: 0;
  animation: hero-reply-in 0.35s ease-out;
}

@keyframes hero-reply-in {
  from { opacity: 0; transform: translateY(3px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__reply-text {
    animation: none;
  }
}

.hero-chat__screen-text {
  min-width: 0;
  flex: 1;
}

/* Explicit dismiss control for a terminal state (addendum: "être
   fermables") — never rely on the TTL alone. */
.hero-chat__screen-close {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-left: auto;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: rgba(238, 241, 248, 0.5);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.hero-chat__screen-close:hover {
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}

.hero-chat__screen-close:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 1px;
}

.hero-chat__screen-title {
  margin: 0;
  font-weight: 700;
  font-size: 1em;
  color: #eef1f8;
}

.hero-chat__screen-state--success .hero-chat__screen-title {
  color: #a8f0c6;
}

.hero-chat__screen-state--error .hero-chat__screen-title {
  color: #ffb3b3;
}

.hero-chat__screen-detail {
  margin: 3px 0 0;
  font-size: 0.82em;
  color: rgba(238, 241, 248, 0.6);
  text-shadow: none;
}

.hero-chat__screen-steps {
  display: flex;
  gap: 5px;
  margin-top: 10px;
  padding-left: 30px; /* aligns under the title, past the icon column */
}

.hero-chat__screen-step-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(120, 170, 255, 0.18);
  transition: background 0.25s ease, transform 0.2s ease;
}

.hero-chat__screen-step-dot--done {
  background: #78aaff;
}

.hero-chat__screen-step-dot--active {
  background: #78aaff;
  transform: scale(1.4);
  animation: hero-chat-loading-bounce 1s ease-in-out infinite;
}

.hero-chat__screen-step-dot--rejected {
  background: #ff6b6b;
}

/* Compact reactive waveform for the "voiceRecording" state — a lighter
   variant of the composer's own recorder waveform (hero-chat-audio.js
   drives both from the same analyser sample loop). */
.hero-chat__screen-waveform {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 20px;
  margin-top: 10px;
  padding-left: 30px;
}

.hero-chat__screen-waveform span {
  width: 2.5px;
  min-height: 2px;
  border-radius: 2px;
  background: #78aaff;
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__screen-state {
    animation: none;
  }
  .hero-chat__screen-step-dot--active {
    animation: none;
  }
}

.hero-chat__turnstile {
  display: flex;
  justify-content: center;
}

/* ==========================================================================
   Single-bar composer (redesign brief §4-§7) — one glass surface holding
   [+] [ textarea ] [tier ˅] [mic] [send]. Replaces the old separate
   input-row + floating send button: everything below lives inside ONE
   bordered/shadowed container so there's never "a rectangle inside a
   rectangle" (§4: "pas de rectangle dans un rectangle inutile").
   ========================================================================== */

/* Real bug found via testing: the composer's own cramped-vs-comfortable
   layout was previously decided by a VIEWPORT media query — but the
   composer's actual available width is the CARD's width, not the
   viewport's. Narrowing the browser window itself (not just viewing on
   a real mobile device) shrinks the card while the viewport can still be
   "desktop-sized," so a viewport query never fired and the single-row
   layout broke: the placeholder wrapped to 2 lines, a vertical scrollbar
   appeared, the "0/200" counter overlapped the text. `.hero-chat__stack`
   is the one wrapper that's ALWAYS the composer's immediate layout
   ancestor — physically moved as a whole between the compact card and
   the expanded modal's composer slot (hero-chat-expanded.js's
   moveStack()) — so declaring the query container here, once, tracks
   the composer's real available width in both places automatically. */
.hero-chat__stack {
  container-type: inline-size;
}

/* Purely visual stand-in left behind in the compact card while the real
   stack is physically in the expanded modal (hero-chat-expanded.js) —
   deliberately styled identically via the same shared classes (that's the
   whole point: the card must not visibly change), `inert` already blocks
   all interaction/focus/selection natively; pointer-events is set here
   too only as a defensive no-op against a stray hover state in the brief
   instant before the overlay's own scrim covers it. */
.hero-chat__stack--ghost {
  pointer-events: none;
}

/* Redesign: a "sunken" surface inside the big card — darker and much less
   violet at rest than before (the old version kept a permanent violet
   wash across the whole bar); violet only ever appears on focus. */
.hero-chat__composer {
  display: flex;
  flex-direction: column;
  background: rgba(16, 14, 25, 0.68);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 19px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025), 0 8px 24px rgba(0, 0, 0, 0.16);
  padding: 6px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.hero-chat__composer:focus-within {
  border-color: rgba(139, 103, 246, 0.62);
  box-shadow: 0 0 0 3px rgba(139, 103, 246, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.04);
}

.hero-chat__composer-row {
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: 2px;
  min-height: 44px;
}

/* Cooldown countdown badge (hero-chat.js's startCooldown) — floats above
   the send button rather than living inside it (see index.njk's own
   comment on why: writing it into the button's internal span already
   broke that button's icon once). Same small pill language as the
   "thinking" ring's own number (hero-chat-expanded.css), just simpler:
   no ring needed at this size, the number alone reads fine. */
.hero-chat__cooldown-badge {
  position: absolute;
  bottom: calc(100% + 8px);
  right: 0;
  padding: 4px 10px;
  border-radius: 9999px;
  background: linear-gradient(135deg, rgba(184, 107, 248, 0.9), rgba(63, 123, 248, 0.9));
  color: #fff;
  font-family: var(--font-satoshi);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  box-shadow: 0 4px 14px rgba(63, 123, 248, 0.35);
  pointer-events: none;
}

/* Round icon buttons: +, mic, send — 44px is the accessible minimum tap
   target (§5). Transparent at rest, violet on hover, no nested rectangle
   border like the old .hero-chat__icon-btn had. */
.hero-chat__composer-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  padding: 0;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: rgba(238, 241, 248, 0.72);
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, transform 0.15s ease;
}

.hero-chat__composer-btn svg {
  width: 19px;
  height: 19px;
}

.hero-chat__composer-btn:hover:not(:disabled) {
  background: rgba(184, 107, 248, 0.16);
  color: #d9b8ff;
}

.hero-chat__composer-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

.hero-chat__composer-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.hero-chat__composer-btn--add.is-active {
  background: rgba(184, 107, 248, 0.22);
  color: #d9b8ff;
}

.hero-chat__composer-btn--mic.is-recording {
  background: rgba(255, 95, 86, 0.18);
  color: #ff8f86;
  animation: hero-chat-mic-pulse 1.4s ease-in-out infinite;
}

@keyframes hero-chat-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 95, 86, 0.35); }
  50% { box-shadow: 0 0 0 6px rgba(255, 95, 86, 0); }
}

/* The send button keeps the site's signature blue→violet gradient — the
   one deliberately "filled" control in the row, everything else stays
   transparent-at-rest (§5). */
.hero-chat__composer-btn--send {
  position: relative;
  background: linear-gradient(270.04deg, #3f7bf8 0.04%, #b86bf8 99.96%);
  color: #fff;
}

.hero-chat__composer-btn--send:hover:not(:disabled) {
  opacity: 0.88;
  background: linear-gradient(270.04deg, #3f7bf8 0.04%, #b86bf8 99.96%);
  color: #fff;
}

.hero-chat__composer-btn--send svg {
  width: 16px;
  height: 16px;
  transform: translate(-1px, 1px);
  transition: opacity 0.15s ease;
}

.hero-chat__composer-btn--send:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* While a request is genuinely in flight — the airplane hides and a real
   elapsed-time readout (driven by performance.now() in hero-chat.js, own
   dedicated element — never the same span the §5 bug-fix follow-up
   removed, and never sharing storage with the 0/200 counter) takes its
   place, plus a gentle opacity breathing on the button itself as the
   "active" cue. No spinner ring, no fake percentage, no fake ETA. */
.hero-chat__composer-btn--send.is-sending svg {
  opacity: 0;
}

.hero-chat__send-timer {
  display: none;
  font-family: var(--font-satoshi);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  line-height: 1;
  color: #fff;
}

.hero-chat__composer-btn--send.is-sending .hero-chat__send-timer {
  display: inline-flex;
}

.hero-chat__composer-btn--send.is-sending {
  animation: hero-chat-send-breathe 1.6s ease-in-out infinite;
}

@keyframes hero-chat-send-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.72; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__composer-btn--send.is-sending {
    animation: none;
  }
}

.hero-chat__composer-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
}

/* Auto-growing 1→4/5 lines (§6) — hero-chat.js resizes `height` on input;
   max-height caps it at ~5 lines so a very long paste doesn't push the
   tier/mic/send row off-surface. No visible border of its own: the
   composer's own border is the only edge (§4). */
.hero-chat__composer-input {
  width: 100%;
  background: transparent;
  border: none;
  color: #fff;
  font-family: var(--font-satoshi);
  font-size: 14px;
  line-height: 1.45;
  resize: none;
  padding: 12px 8px;
  min-height: 44px;
  max-height: 136px;
  overflow-y: auto;
}

.hero-chat__composer-input:focus {
  outline: none;
}

.hero-chat__composer-input::placeholder {
  color: rgba(238, 241, 248, 0.4);
}

/* Simplification chantier §1 — always visible (0/200 at rest, discreet
   gray), bottom-right of the textarea zone, never inside the send button.
   Shifts amber near the limit, then red/pink right at it — a real,
   immediate visual signal, not just the browser's own maxlength cutoff. */
.hero-chat__composer-counter {
  position: absolute;
  right: 4px;
  bottom: 2px;
  font-family: monospace;
  font-size: 10px;
  color: rgba(255, 255, 255, 0.38);
  pointer-events: none;
  background: rgba(20, 16, 30, 0.55);
  padding: 1px 5px;
  border-radius: 6px;
  transition: color 0.15s ease;
}

.hero-chat__composer-counter.is-near-limit {
  color: #ffc36b;
}

.hero-chat__composer-counter.is-at-limit {
  color: #ff6b9c;
}

/* Tier selector (§9/§15) — compact, transparent, same 44px row height as
   its neighbours, a pill rather than a circle since it carries a label. */
.hero-chat__mode {
  position: relative;
  flex-shrink: 0;
  align-self: center;
}

.hero-chat__mode[hidden] {
  display: none;
}

.hero-chat__mode-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 32px;
  margin: 0 2px;
  padding: 0 10px;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03);
  color: rgba(238, 241, 248, 0.72);
  font-family: var(--font-satoshi);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}

.hero-chat__mode-btn:hover:not(:disabled) {
  border-color: rgba(184, 107, 248, 0.45);
  color: #d9b8ff;
}

.hero-chat__mode-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

.hero-chat__mode-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.hero-chat__mode-btn[aria-expanded="true"] {
  border-color: #b86bf8;
  color: #d9b8ff;
  background: rgba(184, 107, 248, 0.12);
}

/* The Normal/Thinking label is never hidden by width any more. It used to
   be (two separate, disagreeing viewport-width media queries — one in
   this file, one in hero-chat-expanded.css — because the composer's real
   width differs a lot between the compact card and the expanded modal at
   the same viewport width, so a viewport breakpoint structurally
   couldn't get both contexts right at once). But the actual fix for
   narrow widths is the two-row composer layout a bit further down
   (`@container (max-width: 560px)`, already covers every real mobile
   width): once the mode button moves to its own grid cell there
   (`grid-template-columns: 44px 1fr auto 44px 44px`, the button sized
   `auto` = exactly its own content), it's never actually competing with
   the textarea for space and needs no separate icon-only fallback. */

.hero-chat__mode-btn svg {
  flex-shrink: 0;
  transition: transform 0.18s ease;
}

.hero-chat__mode-btn[aria-expanded="true"] svg {
  transform: rotate(180deg);
}

/* Opaque dark-glass popover, anchored above the bar so it never gets
   clipped by the composer's own rounded corners. */
.hero-chat__mode-popover {
  position: absolute;
  bottom: calc(100% + 10px);
  right: 0;
  width: 272px;
  max-width: calc(100vw - 40px);
  background: #1b1227;
  border: 1px solid rgba(184, 107, 248, 0.28);
  border-radius: 14px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.48);
  padding: 6px;
  z-index: 20;
}

.hero-chat__mode-popover[hidden] {
  display: none;
}

.hero-chat__mode-option {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease;
}

.hero-chat__mode-option:hover:not(:disabled) {
  background: rgba(184, 107, 248, 0.14);
}

.hero-chat__mode-option:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: -2px;
}

.hero-chat__mode-option:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.hero-chat__mode-option-check {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  color: #b86bf8;
  visibility: hidden;
}

.hero-chat__mode-option[aria-selected="true"] .hero-chat__mode-option-check {
  visibility: visible;
}

.hero-chat__mode-option-body {
  min-width: 0;
}

.hero-chat__mode-option-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-satoshi);
  font-size: 13px;
  font-weight: 700;
  color: #eef1f8;
}

.hero-chat__mode-option-badge {
  font-family: var(--font-satoshi);
  font-size: 9.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: #b8f0d6;
  background: rgba(61, 220, 132, 0.16);
  border-radius: 999px;
  padding: 2px 6px;
}

.hero-chat__mode-option-desc {
  margin: 2px 0 0;
  font-family: var(--font-satoshi);
  font-size: 11.5px;
  line-height: 1.35;
  color: rgba(238, 241, 248, 0.6);
}

.hero-chat__mode-option-unavailable {
  margin: 2px 0 0;
  font-family: var(--font-satoshi);
  font-size: 11px;
  font-weight: 600;
  color: #ffb3b3;
}

@media (max-width: 480px) {
  /* Popover would otherwise overflow a narrow phone anchored purely by
     `right: 0` on a button near the composer's own right edge. */
  .hero-chat__mode-popover {
    right: auto;
    left: 50%;
    transform: translateX(-50%);
  }
}

/* Container query, not a viewport one (see .hero-chat__stack above) — this
   fires whenever the composer's own available width is tight, whether
   that's a real mobile viewport OR a desktop browser window narrowed down
   with the card still sitting in an otherwise "desktop" layout. 560px is
   deliberately generous: comfortably above the ~380-420px point where the
   single-row layout actually starts fighting for space, so the switch
   happens before anything visibly breaks, not after. */
@container (max-width: 560px) {
  /* Full ChatGPT-style two-row composer (real redesign, not the
     previous single-row squeeze that crushed the textarea to ~28px wide
     and forced the mode label, counter and placeholder to fight for the
     same few pixels): the text field gets its own full-width row with
     real breathing room, and every control (+, mode, mic, send) moves to
     a second row underneath. CSS-only — grid-template-areas remap the
     exact same DOM children used at every other width, one physical
     composer throughout, nothing duplicated. */
  .hero-chat__composer-row {
    display: grid;
    grid-template-columns: 44px 1fr auto 44px 44px;
    grid-template-areas:
      "input input input input input"
      "add   .     mode  mic   send";
    align-items: center;
    row-gap: 4px;
    column-gap: 2px;
  }

  .hero-chat__doc-menu-wrap {
    grid-area: add;
  }

  .hero-chat__composer-input-wrap {
    grid-area: input;
  }

  .hero-chat__mode {
    grid-area: mode;
  }

  .hero-chat__composer-btn--mic {
    grid-area: mic;
  }

  .hero-chat__composer-btn--send {
    grid-area: send;
  }

  .hero-chat__mode-btn {
    padding: 0 6px;
    /* 32px everywhere else is fine for a mouse pointer, but this is a
       real touch target on mobile — the accessible 44px minimum applies
       here too, not just to the round icon buttons either side of it. */
    min-height: 44px;
  }

  /* Real bug found via the expanded modal's own responsive testing at
     320-360px: the modal's near-fullscreen chrome leaves the composer
     noticeably narrower there than the compact card ever gets (measured
     ~104px vs ~286px available for the same textarea), so even the
     already-shortened placeholderMobile string wraps onto a second line
     that lands directly under the always-visible "0/200" counter,
     visually reading as overlapping controls. Reserved bottom padding
     keeps wrapped placeholder/typed text clear of the counter's row
     regardless of how many lines it wraps to, in both compact and
     expanded contexts (same physical element, one rule). */
  .hero-chat__composer-input {
    padding-bottom: 20px;
  }
}

/* "+" document-purpose menu — same dark-glass popover language as the
   mode selector, anchored to the left edge instead (the add button is
   the leftmost control). */
.hero-chat__doc-menu-wrap {
  position: relative;
  flex-shrink: 0;
}

.hero-chat__doc-menu {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 0;
  width: 240px;
  max-width: calc(100vw - 40px);
  background: #1b1227;
  border: 1px solid rgba(184, 107, 248, 0.28);
  border-radius: 14px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.48);
  padding: 6px;
  z-index: 20;
}

.hero-chat__doc-menu[hidden] {
  display: none;
}

.hero-chat__doc-menu-item {
  display: block;
  width: 100%;
  padding: 9px 10px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: #eef1f8;
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease;
}

.hero-chat__doc-menu-item:hover {
  background: rgba(184, 107, 248, 0.14);
}

.hero-chat__doc-menu-item:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: -2px;
}

/* Short disclaimer at the actual moment of choosing what to upload —
   below the 4 purpose buttons, inside the same popover. */
.hero-chat__doc-menu-hint {
  margin: 4px 4px 2px;
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-family: var(--font-satoshi);
  font-size: 10.5px;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.4);
}

@media (max-width: 480px) {
  .hero-chat__doc-menu {
    left: -6px;
  }
}

/* Discreet secondary line under the footer links (simplification chantier
   §4/§7) — "Profil · Missions · Projets IA · Documents". Small, muted,
   never a second heavy block on the card. */
.hero-chat__helper-line {
  margin: 2px 0 0;
  font-family: var(--font-satoshi);
  font-size: 10.5px;
  color: rgba(255, 255, 255, 0.32);
  text-align: center;
}

.hero-chat__error {
  font-family: var(--font-satoshi);
  font-size: 13px;
  color: #ff8a8a;
  display: none;
}

.hero-chat__error.is-visible {
  display: block;
}

/* Non-blocking notices (e.g. a tier fallback, §14 "tierFallback must be
   visible") — same slot as a real error, but amber rather than red so it
   doesn't read as something having failed outright. */
.hero-chat__error--warning {
  color: #ffc36b;
}

/* ==========================================================================
   Chatbot v2 — document chips + security-scan animation (§4/§6)
   ========================================================================== */

.hero-chat__documents {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.hero-chat__documents[hidden] {
  display: none;
}

/* A plain, compact chip once a document has fully cleared the pipeline —
   no ongoing animation, just name/format/size/state + a remove button. */
.hero-chat__doc-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.85);
}

.hero-chat__doc-chip.is-ready {
  border-color: rgba(61, 220, 132, 0.35);
}

.hero-chat__doc-chip.is-rejected {
  border-color: rgba(255, 95, 86, 0.45);
  background: rgba(255, 95, 86, 0.08);
}

.hero-chat__doc-chip-icon {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  color: rgba(255, 255, 255, 0.6);
}

.hero-chat__doc-chip.is-ready .hero-chat__doc-chip-icon {
  color: #3ddc84;
}

.hero-chat__doc-chip.is-rejected .hero-chat__doc-chip-icon {
  color: #ff5f56;
}

.hero-chat__doc-chip-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hero-chat__doc-chip-meta {
  color: rgba(255, 255, 255, 0.45);
  font-size: 11px;
  flex-shrink: 0;
}

.hero-chat__doc-chip-remove {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: rgba(255, 255, 255, 0.5);
  cursor: pointer;
}

/* §13 — visible glyph stays small (unchanged DA), but the real tap target
   is widened to the 44px accessibility floor via an invisible hit-area
   overlay rather than growing the icon itself. */
.hero-chat__doc-chip-remove::before {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: max(44px, 100%);
  height: max(44px, 100%);
  transform: translate(-50%, -50%);
}

.hero-chat__doc-chip-remove:hover {
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
}

.hero-chat__doc-chip-remove:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 1px;
}

/* Rejected-document action button ("Choisir un autre fichier") — the real,
   clickable action that lives in the actual interface (this strip), while
   the monitor screen (hero-screen-controller.js) shows the corresponding
   state — per the addendum's "les boutons d'action réels vivent dans la
   vraie interface, pendant que l'écran affiche l'état". The step-by-step
   scan animation itself now renders entirely on the screen, not here
   anymore (addendum §1/§4-§6) — this strip only ever holds the two settled
   end states (ready chip / rejected row). */
.hero-chat__scan-action-btn {
  position: relative;
  font-family: var(--font-satoshi);
  font-size: 11.5px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: transparent;
  color: #fff;
  cursor: pointer;
}

/* §13 — same invisible hit-area widening as .hero-chat__doc-chip-remove
   above, applied here since this pill (used by both the rejected-document
   retry action and the new mic-error retry button) is visually shorter
   than 44px by design. */
.hero-chat__scan-action-btn::before {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: max(44px, 100%);
  height: 44px;
  transform: translate(-50%, -50%);
}

.hero-chat__scan-action-btn:hover {
  background: rgba(255, 255, 255, 0.08);
}

.hero-chat__scan-action-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 1px;
}

/* Citations under an assistant reply that used a document (§10). */
.hero-chat__citations {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.hero-chat__citation {
  font-family: var(--font-satoshi);
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 999px;
  background: rgba(120, 170, 255, 0.12);
  border: 1px solid rgba(120, 170, 255, 0.3);
  color: #bcd3ff;
}

/* ==========================================================================
   Chatbot v2 — voice recording + voice message bubble (§11/§12/§13)
   ========================================================================== */

.hero-chat__voice-recorder {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 12px;
  background: rgba(255, 95, 86, 0.08);
  border: 1px solid rgba(255, 95, 86, 0.3);
}

.hero-chat__voice-recorder[hidden] {
  display: none;
}

/* One-shot recorder only (data-chat-voice-recorder) — the realtime call
   overlay (data-chat-voice-live) has no matching element, so this simply
   never renders there. flex-wrap above lets it drop to its own full-width
   line instead of squeezing the dot/waveform/timer/actions row. */
.hero-chat__voice-hint {
  flex-basis: 100%;
  font-family: var(--font-satoshi);
  font-size: 10.5px;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.45);
}

/* Stabilization pass §9 — mic error recovery row: same warm-amber
   language as a document rejection chip, real Retry/Close buttons living
   here in the real interface (status itself is on the monitor screen). */
.hero-chat__mic-error {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  padding: 8px 10px;
  border-radius: 10px;
  background: rgba(255, 195, 107, 0.08);
  border: 1px solid rgba(255, 195, 107, 0.3);
  font-family: var(--font-satoshi);
  font-size: 12px;
  color: rgba(255, 214, 148, 0.92);
}

.hero-chat__mic-error[hidden] {
  display: none;
}

.hero-chat__mic-error-text {
  flex: 1;
  min-width: 0;
}

.hero-chat__voice-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #ff5f56;
  flex-shrink: 0;
  animation: hero-chat-mic-pulse 1.2s ease-in-out infinite;
}

.hero-chat__voice-waveform {
  flex: 1;
  height: 24px;
  display: flex;
  align-items: center;
  gap: 2px;
  overflow: hidden;
}

.hero-chat__voice-bar {
  width: 3px;
  min-height: 3px;
  border-radius: 2px;
  background: #ff9d97;
  transition: height 0.1s ease;
}

.hero-chat__voice-timer {
  font-family: monospace;
  font-size: 12px;
  color: #fff;
  flex-shrink: 0;
}

.hero-chat__voice-actions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.hero-chat__voice-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  cursor: pointer;
}

.hero-chat__voice-btn svg {
  width: 15px;
  height: 15px;
}

.hero-chat__voice-btn--cancel {
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
}

.hero-chat__voice-btn--send {
  background: linear-gradient(270.04deg, #3f7bf8 0.04%, #b86bf8 99.96%);
  color: #fff;
}

.hero-chat__voice-btn--mute {
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
}

/* Muted reads as a warning state (same coral already used for the launch
   button/errors elsewhere on this screen), not a neutral toggle — it's
   the one state where the live session is connected but NOT listening,
   worth being visually distinct from "everything's fine". */
.hero-chat__voice-btn--mute.is-muted {
  background: rgb(255 91 111 / 16%);
  color: #ff6577;
}

.hero-chat__voice-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__voice-dot {
    animation: none !important;
  }
}

/* ==========================================================================
   Chatbot v2 — consent banner (§15) + data/memory settings (§26)
   ========================================================================== */

.hero-chat__consent {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(184, 107, 248, 0.3);
}

.hero-chat__consent[hidden] {
  display: none;
}

.hero-chat__consent-text {
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.85);
  margin: 0;
}

.hero-chat__consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.hero-chat__consent-btn {
  font-family: var(--font-satoshi);
  font-size: 12px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: transparent;
  color: #fff;
  cursor: pointer;
}

.hero-chat__consent-btn--primary {
  border: none;
  background: linear-gradient(270.04deg, #3f7bf8 0.04%, #b86bf8 99.96%);
}

.hero-chat__consent-btn:hover {
  opacity: 0.85;
}

.hero-chat__consent-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

/* Settings/history entry point — a single gear icon anchored to the
   card's own top-right corner. */
.hero-chat__settings-anchor {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 5;
}

.hero-chat__settings-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: rgba(255, 255, 255, 0.6);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}

.hero-chat__settings-btn:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.09);
  border-color: rgba(184, 107, 248, 0.35);
}

.hero-chat__settings-btn:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

.hero-chat__settings-btn svg {
  width: 18px;
  height: 18px;
}

/* Custom tooltip for the gear button — same hover/focus-within pattern
   already proven on the "i" info bubble above (.hero-chat__info-tip), so
   this reads as one consistent piece of UI language rather than a second
   tooltip mechanism. Deliberately replaces the browser's own native
   `title` tooltip (removed from the button markup) — a plain OS tooltip
   box next to this custom one would look like two different products. */
.hero-chat__settings-tip {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: max-content;
  max-width: 200px;
  padding: 7px 11px;
  border-radius: 10px;
  background: rgba(18, 16, 28, 0.97);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
  font-family: var(--font-satoshi);
  font-size: 12px;
  color: rgba(238, 241, 248, 0.9);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
  pointer-events: none;
  z-index: 6;
}

.hero-chat__settings-anchor:hover .hero-chat__settings-tip,
.hero-chat__settings-anchor:focus-within .hero-chat__settings-tip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* Hidden the instant the panel itself opens — a tooltip lingering next to
   an already-open panel is redundant and can visually clash with it. */
.hero-chat__settings-btn[aria-expanded="true"] ~ .hero-chat__settings-tip {
  opacity: 0;
  visibility: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__settings-tip {
    transition: none;
  }
}

/* ==========================================================================
   Settings/history panel — full redesign: progressive disclosure over one
   compact card instead of a single long scrolling page. The main view is
   five one-line rows + a discreet privacy line + a discreet danger link;
   clicking a row swaps this SAME panel's body into a sub-view
   (memories/documents/conversations/privacy/danger) with a back arrow,
   never a second modal — see hero-chat-settings.js for the view-switching
   logic. Anchored on desktop (not a full-screen modal); a genuine bottom
   sheet on mobile / when opened from the expanded conversation modal
   (reparented to <body>, see the `body > .hero-chat__panel` rules below).
   ========================================================================== */
.hero-chat__panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 8px);
  right: 0;
  display: flex;
  flex-direction: column;
  /* "environ 390–420px" — a single row's own content (a full-length
     document name, or "Afficher mon prénom" + its toggle) needs this
     much room to never wrap into a cramped multi-line row. */
  width: min(404px, calc(100vw - 32px));
  /* Height is automatic — the main view's five rows + privacy line +
     danger link comfortably fit well under this ceiling with no scroll;
     it only ever engages for a sub-view with a genuinely long list. */
  max-height: min(524px, calc(100vh - 120px));
  padding: 0;
  border-radius: 16px;
  background: #1b1227;
  border: 1px solid rgba(184, 107, 248, 0.3);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 200ms ease, transform 200ms ease;
}

.hero-chat__panel[hidden] {
  display: none;
}

.hero-chat__panel.is-open {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__panel {
    transition: none;
  }
}

/* --- Header (fixed — never scrolls, never re-animates on a view switch,
   only its own text/back-button content is swapped) --------------------- */
.hero-chat__panel-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 14px 14px 14px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  background: #201530; /* slightly lighter than the body — an opaque, visually distinct "aubergine" header band */
}

.hero-chat__panel-header-text {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.hero-chat__panel-header-titles {
  min-width: 0;
}

.hero-chat__panel-back {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-left: -6px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: rgba(255, 255, 255, 0.75);
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease;
}

.hero-chat__panel-back:hover {
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
}

.hero-chat__panel-back:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

.hero-chat__panel-title {
  font-family: var(--font-satoshi);
  font-size: 14px;
  font-weight: 700;
  color: #fff;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hero-chat__panel-subtitle {
  font-family: var(--font-satoshi);
  font-size: 11.5px;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.5);
  margin: 3px 0 0;
}

.hero-chat__panel-header-actions {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

.hero-chat__panel-header-action {
  font-family: var(--font-satoshi);
  font-size: 11.5px;
  font-weight: 700;
  color: #cda6ff;
  background: rgba(184, 107, 248, 0.12);
  border: 1px solid rgba(184, 107, 248, 0.3);
  border-radius: 999px;
  padding: 5px 10px;
  cursor: pointer;
  white-space: nowrap;
}

.hero-chat__panel-header-action:hover {
  color: #fff;
  background: rgba(184, 107, 248, 0.22);
}

.hero-chat__panel-header-action:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 1px;
}

/* Visual size stays a compact 30px circle (matching the header's own
   density), but the real tap target is expanded to 44×44 via an
   invisible centered hit area — same "hit-slop" technique used below on
   every other small control in this panel (toggle, back button, small
   text buttons, danger link) — never shrinking the visible design to
   hit that number, never leaving a genuinely sub-44px tap target either. */
.hero-chat__panel-close {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: transparent;
  color: rgba(255, 255, 255, 0.75);
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}

.hero-chat__panel-close::before,
.hero-chat__panel-back::before,
.hero-chat__panel-toggle::before,
.hero-chat__panel-btn-sm::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.hero-chat__panel-close:hover {
  background: rgba(184, 107, 248, 0.16);
  color: #d9b8ff;
  border-color: rgba(184, 107, 248, 0.5);
}

.hero-chat__panel-close:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

/* --- Body: the one part that swaps content (and animates) per view ----- */
.hero-chat__panel-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 14px 14px;
  scrollbar-width: thin;
  scrollbar-color: rgba(184, 107, 248, 0.4) transparent;
}

.hero-chat__panel-body::-webkit-scrollbar {
  width: 6px;
}

.hero-chat__panel-body::-webkit-scrollbar-thumb {
  background: rgba(184, 107, 248, 0.4);
  border-radius: 999px;
}

/* A sub-view's own list gets its scroll bounded independently of the
   panel's overall max-height — "scroll interne uniquement dans une liste
   détaillée, jamais sur toute la vue principale". */
.hero-chat__panel-list--scroll {
  max-height: min(392px, calc(100vh - 220px));
  overflow-y: auto;
}

/* View-switch animation — a light horizontal slide + fade, direction
   matching forward (opening a sub-view) vs. back (returning to main).
   Both keyframes fully define opacity + transform at both ends so
   nothing is left dangling once the animation completes. */
@keyframes hero-chat-panel-view-out-forward {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0; transform: translateX(-14px); }
}
@keyframes hero-chat-panel-view-in-forward {
  from { opacity: 0; transform: translateX(14px); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes hero-chat-panel-view-out-back {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0; transform: translateX(14px); }
}
@keyframes hero-chat-panel-view-in-back {
  from { opacity: 0; transform: translateX(-14px); }
  to { opacity: 1; transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__panel-body {
    animation: none !important;
  }
}

/* --- Rows — one list, one border, one row height (§ "un seul encadré
   extérieur... pas d'empilement de gros encadrés imbriqués") ------------ */
.hero-chat__panel-list {
  display: flex;
  flex-direction: column;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.02);
}

.hero-chat__panel-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 58px;
  padding: 9px 14px;
  border: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  background: transparent;
  font-family: var(--font-satoshi);
  font-size: 13px;
  color: #fff;
  text-align: left;
  width: 100%;
}

.hero-chat__panel-list > .hero-chat__panel-row:last-child {
  border-bottom: none;
}

.hero-chat__panel-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.hero-chat__panel-row-label {
  color: #fff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hero-chat__panel-row-status {
  font-size: 11.5px;
  color: #b6a8d6; /* gray-violet secondary status, per spec */
}

/* One-line disclaimer under the memory toggle — the moment memory
   actually gets activated, not buried in a separate confidentiality
   sub-view the visitor may never open. */
.hero-chat__panel-row-hint {
  margin: -6px 0 4px;
  padding: 0 2px;
  font-family: var(--font-satoshi);
  font-size: 11px;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.4);
}

/* Nav rows (Souvenirs/Documents/Conversations) are real <button>s so the
   whole row is clickable, not just the chevron. */
.hero-chat__panel-row--nav {
  cursor: pointer;
  transition: background 0.15s ease;
}

.hero-chat__panel-row--nav:hover {
  background: rgba(255, 255, 255, 0.04);
}

.hero-chat__panel-row--nav:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: -2px;
}

.hero-chat__panel-row-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

.hero-chat__panel-chevron {
  color: #a78bfa; /* blue/violet touch on chevrons, per spec */
  flex-shrink: 0;
}

.hero-chat__panel-row--privacy {
  cursor: pointer;
  gap: 8px;
  color: rgba(255, 255, 255, 0.7);
  font-size: 12px;
  min-height: 46px;
  transition: background 0.15s ease, color 0.15s ease;
}

.hero-chat__panel-row--privacy:hover {
  background: rgba(255, 255, 255, 0.04);
  color: #fff;
}

.hero-chat__panel-row--privacy:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: -2px;
}

.hero-chat__panel-lock {
  flex-shrink: 0;
  opacity: 0.7;
}

.hero-chat__panel-row--form {
  gap: 8px;
  min-height: auto;
}

.hero-chat__panel-input {
  flex: 1;
  min-width: 0;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 8px;
  padding: 6px 10px;
  color: #fff;
  font-family: var(--font-satoshi);
  font-size: 12.5px;
}

.hero-chat__panel-input:focus {
  outline: none;
  border-color: #b86bf8;
}

/* The discreet danger link — no box, no red zone, sits below the main
   list on its own. Full red only appears once inside its own sub-view
   and in the final confirm() dialog. */
.hero-chat__panel-danger-link {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  margin-top: 10px;
  padding: 4px;
  background: transparent;
  border: none;
  font-family: var(--font-satoshi);
  font-size: 12px;
  font-weight: 600;
  color: rgba(255, 138, 130, 0.75);
  text-align: left;
  cursor: pointer;
}

.hero-chat__panel-danger-link:hover {
  color: #ff9d97;
  text-decoration: underline;
}

.hero-chat__panel-danger-link:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

/* The danger sub-view's own three actions — real red only shows up here. */
.hero-chat__panel-row--danger {
  cursor: pointer;
  color: #ff9d97;
  font-weight: 600;
  transition: background 0.15s ease;
}

.hero-chat__panel-row--danger:hover:not(:disabled) {
  background: rgba(255, 95, 86, 0.1);
}

.hero-chat__panel-row--danger:disabled {
  color: rgba(255, 157, 151, 0.35);
  cursor: not-allowed;
}

.hero-chat__panel-row--danger:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: -2px;
}

/* --- Small text buttons used inside sub-view rows (Enregistrer/Reprendre/
   Supprimer) — compact, never a bordered "card" around them. ------------ */
.hero-chat__panel-btn-sm {
  position: relative;
  flex-shrink: 0;
  font-family: var(--font-satoshi);
  font-size: 11.5px;
  font-weight: 600;
  padding: 5px 9px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: transparent;
  color: #fff;
  cursor: pointer;
}

.hero-chat__panel-btn-sm:hover {
  background: rgba(255, 255, 255, 0.08);
}

.hero-chat__panel-btn-sm--danger {
  border-color: rgba(255, 95, 86, 0.4);
  color: #ff9d97;
}

.hero-chat__panel-btn-sm:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 1px;
}

.hero-chat__panel-row-actions {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}

/* Real per-action loading feedback — the exact button just clicked dims
   and shows a spinning ring while its own request is in flight, distinct
   from the screen-level error flash `reportFailure` already raises on
   failure. */
.hero-chat__panel-btn-sm.is-loading,
.hero-chat__panel-toggle.is-loading {
  position: relative;
  color: transparent;
  pointer-events: none;
}

.hero-chat__panel-btn-sm.is-loading::after,
.hero-chat__panel-toggle.is-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  animation: hero-chat-panel-spin 0.7s linear infinite;
}

@keyframes hero-chat-panel-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__panel-btn-sm.is-loading::after,
  .hero-chat__panel-toggle.is-loading::after {
    animation: none;
  }
}

.hero-chat__panel-toggle {
  position: relative;
  width: 36px;
  height: 20px;
  border-radius: 999px;
  border: none;
  background: rgba(255, 255, 255, 0.15);
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.2s ease;
}

.hero-chat__panel-toggle::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  transition: transform 0.2s ease;
}

.hero-chat__panel-toggle.is-on {
  background: linear-gradient(270.04deg, #3f7bf8 0.04%, #b86bf8 99.96%);
}

.hero-chat__panel-toggle.is-on::after {
  transform: translateX(16px);
}

.hero-chat__panel-toggle:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.hero-chat__panel-toggle:focus-visible {
  outline: 2px solid #b86bf8;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .hero-chat__panel-toggle,
  .hero-chat__panel-toggle::after {
    transition: none;
  }
}

/* --- Document status pill (documents sub-view) -------------------------- */
.hero-chat__panel-doc-status {
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: 999px;
  color: rgba(255, 255, 255, 0.55);
  background: rgba(255, 255, 255, 0.08);
}

.hero-chat__panel-doc-status--ready {
  color: #a8f0b8;
  background: rgba(123, 216, 143, 0.14);
}

.hero-chat__panel-doc-status--rejected {
  color: #ffb3ac;
  background: rgba(255, 95, 86, 0.14);
}

/* --- Empty states — one short line (+ one short hint for memories only),
   never a large icon or a half-panel-tall blank area. -------------------- */
.hero-chat__panel-empty {
  padding: 20px 8px;
  text-align: center;
}

.hero-chat__panel-empty-text {
  margin: 0;
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.5);
}

.hero-chat__panel-empty-hint {
  margin: 4px 0 0;
  font-family: var(--font-satoshi);
  font-size: 11px;
  color: rgba(255, 255, 255, 0.32);
}

.hero-chat__panel-detail-text {
  margin: 4px 2px;
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.7);
}

/* --- Mobile / body-reparented positioning -------------------------------
   Near-fullscreen bottom sheet: on mobile always, and on any viewport
   whenever the panel has been reparented to <body> — which happens both
   for the mobile case AND whenever it's opened from inside the expanded
   conversation modal (hero-chat-settings.js's `anchorToBody` option),
   since the panel's normal anchor sits outside the modal and would
   otherwise render invisibly behind its scrim. z-index 1010 is
   deliberately above the modal's own 1000 (hero-chat-expanded.css) so it
   reads as "on top of" the conversation window, not behind it. The two
   real cases get their own rule: a compact popover anchored near the
   top-right above 480px, a genuine bottom sheet only below it. */
body > .hero-chat__panel {
  position: fixed;
  z-index: 1010;
}

@media (min-width: 481px) {
  body > .hero-chat__panel {
    top: 84px;
    right: 24px;
    left: auto;
    bottom: auto;
    width: min(404px, calc(100vw - 32px));
    max-height: min(524px, calc(100vh - 120px));
  }
}

@media (max-width: 480px) {
  .hero-chat__panel,
  body > .hero-chat__panel {
    left: 14px;
    right: 14px;
    bottom: 14px;
    top: auto;
    width: auto;
    max-height: 75vh;
    z-index: 1010;
  }

  .hero-chat__panel-list--scroll {
    max-height: 48vh;
  }

  /* A compact drag-handle bar at the top of the sheet — purely
     decorative (closing stays via the explicit close button/
     outside-click/Escape, never drag-to-dismiss). */
  .hero-chat__panel-header {
    position: relative;
    padding-top: 20px;
  }

  .hero-chat__panel-header::before {
    content: "";
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.22);
  }
}

/* Editorial secondary link (pages/index.njk) — moved INSIDE .hero-chat's
   own bottom strip (explicit direction: "il faut le mettre ici dans la
   bande grise"), the last thing in the card rather than floating below
   it. Discreet on purpose either way: smaller and quieter than
   .hero__cta-btn above, an underline that only appears on hover/focus
   rather than a bordered pill, so it never competes with "Collaborons"
   as a second call to action. */
.hero-architecture-link {
  width: 100%;
  text-align: left;
  margin-top: 22px;
  margin-bottom: 6px;
}

.hero-architecture-link a {
  font-family: var(--font-satoshi);
  font-size: 14px;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.6);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: color 0.2s ease, text-decoration-color 0.2s ease;
}

.hero-architecture-link a:hover,
.hero-architecture-link a:focus-visible {
  color: #fff;
  text-decoration-color: currentColor;
}
