/* demo-popup.css — "Hören Sie Lina" full-screen demo modal.
   Shell, layout, industry chips and the intro/live/ended state CSS. GSAP (see
   demo-popup.js) drives every transform/opacity/filter tween described in the
   animation spec; this file supplies the static layout, the design tokens,
   the ambient/parallax decoration layers, and the small CSS-only loops
   (orb rings, button sheen) plus their prefers-reduced-motion shutoff. */

.demo-popup {
  position: fixed; inset: 0; z-index: 10000;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.demo-popup[hidden] { display: none; }
body.demo-popup-open { overflow: hidden; }

/* ---------- backdrop: layered parallax ambience, not a flat panel ---------- */
.demo-popup__backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0);
  /* The blur is set ONCE and never animated. Transitioning backdrop-filter
     re-blurs the whole viewport every frame, which was the single biggest
     source of the hitching while the popup opened. The backdrop only renders
     at all when the popup is open (the host is `hidden` otherwise), and fading
     the tint over a fixed blur is visually indistinguishable from ramping the
     blur itself — the settled state is pixel-identical. */
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  overflow: hidden;
}
.demo-popup.is-open .demo-popup__backdrop {
  background: rgba(0, 0, 0, .55);
}
.demo-popup__blob {
  position: absolute; border-radius: 50%;
  pointer-events: none;
  /* No permanent will-change: this markup exists in the DOM (inside
     [hidden]) even on pages where the popup is never opened, and a
     permanently-promoted compositing layer for two 700px+ blurred blobs has
     its own always-on memory/compositing cost. GSAP's mousemove-driven
     quickTo() only ever moves them while the popup is actually open. */
}
.demo-popup__blob--a {
  /* min() caps the worst case: blurring an 80px-radius filter scales with the
     element's pixel area, and 60vmax on a 4K/ultrawide screen can mean
     blurring a >2000px layer. Typical desktop viewports (the vast majority)
     never hit the cap, so the on-screen size is unchanged there. */
  width: min(60vmax, 720px); height: min(60vmax, 720px); top: -18vmax; left: -14vmax;
  background: radial-gradient(circle, var(--lavender), transparent 70%);
  filter: blur(80px); opacity: .35;
}
.demo-popup__blob--b {
  width: min(36vmax, 460px); height: min(36vmax, 460px); bottom: -10vmax; right: -10vmax;
  background: radial-gradient(circle, var(--accent), transparent 70%);
  filter: blur(60px); opacity: .28;
}
/* Static grain texture — inline SVG feTurbulence, no network request.
   Explicit small background-size + repeat: the turbulence is rasterised once
   at 180x180 and tiled by the compositor, instead of the browser deciding how
   large a canvas to run feTurbulence over (unspecified before this rule). At
   .05 opacity blended with `overlay` the tile seam is invisible. */
.demo-popup__grain {
  position: absolute; inset: -2%;
  opacity: .05; mix-blend-mode: overlay; pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 180px 180px; background-repeat: repeat;
}

/* ---------- modal card ---------- */
.demo-popup__modal {
  position: relative; z-index: 1; isolation: isolate;
  width: 100%; max-width: 640px;
  background: var(--screen); color: var(--cream);
  border-radius: var(--radius-lg);
  padding: 48px 44px;
  box-shadow: 0 60px 120px -40px rgba(0, 0, 0, .7);
  opacity: 0; transform: scale(.92);
  max-height: calc(100vh - 48px); overflow-y: auto; overflow-x: hidden;
  transition: max-width .5s var(--ease);
  --demo-progress: 0;
}
.demo-popup.is-open .demo-popup__modal { opacity: 1; transform: none; }
/* Wide enough that a 7-column week grid gets real column width instead of
   fragmenting every label into 2-3 letter shards. */
.demo-popup__modal--wide { max-width: 1120px; }

/* Programmatic modal.focus() (see open() in demo-popup.js) triggers the
   browser's default outline around the ENTIRE ~1100px card — not a real
   keyboard-navigation moment, just an accessibility handoff. Suppress that
   specific case but keep a real, designed ring for genuine keyboard focus
   (e.g. a user tabs back to the card itself). */
.demo-popup__modal:focus { outline: none; }
.demo-popup__modal:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -3px;
}
/* Every real interactive control inside the popup gets the same designed
   ring: never removed outright, just restyled away from browser-default blue. */
.demo-popup :is(button, a, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 8px;
}
.demo-chip:focus-visible { outline-offset: 2px; }
.demo-popup__custom-input:focus-visible { outline-offset: 2px; }
/* overflow-y:auto makes a plain div keyboard-focusable in Chrome (for
   scroll-by-keyboard) even with no tabindex — that also picked up the
   browser-default blue ring the same way the modal itself did. */
.demo-popup__cal:focus { outline: none; }
.demo-popup__cal:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/* Progress-driven ambient wash, clipped to the card's rounded corners. Continuous
   lerp comes from setProgress()/--demo-progress in JS; CSS just reads the var. */
.demo-popup__ambient {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit; overflow: hidden;
}
.demo-popup__ambient::before {
  content: ""; position: absolute; inset: -20%;
  background: conic-gradient(from 180deg, var(--lavender), var(--accent), var(--sky), var(--lavender));
  /* opacity + transform read the smooth continuous --demo-progress: both are
     compositor-only, free to recompute every frame of the 90s ramp. The
     hue-rotate below deliberately reads the quantised --demo-progress-q
     instead (see setQuantizedProgress() in demo-popup.js) — changing a
     filter's value forces a full re-rasterisation of this blurred layer,
     unlike opacity/transform, so doing that on every single rAF frame for up
     to 90 seconds was the single most expensive thing in this popup. 16
     quantised steps is well under the threshold of visible stepping. */
  opacity: calc(.06 + var(--demo-progress, 0) * .1);
  filter: blur(50px) hue-rotate(calc(var(--demo-progress-q, 0) * 60deg));
  transform: scale(calc(1 + var(--demo-progress, 0) * .12));
}
/* Border sheen that drifts across the top edge as the call progresses. */
.demo-popup__ambient::after {
  content: ""; position: absolute; top: 0; left: -30%; width: 60%; height: 2px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  opacity: calc(.25 + var(--demo-progress, 0) * .55);
  transform: translateX(calc(var(--demo-progress, 0) * 220%));
}
.demo-popup__modal > * { position: relative; z-index: 1; }

.demo-popup__close {
  position: absolute; top: 16px; right: 18px; z-index: 2;
  font-size: 1rem; color: rgba(244, 237, 228, .4);
  padding: 6px 10px; border-radius: 8px;
  transition: color .2s, background .2s;
}
.demo-popup__close:hover { color: var(--cream); background: rgba(255, 255, 255, .08); }

/* ---------- STATE A: intro ---------- */
.demo-popup__intro[hidden] { display: none; }
.demo-popup__intro { text-align: center; }
.demo-popup__title {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.5rem, 3vw, 2rem); letter-spacing: -.02em;
  margin-bottom: 10px;
}
/* Per-letter reveal scaffolding (letters injected + animated by demo-popup.js).
   Word wrapper keeps normal wrapping; the letter mask hides the pre-animation
   translateY so letters rise up out of a clipped box, not off-page. */
.demo-popup__title .dp-word { display: inline-block; }
.demo-popup__title .dp-letter { display: inline-block; overflow: hidden; line-height: 1.15; vertical-align: bottom; }
.demo-popup__title .dp-letter i { display: inline-block; font-style: normal; will-change: transform, filter, opacity; }

.demo-popup__sub { font-size: .98rem; color: rgba(244, 237, 228, .65); margin-bottom: 28px; }

.demo-popup__chips { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 18px; }
.demo-chip {
  position: relative;
  padding: 10px 18px; border-radius: 999px;
  border: 1px solid rgba(244, 237, 228, .2);
  background: rgba(244, 237, 228, .06); color: var(--cream);
  font-size: .9rem; font-weight: 600;
  transition: transform .3s cubic-bezier(.34, 1.56, .64, 1), filter .3s ease, border-color .2s ease, background .2s ease;
}
.demo-chip:hover { transform: translateY(-3px) scale(1.03); filter: drop-shadow(0 8px 18px rgba(255, 90, 60, .35)); border-color: rgba(244, 237, 228, .4); }
.demo-chip.is-selected { background: var(--accent); border-color: var(--accent); color: #fff; }
/* selected border "draws itself in" via a scaleX sweep, not a hard cut */
.demo-chip::after {
  content: ""; position: absolute; inset: -1px; border-radius: 999px;
  border: 2px solid var(--accent); opacity: 0; transform: scaleX(0); transform-origin: left center;
  pointer-events: none;
}
.demo-chip.is-selected::after {
  opacity: 1; transform: scaleX(1);
  transition: transform .45s var(--ease-out), opacity .2s ease;
}

.demo-popup__custom { margin-bottom: 24px; }
.demo-popup__custom-input {
  width: 100%; max-width: 320px; padding: 12px 16px; border-radius: 12px;
  border: 1px solid rgba(244, 237, 228, .2); background: rgba(244, 237, 228, .06);
  color: var(--cream); font-size: .92rem; text-align: center;
  transition: border-color .2s ease, background .2s ease;
}
.demo-popup__custom-input::placeholder { color: rgba(244, 237, 228, .4); }
.demo-popup__custom-input:focus { outline: 2px solid var(--accent); border-color: transparent; background: rgba(244, 237, 228, .1); }

.demo-popup__start {
  position: relative; overflow: hidden;
  width: 100%; justify-content: center; border-radius: 12px;
  transition: transform .15s var(--ease-out), opacity .2s ease;
}
.demo-popup__start:disabled { opacity: .4; cursor: not-allowed; }
.demo-popup__start:active:not(:disabled) { transform: scale(.97); }
/* slow gradient sheen while the CTA is live/enabled */
.demo-popup__start::after {
  content: ""; position: absolute; top: 0; left: -40%; width: 40%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .35), transparent);
  transform: translateX(-20%); pointer-events: none;
}
.demo-popup__start:not(:disabled)::after { animation: demoSheen 2.6s ease-in-out infinite; }
@keyframes demoSheen { 0% { transform: translateX(-20%); } 60%, 100% { transform: translateX(320%); } }

/* ---------- STATE B: live split ---------- */
.demo-popup__live[hidden] { display: none; }
/* 1.4fr/1fr, not an even split: the 7-column week grid needs the column
   width, the orb panel is a fixed-size circle that doesn't. */
.demo-popup__split { display: grid; grid-template-columns: 1.4fr 1fr; gap: 24px; align-items: stretch; }
.demo-popup__split.is-ended { grid-template-columns: 1fr; }
.demo-popup__cal, .demo-popup__orbpanel {
  /* Fixed, equal height for both panels — the calendar's own content (8 rows)
     can exceed this on a short viewport, so it scrolls internally rather than
     clipping or stretching the card past the viewport. */
  height: min(440px, 56vh);
  border-radius: var(--radius);
  background: rgba(244, 237, 228, .04); padding: 16px;
}
.demo-popup__cal { overflow-y: auto; overflow-x: hidden; }
.demo-popup__orbpanel { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; text-align: center; position: relative; overflow: hidden; }
.demo-popup__split.is-ended .demo-popup__orbpanel { display: none; }

.demo-popup__orbtag { font-size: .84rem; color: rgba(244, 237, 228, .55); margin: 0; }
.demo-popup__status { font-size: .88rem; font-weight: 600; color: var(--cream); min-height: 1.3em; margin: 0; }
.demo-popup__end { border-radius: 10px; margin-top: 6px; }

/* Orb mount: another module renders the actual orb inside; this file only owns
   the container-level state treatment (glow ring behind it + breathing/rotation
   on the container itself, so it layers under whatever gets injected). */
.demo-popup__orbglow {
  position: absolute; width: 240px; height: 240px; border-radius: 50%;
  background: radial-gradient(circle, var(--accent), transparent 70%);
  filter: blur(30px);
  /* --demo-voice (0..1) is the live call loudness, written per frame by
     demo-wire.js. Opacity + transform only, so it stays compositor-only. */
  opacity: calc(.35 + var(--demo-progress, 0) * .45 + var(--demo-voice, 0) * .3);
  transform: scale(calc(1 + var(--demo-progress, 0) * .35 + var(--demo-voice, 0) * .22));
  pointer-events: none; z-index: 0;
}
.demo-popup__orbmount {
  position: relative; z-index: 1;
  width: 190px; height: 190px; border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, var(--accent), var(--accent-deep) 70%);
  /* overflow stays visible here (not hidden): the state rings below are
     ::before/::after on THIS element, deliberately drawn bigger via
     inset:-14px so they bloom outward past the circle. Clipping happens on
     the canvas itself instead, see the rule below. */
}
/* The orb's <canvas> is appended into this element by demo-wire.js at
   runtime. It ships with no intrinsic CSS size, so hero-orb.js's
   getBoundingClientRect() read would return the canvas's default 300x150
   box instead of this mount's box — a fixed-size rectangle floating inside a
   much taller panel. Force it to fill the mount; hero-orb.js's own
   ResizeObserver then keys the renderer/camera off *this* box. overflow:hidden
   is on the canvas itself (not the mount) so the state rings above can still
   bloom past the circle undipped. */
.demo-popup__orbmount canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  border-radius: 50%; overflow: hidden;
}
.demo-popup__orbmount::before, .demo-popup__orbmount::after {
  content: ""; position: absolute; inset: -14px; border-radius: 50%;
  border: 2px solid var(--accent); opacity: 0;
}
.demo-popup__orbmount[data-orb-state="idle"] { animation: demoOrbBreathe 3s ease-in-out infinite; }
.demo-popup__orbmount[data-orb-state="listening"]::before { opacity: .5; animation: demoOrbPulse 1.6s ease-out infinite; }
.demo-popup__orbmount[data-orb-state="speaking"]::before { opacity: .6; animation: demoOrbRing 1.1s ease-out infinite; }
.demo-popup__orbmount[data-orb-state="speaking"]::after { opacity: .4; animation: demoOrbRing 1.1s ease-out infinite .35s; }
.demo-popup__orbmount[data-orb-state="thinking"] { animation: demoOrbRotate 3.5s linear infinite; }
.demo-popup__orbmount[data-orb-state="thinking"]::before { opacity: .5; animation: demoOrbShimmer 1.4s ease-in-out infinite; }

@keyframes demoOrbBreathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.04); } }
@keyframes demoOrbPulse { 0% { transform: scale(.85); opacity: .55; } 100% { transform: scale(1.35); opacity: 0; } }
@keyframes demoOrbRing { 0% { transform: scale(.8); opacity: .6; } 100% { transform: scale(1.7); opacity: 0; } }
@keyframes demoOrbRotate { to { transform: rotate(360deg); } }
@keyframes demoOrbShimmer { 0%, 100% { opacity: .35; } 50% { opacity: .85; } }

/* ---------- STATE C: ended ---------- */
.demo-popup__ended[hidden] { display: none; }
.demo-popup__ended { text-align: center; margin-top: 28px; }
.demo-popup__ended-title { font-family: var(--font-display); font-weight: 700; font-size: 1.3rem; margin-bottom: 18px; }
.demo-popup__ended-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* stack orb ABOVE the calendar on narrow screens */
@media (max-width: 900px) {
  .demo-popup__split { grid-template-columns: 1fr; gap: 18px; }
  .demo-popup__orbpanel { order: -1; }
  /* Two stacked 440px panels would blow well past any phone viewport height
     (the modal's own overflow-y:auto would then swallow the whole layout in
     a scroll). Shrink both so intro + live still fit a typical phone. */
  .demo-popup__cal, .demo-popup__orbpanel { height: min(300px, 40vh); }
  .demo-popup__orbmount { width: 130px; height: 130px; }
  .demo-popup__orbglow { width: 170px; height: 170px; }
}
@media (max-width: 600px) {
  .demo-popup__modal { padding: 28px 20px; }
  /* min(300,40vh) still loses the "one scroll region" fight on a genuinely
     short phone: at 360x640 it works out to ~530px of stacked panels alone,
     more than the ~536px the modal has left after its own padding, so the
     modal's outer overflow-y:auto would engage ON TOP OF the calendar
     panel's existing internal scroll (its 8 rows already exceed a short
     panel by design — see the comment on .demo-popup__cal above) — an inner
     scroll inside an outer scroll, which is the exact double-scroll this
     file's live state must avoid. Tightening the vh share here is enough to
     keep the outer modal from ever needing to scroll on a typical phone
     (verified against 360x640 and 390x844 by hand, see FE/CLAUDE.md task
     notes) — leaving the calendar panel's own scroll as the ONLY scroll
     region in the live state. */
  .demo-popup__cal, .demo-popup__orbpanel { height: min(260px, 34vh); }
  .demo-popup__split { gap: 14px; }
  .demo-popup__orbmount { width: 108px; height: 108px; }
  .demo-popup__orbglow { width: 150px; height: 150px; }

  /* ---- intro state: keep "Gespräch starten" above the fold ---- */
  .demo-popup__title { margin-bottom: 6px; }
  .demo-popup__sub { margin-bottom: 18px; font-size: .9rem; }
  .demo-popup__chips { gap: 8px; margin-bottom: 14px; }
  .demo-popup__custom { margin-bottom: 18px; }

  /* ---- ended state: don't push the CTA far below the frozen calendar ---- */
  .demo-popup__ended { margin-top: 16px; }

  /* ---- touch targets: 44x44 effective tap area (WCAG 2.5.5-ish), none of
     these were designed against that number originally — the chips/close
     button were sized for a mouse-hover desktop card. min-height/min-width
     grow the HIT box; visual padding is left close to original so nothing
     looks bloated. ---- */
  .demo-chip {
    padding: 12px 16px; min-height: 44px;
    display: inline-flex; align-items: center;
  }
  .demo-popup__close {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
    top: 6px; right: 6px; padding: 0;
  }
  .demo-popup__start { min-height: 48px; }
  .demo-popup__end { min-height: 44px; padding: 10px 20px; }
  .demo-popup__ended-actions .btn { min-height: 44px; }
}

/* Landscape phones (e.g. 844x390, 667x375): width alone doesn't catch these —
   844 is comfortably inside the max-width:900 stacking rule above, but the
   ~390px of TOTAL viewport height there can't fit two stacked ~156px panels
   (min(300,40vh)) plus the modal's default (non-mobile-width) padding
   without the outer modal scrolling — again risking the double-scroll the
   live state must avoid. Gate on max-height instead of a narrower max-width,
   since the failure mode here is shortness, not narrowness, and pair it with
   max-width:900 so an actually-short-but-WIDE window (which nobody has
   reported a problem on) keeps the roomier side-by-side desktop layout. */
@media (max-width: 900px) and (max-height: 500px) {
  .demo-popup__modal { padding: 16px 24px; }
  .demo-popup__split { gap: 10px; }
  .demo-popup__cal, .demo-popup__orbpanel { height: min(140px, 32vh); padding: 10px; }
  .demo-popup__orbmount { width: 64px; height: 64px; }
  .demo-popup__orbglow { width: 96px; height: 96px; }
  .demo-popup__orbpanel { gap: 6px; }
  /* Decorative-only line — first thing to go when height is this tight. */
  .demo-popup__orbtag { display: none; }
  .demo-popup__status { font-size: .8rem; }
  /* Safety net, not the primary plan: at this extreme a few px of orb-panel
     content (orb + status + button) can still exceed the shrunk panel. Left
     un-clipped (button always reachable) beats a pixel-perfect single scroll
     region in a corner case this narrow — see the file-level report for why
     this is the one place a second scroll surface was accepted. */
  .demo-popup__orbpanel { overflow-y: auto; }
  .demo-popup__title { font-size: 1.15rem; margin-bottom: 2px; }
  .demo-popup__sub { display: none; }
  .demo-popup__chips { margin-bottom: 8px; }
  .demo-popup__custom { margin-bottom: 10px; }
}

/* ---------- popup week grid: dense 7-column layout ----------
   #demoCalWeek shares .cal-cell / .cal-event with the homepage 5-day
   dashboard (calendar.js), but the popup gives it far less width for two
   more columns. Everything below is scoped to #demoPopupCalPanel so the
   homepage grid (#calWeek) is untouched. */
#demoPopupCalPanel .cal-cell { min-height: 38px; }
/* Stays put while the panel scrolls internally (the grid can run slightly
   taller than the fixed panel height, see .demo-popup__cal above) — without
   this the day labels scroll away first, leaving rows of events with no
   header to read them against. */
#demoPopupCalPanel .cal-cell--head {
  min-height: 30px; font-size: .72rem;
  position: sticky; top: -16px; z-index: 3;
  background: var(--screen);
}
#demoPopupCalPanel .cal-cell--time { font-size: .62rem; }
#demoPopupCalPanel .cal-event {
  inset: 3px; padding: 4px 6px; font-size: .64rem; border-radius: 7px;
}
#demoPopupCalPanel .cal-event small { font-size: .56rem; margin-top: 1px; }
/* Truncate with an ellipsis instead of wrapping to 2-3 broken lines — the
   title text is wrapped in .cal-event__title by calendar.js specifically so
   it can be clipped independently of the name line underneath it. */
#demoPopupCalPanel .cal-event__title {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Restrained palette: the homepage's four saturated hues (red/purple/green/
   blue) read as a toy against this dark modal. One quiet neutral tone for
   every pre-filled slot; the single saturated accent is reserved for the
   slot the visitor just booked (.cal-event--live below), so the eye goes
   straight to it. */
#demoPopupCalPanel .cal-event--a,
#demoPopupCalPanel .cal-event--b,
#demoPopupCalPanel .cal-event--c,
#demoPopupCalPanel .cal-event--d {
  background: linear-gradient(150deg, rgba(244, 237, 228, .16), rgba(244, 237, 228, .07));
  box-shadow: inset 0 0 0 1px rgba(244, 237, 228, .12);
}
#demoPopupCalPanel .cal-event--live {
  /* Brand accent (coral) rather than the homepage's green — the popup has
     already spent its "one saturated colour" budget on var(--accent)
     everywhere else (chips, CTA, orb), a second hue here would compete. */
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
}
#demoPopupCalPanel .cal-event--live .cal-event__glow { background: var(--accent); }
/* The popup's cells are ~30% shorter than the homepage grid's, so the check
   badge shrinks with them and the title reserves less room for it. */
#demoPopupCalPanel .cal-event--live .cal-event__check {
  top: 3px; right: 4px; width: 11px; height: 11px;
}
#demoPopupCalPanel .cal-event--live .cal-event__label .cal-event__title {
  padding-right: 13px;
}

/* ---- popup week grid on phones: horizontal scroll, not shattered text ----
   At ~360px wide, minus the modal's side padding and the 38px hour gutter,
   seven day columns get roughly 39px each — event labels like "Behandlung"
   or "Erstgespräch" break into unreadable 2-3 letter fragments, the exact
   defect already fixed on desktop. Two ways out: drop the label entirely
   (the homepage's phone treatment — see the max-width:640px block in
   responsive.css, where .cal-event becomes a plain colour swatch with the
   text hidden), or keep every label full-size and let the grid itself run
   wider than the panel, scrolling horizontally.
   The popup's calendar exists to sell ONE moment — the booked slot, with its
   service + caller name — so dropping that text here would blunt the exact
   payoff the homepage grid doesn't need to carry (it's ambient background
   there, not the finish line of a 90-second demo). Horizontal scroll wins.
   calendar.js sets `grid-template-columns` inline via
   `repeat(var(--cal-days), minmax(var(--cal-colmin, 0), 1fr))` specifically
   so --cal-colmin can be raised here — JS never touches that custom
   property, so this media-query declaration is the only source for it and
   applies with normal cascade, no !important needed to beat the inline
   style (which is the trap a plain `grid-template-columns` override here
   would have walked into). */
@media (max-width: 600px) {
  #demoPopupCalPanel {
    position: relative;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
  }
  #demoPopupCalPanel #demoCalWeek {
    /* 82px: measured against the longest real label ("Erstgespräch" at the
       popup's .64rem event font, ~66px of text) plus the event box's own
       12px horizontal inset — anything narrower still ellipsis-clips it. */
    --cal-colmin: 82px;
    width: max-content; min-width: 100%;
  }
  /* Visible swipe affordance: a static fade pinned to the panel's right
     inner edge. Deliberately not scroll-position-aware (no JS involved) —
     it only needs to say "there's more this way" the moment the grid
     appears, which is the one moment a visitor who hasn't found the scroll
     yet actually needs telling. */
  #demoPopupCalPanel::after {
    content: ""; position: absolute; top: 16px; bottom: 16px; right: 0; width: 26px;
    background: linear-gradient(to right, transparent, rgba(14, 11, 8, .5));
    pointer-events: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .demo-popup__modal, .demo-popup__backdrop, .demo-chip, .demo-popup__custom-input, .demo-popup__start { transition: none !important; }
  .demo-popup__start::after { animation: none !important; opacity: 0 !important; }
  .demo-popup__orbmount[data-orb-state] { animation: none !important; }
  .demo-popup__orbmount[data-orb-state]::before, .demo-popup__orbmount[data-orb-state]::after { animation: none !important; opacity: 0 !important; }
  .demo-popup__ambient::after { transition: none !important; }
}
