/* THE HOLO STAGE — a film presented as a projection standing in the room.
 *
 * The owner asked for "a hologram experience" on the counselling page. This is
 * the rig. It wraps a plain <video> and dresses it as something being projected
 * into the shop rather than embedded in a document: a beam, a floor caustic,
 * a scan sweep, a chroma fringe at the edges, and motes drifting in the light.
 *
 * FOUR THINGS IT MAY NEVER DO, and each is a rule this page already carries.
 *
 *   1. It may never be the only way to watch. With JavaScript off, none of
 *      this applies and the <video> underneath is a plain playable film with a
 *      poster still (charter rule 14). The stage is an UPGRADE, never a
 *      dependency — every selector below is under html.js for that reason.
 *   2. It may never autoplay. A counselling page does not start talking at
 *      somebody who has just arrived, and a projection that springs to life is
 *      the same thing with atmosphere on it.
 *   3. It may never name a person. The label slot is driven by
 *      life/likeness-consent.js and stays empty unless a consent record exists.
 *   4. It may never move for a guest who asked things not to. Everything
 *      animated here is inside prefers-reduced-motion: no-preference, and the
 *      still version is designed to look deliberate rather than broken.
 *
 * PALETTE. The park's own: deep green-ink ground, warm cream light, and the
 * shop's accent for the beam. No new hue is introduced — a hologram in a
 * different colour from the room it stands in reads as a sticker.
 */

/* ---- the stage: the space the projection stands in ---------------------- */
html.js .holo{
  /* the film rig's own resting values, read by the shared strike keyframes */
  --holo-pool:.5; --holo-shaft:.30;
  position:relative;
  display:flex; flex-direction:column; align-items:center;
  padding:26px 0 34px;
  isolation:isolate;                /* keeps the glows off the card's brackets */
}

/* the floor it lands on — an ellipse of light under the beam */
html.js .holo::after{
  content:"";
  position:absolute; left:50%; bottom:14px; translate:-50% 0;
  width:min(76%,340px); height:26px;
  border-radius:50%;
  background:radial-gradient(50% 50% at 50% 50%,
    var(--holo-hot,#F7E3B0) 0%,
    var(--shop-accent-soft,#F0C876) 34%,
    rgba(240,200,118,.16) 68%,
    transparent 100%);
  opacity:.5; pointer-events:none; z-index:0;
}

/* ---- the beam: two shafts of light from above --------------------------- */
html.js .holo-beam{
  position:absolute; left:50%; top:0; translate:-50% 0;
  width:min(88%,380px); height:100%;
  pointer-events:none; z-index:0; opacity:.30;
  background:
    linear-gradient(to bottom,
      transparent 0%,
      rgba(240,200,118,.16) 22%,
      rgba(240,200,118,.05) 74%,
      transparent 100%);
  clip-path:polygon(38% 0, 62% 0, 88% 100%, 12% 100%);
}

/* ---- the projection: the film itself ------------------------------------ */
html.js .holo-figure{
  position:relative; z-index:1;
  display:block; margin-inline:auto;
}
/* THE CAP IS 30vh AND IT IS 4 LOWER THAN THE PLAIN FILM'S.
   The un-staged film sits at 34vh (life/park-clearing.css, with its own
   measurements written down beside it). The stage adds 26px of headroom and
   34px of floor for the beam and the pool to land in, and that pushed the
   first fact's last line back under the card's edge — the exact fault the
   other file records fixing. Re-measured at 390x844 with the stage mounted,
   card open, no scrolling, against the CARD's bottom rather than the
   viewport's:
     34vh  clipped by 11px
     30vh  23px clear
     27vh  48px clear
     24vh  74px clear
   30vh is the largest that still lands the whole first answer. */
html.js .holo-figure video{
  display:block; margin-inline:auto;
  max-width:100%; max-height:30vh; width:auto; height:auto;
  background:#0B1917;
  /* the projected look: lifted blacks, a cool cast, and a soft bloom that
     reads as light in air rather than a filter on a picture */
  filter:brightness(1.06) contrast(.94) saturate(1.06);
  box-shadow:
    0 0 0 1px rgba(240,200,118,.20),
    0 0 26px -6px rgba(240,200,118,.30),
    0 22px 48px -20px rgba(0,0,0,.7);
}

/* chroma fringe — one pixel of colour separation at the frame edge, which is
   what sells "projected" more than any amount of scanline does */
html.js .holo-figure::before{
  content:"";
  position:absolute; inset:0; pointer-events:none; z-index:2;
  box-shadow:
    inset  1px 0 0 rgba(120,200,255,.34),
    inset -1px 0 0 rgba(255,150,110,.30);
  mix-blend-mode:screen;
}

/* ---- the scan sweep ------------------------------------------------------ */
html.js .holo-scan{
  position:absolute; inset:0; pointer-events:none; z-index:3;
  overflow:hidden;
}
html.js .holo-scan::before{           /* the fine line structure, always on */
  content:"";
  position:absolute; inset:0;
  background:repeating-linear-gradient(
    to bottom,
    rgba(255,248,236,.055) 0 1px,
    transparent 1px 3px);
  opacity:.7;
}

/* ---- the label: EMPTY UNTIL A CONSENT RECORD SAYS OTHERWISE -------------- */
html.js .holo-label{
  position:relative; z-index:4;
  margin-top:16px;
  font-family:var(--mono,"IBM Plex Mono",monospace);
  font-size:.74rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--shop-accent-soft,#F0C876); opacity:.9;
}
html.js .holo-label:empty{ display:none }

/* ---- motion, and the absence of it -------------------------------------- */
@media (prefers-reduced-motion:no-preference){
  html.js .holo-scan::after{          /* the travelling bar */
    content:"";
    position:absolute; left:0; right:0; height:26%;
    background:linear-gradient(to bottom,
      transparent, rgba(255,248,236,.10) 45%, rgba(255,248,236,.16) 55%, transparent);
    animation:holoSweep 5.2s linear infinite;
  }
  html.js .holo-beam{ animation:holoBreathe 7s ease-in-out infinite }
  html.js .holo::after{ animation:holoPool 7s ease-in-out infinite }

  /* AND THE FILM'S RIG SWITCHES ON WITH ITS CARD TOO — 27 August 2026. Same
     switch as the notice's (life/holo-stage.js wires both), same order: the
     floor lands first, then the shaft drops into it. These two rules come
     after the idle pair above and replace them outright while .holo-on is
     present, so the strike and the breathe are one declaration and can never
     fight each other over the same property. Without .holo-on — no card, no
     observer — the idle above is what runs, which is the old behaviour. */
  html.js .holo.holo-on::after{
    animation:holoStrike .30s cubic-bezier(.2,1.4,.4,1) .04s both,
              holoPool 7s ease-in-out .34s infinite;
  }
  html.js .holo.holo-on .holo-beam{
    animation:holoDrop .34s ease-out .12s both,
              holoBreathe 7s ease-in-out .46s infinite;
  }

  @keyframes holoSweep{
    from{ transform:translateY(-30%) } to{ transform:translateY(400%) }
  }
  @keyframes holoBreathe{
    0%,100%{ opacity:.30 } 50%{ opacity:.20 }
  }
  @keyframes holoPool{
    0%,100%{ opacity:.50 } 50%{ opacity:.36 }
  }
}

/* A guest who asked for stillness gets a projection that is simply STANDING
   there — beam, floor pool, scanlines and fringe, none of them moving. It has
   to look chosen, not failed, so the static opacities sit at the midpoint of
   the animated range rather than at either end. */
@media (prefers-reduced-motion:reduce){
  html.js .holo-beam{ opacity:.25 }
  html.js .holo::after{ opacity:.43 }
}

/* ---- small screens ------------------------------------------------------- */
@media (max-width:26rem){
  html.js .holo{ padding:18px 0 26px }
  html.js .holo-beam{ width:min(94%,320px) }
}

/* ---- HAYDEN: THE NOTICE IN LIGHT ---------------------------------------
   A ParkSyde character presented as text, which is the only presentation a
   character may have that carries no likeness of anybody (life/characters.js).
   So this is typography standing in a beam, not a person.

   WITH SCRIPTS OFF none of this applies and the markup underneath is a
   heading and an ordinary numbered list — charter rule 14, and a perfectly
   good way to read eight sentences. Everything below is under html.js. */
html.js .holo-notice{
  position:relative;
  margin:26px 0 4px;
  padding:24px 20px 20px;
  border:1px solid rgba(240,200,118,.22);
  border-radius:3px;
  background:
    linear-gradient(to bottom, rgba(240,200,118,.055), rgba(240,200,118,.012));
  overflow:hidden;
  isolation:isolate;
}
/* the same scanline structure as the projection, so the two read as one rig */
html.js .holo-notice::before{
  content:"";
  position:absolute; inset:0; pointer-events:none; z-index:0;
  background:repeating-linear-gradient(
    to bottom, rgba(255,248,236,.05) 0 1px, transparent 1px 3px);
  opacity:.6;
}
html.js .holo-notice > *{ position:relative; z-index:1 }

html.js .holo-notice h3{
  margin:0 0 14px;
  font-family:var(--mono,"IBM Plex Mono",monospace);
  font-size:.74rem; font-weight:600;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--shop-accent-soft,#F0C876);
}

html.js .holo-lines{
  margin:0; padding:0; list-style:none;
  display:flex; flex-direction:column; gap:11px;
}
html.js .holo-lines li{
  font-size:.97rem; line-height:1.45;
  /* the projected cast: a hair of warmth and a soft glow off the glyphs */
  text-shadow:0 0 14px rgba(240,200,118,.20);
}

html.js .holo-sig{
  margin:16px 0 0; padding-top:12px;
  border-top:1px solid rgba(240,200,118,.16);
  font-family:var(--mono,"IBM Plex Mono",monospace);
  font-size:.72rem; line-height:1.6; letter-spacing:.04em;
  color:var(--shop-accent-soft,#F0C876); opacity:.78;
}

/* THE LINES ARRIVE ONE AT A TIME, and only for a guest who did not ask for
   stillness. holo-stage.js sets --i per line; the delay is that index times
   one beat, so eight sentences land over about three seconds. It is a notice
   coming up in light rather than a paragraph appearing all at once.
   A guest with reduced motion gets all eight immediately and legibly — the
   default state below is VISIBLE, and the animation only ever fades in from
   it, so a script that never runs leaves the text readable rather than
   invisible. That ordering is the whole trick. */
@media (prefers-reduced-motion:no-preference){
  html.js .holo-notice.holo-on .holo-lines li{
    animation:holoLine .5s ease-out both;
    /* .13s a line, not .34s. At .34 the last of six sentences was still
       invisible 2.2 seconds after the card opened, and a page somebody reads
       in distress may not withhold a sentence about crisis numbers while it
       does a flourish. Six lines now land inside 1.15s and it still reads as
       arriving rather than appearing. */
    /* AND THEY WAIT FOR THE PROJECTOR — 27 August 2026, but only just.
       .38s, not the .62s the figure takes to fully settle, and the four
       hundredths matter. Held until the figure stopped shaking, the panel
       showed a heading over an empty block for two thirds of a second, which
       reads as a layout that has broken rather than one that is arriving.
       Starting at .38 overlaps the last of the materialise, so the words come
       up THROUGH the same power-up instead of queueing behind it.
       AND THE BEAT DROPS TO .11. The note above this one is the reason: a
       page somebody may be reading in distress does not withhold a sentence
       about crisis numbers to do a flourish. At .38 + 5 x .11 + .5 the last
       of six lands at 1.43s — 0.28s later than before the projector existed,
       which is the whole price of it and is the most this is allowed to cost.
       --holo-lag is 0 on a notice with no figure, so a room whose leader has
       no avatar loses the wait and not the arrival. */
    animation-delay:calc(var(--holo-lag,0s) + var(--i,0) * .11s);
  }
  html.js .holo-notice.holo-on.holo-hasfig{ --holo-lag:.38s }
  /* the heading is the projector's own label and comes up with the light, so
     nothing is ever left standing alone over a blank panel */
  html.js .holo-notice.holo-on.holo-hasfig h3{
    animation:holoLine .40s ease-out .24s both;
  }
  @keyframes holoLine{
    from{ opacity:0; transform:translateY(5px); filter:blur(2px) }
    to  { opacity:1; transform:none;            filter:none }
  }
}

@media (max-width:26rem){
  html.js .holo-notice{ padding:20px 16px 16px }
  html.js .holo-lines li{ font-size:.94rem }
}

/* ==========================================================================
   THE PROJECTOR — 27 August 2026, the owner: "they were to be holograms that
   switch on when a button is pressed."

   The first build gave the leader a 78px figure floated at the right of a
   text block with the room's scanlines faintly over the top. Everything was
   technically there and it read as a sticker beside a paragraph. This is the
   rebuild: the figure stands centre-stage in its own shaft of light, over its
   own floor pool, and the whole rig is DARK until the card opens — then it
   strikes on, in order, the way a projector does.

   THE SEQUENCE, and every number in it is a decision:
     0ms    nothing. The panel is a dark box.
     40ms   the emitter fires: the floor pool snaps open sideways, overshoots
            and settles. A light lands before anything stands in it.
     120ms  the beam drops — a shaft clipped from the top down.
     240ms  the figure MATERIALISES: a bottom-to-top mask wipe, heavy chroma
            split either side, and a shudder that decays over three beats.
     620ms  settled. The chroma narrows to a hair, the shudder stops, and the
            slow breathe/scan idle takes over.
     620ms+ the six sentences arrive, one every .13s (see --holo-lag above).

   WHAT IT MAY NOT DO, same four rules as the film stage at the top of this
   file, and one more that only applies here:
     · It may never withhold the words. Everything below is under html.js AND
       under .holo-on, and .holo-on is only ever ADDED. With scripts off, with
       this stylesheet blocked, or if the observer in holo-stage.js never
       fires, what is on the screen is a heading and a numbered list.
     · Reduced motion never sees a switch at all — the projection is simply
       already standing there, lit, at the midpoint of the idle range. A
       hologram that assembles itself is a flourish, and a flourish is exactly
       what that guest asked not to be shown.
     · No photograph is anywhere in this path. The figure is drawn by
       life/avatar-catalogue.js from five declared values. A hologram is a
       very tempting place to put a real face, and that is precisely why the
       rule sits here in the file that draws it.
   ========================================================================== */
html.js .holo-proj{
  position:relative;
  display:block; margin:0 auto 20px;
  width:clamp(96px,30%,124px);
  /* the beam and the pool live outside the figure's box, so the stage
     reserves their room here rather than letting them spill onto the type */
  padding:26px 0 22px;
}
html.js .holo-proj-fig{
  position:relative; z-index:2;
  display:block;
}
html.js .holo-proj-fig svg{ width:100%; height:auto; display:block }

/* the shaft: a cone widening downward, clipped to a trapezoid so it reads as
   light in air rather than a rectangle of paint */
html.js .holo-proj-beam{
  position:absolute; left:50%; top:0; translate:-50% 0;
  width:230%; height:100%;
  z-index:0; pointer-events:none; opacity:.34;
  background:linear-gradient(to bottom,
    rgba(240,200,118,.30) 0%,
    rgba(240,200,118,.13) 46%,
    rgba(240,200,118,.03) 86%,
    transparent 100%);
  clip-path:polygon(40% 0, 60% 0, 100% 100%, 0 100%);
  /* the shaft fades in over its first tenth so the emitter, not a horizontal
     edge, is where the light appears to start */
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 9%);
          mask-image:linear-gradient(to bottom,transparent 0,#000 9%);
  filter:blur(.5px);
}
/* the floor it lands on */
html.js .holo-proj-pool{
  position:absolute; left:50%; bottom:8px; translate:-50% 0;
  width:190%; height:20px; z-index:1; pointer-events:none;
  border-radius:50%;
  background:radial-gradient(50% 50% at 50% 50%,
    var(--holo-hot,#F7E3B0) 0%,
    var(--shop-accent-soft,#F0C876) 30%,
    rgba(240,200,118,.14) 66%,
    transparent 100%);
  opacity:.46;
}
/* THE SCAN LINES LIVE IN THE LIGHT, NOT IN A BOX BEHIND THE FIGURE.
   First build put them on the stage's own padding box — 95px wide against a
   219px beam — and the result was a hard-edged striped rectangle sitting
   behind a figure, which reads as a CRT panel rather than as structure in a
   shaft of light. So the scan takes the beam's exact geometry and the beam's
   exact clip, and it is masked out at the very top so the two arrive at the
   emitter together instead of the stripes starting a hair above the light. */
html.js .holo-proj-scan{
  position:absolute; left:50%; top:0; translate:-50% 0;
  width:230%; height:100%;
  z-index:3; pointer-events:none;
  background:repeating-linear-gradient(to bottom,
    rgba(255,248,236,.11) 0 1px, transparent 1px 3px);
  clip-path:polygon(40% 0, 60% 0, 100% 100%, 0 100%);
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 14%,#000 88%,transparent 100%);
          mask-image:linear-gradient(to bottom,transparent 0,#000 14%,#000 88%,transparent 100%);
  opacity:.55;
}

/* THE EMITTER. A beam with a hard flat top is a lampshade; a beam with a
   bright point at its apex is a projector. Two pixels of hot light and a
   short glow, sitting exactly where the shaft's two top corners meet. */
html.js .holo-proj::before{
  content:"";
  position:absolute; left:50%; top:0; translate:-50% 0;
  width:22%; height:3px; border-radius:999px; z-index:2; pointer-events:none;
  background:var(--holo-hot,#F7E3B0);
  box-shadow:0 0 14px 2px rgba(247,227,176,.55);
  opacity:.85;
}

/* THE CHROMA SPLIT IS A drop-shadow PAIR, NOT A FILTER STACK. Two coloured
   copies of the SVG's own alpha, one cool and one warm, offset either side.
   It costs one filter on one small element and it is the single thing that
   sells "projected" — a scanline overlay alone reads as a CRT texture. */
html.js .holo-proj-fig{
  filter:
    drop-shadow(0 0 18px rgba(240,200,118,.40))
    drop-shadow(calc(var(--holo-split,1px) * -1) 0 0 rgba(120,200,255,.55))
    drop-shadow(var(--holo-split,1px) 0 0 rgba(255,150,110,.48));
}

/* ---- DARK UNTIL THE BUTTON IS PRESSED ----------------------------------
   Only under .holo-on does any of this light up, and .holo-on arrives when
   the card opens. Note the ORDER: the resting state here is the LIT one, and
   the switch-on below animates FROM dark TO it — so a broken observer leaves
   a projection standing, never an empty box. */
@media (prefers-reduced-motion:no-preference){
  html.js .holo-notice.holo-on .holo-proj-pool{
    animation:holoStrike .30s cubic-bezier(.2,1.4,.4,1) .04s both,
              holoPool 7s ease-in-out .34s infinite;
  }
  html.js .holo-notice.holo-on .holo-proj-beam{
    animation:holoDrop .34s ease-out .12s both,
              holoBreathe 7s ease-in-out .46s infinite;
  }
  html.js .holo-notice.holo-on .holo-proj-fig{
    animation:holoRise .40s ease-out .24s both,
              holoShudder .16s steps(3,end) .24s 3;
  }
  html.js .holo-notice.holo-on .holo-proj-scan{
    animation:holoScanIn .30s ease-out .30s both;
  }
  /* the emitter fires FIRST — before the shaft, before the floor. Something
     has to be switched on for anything else to be explained by it. */
  html.js .holo-notice.holo-on .holo-proj::before{
    animation:holoFire .22s ease-out both;
  }
  @keyframes holoFire{
    0%  { opacity:0; transform:scaleX(.1) }
    45% { opacity:1; transform:scaleX(1.25) }
    100%{ opacity:.85; transform:scaleX(1) }
  }
  /* the split starts wide and closes as it settles */
  html.js .holo-notice.holo-on .holo-proj{
    animation:holoSplit .62s ease-out both;
  }

  /* THE SETTLED OPACITY IS A VARIABLE BECAUSE TWO RIGS SHARE THESE KEYFRAMES.
     An animation's final frame REPLACES the element's own opacity, so a fixed
     value here would land the film's pool on the notice's .46 and then have
     holoPool pick up at .50 one frame later — a visible step at the exact
     moment the thing is meant to settle. Each rig declares its own resting
     value and the strike ends on it. */
  @keyframes holoStrike{
    from{ transform:scaleX(.04); opacity:0 }
    to  { transform:scaleX(1);   opacity:var(--holo-pool,.46) }
  }
  @keyframes holoDrop{
    from{ clip-path:polygon(40% 0, 60% 0, 60% 0, 40% 0); opacity:0 }
    to  { clip-path:polygon(40% 0, 60% 0, 100% 100%, 0 100%); opacity:var(--holo-shaft,.34) }
  }
  /* the wipe: the figure assembles from the floor up. mask-image is the
     honest tool here — no second copy of the artwork, no clip rectangle to
     keep in step with a viewBox that varies by character. */
  @keyframes holoRise{
    from{ opacity:0;
          -webkit-mask-image:linear-gradient(to top,#000 0%,#000 0%,transparent 6%);
                  mask-image:linear-gradient(to top,#000 0%,#000 0%,transparent 6%) }
    60% { opacity:1 }
    to  { opacity:1;
          -webkit-mask-image:linear-gradient(to top,#000 0%,#000 100%,transparent 106%);
                  mask-image:linear-gradient(to top,#000 0%,#000 100%,transparent 106%) }
  }
  /* three hard steps sideways, not a smooth wobble: interference is discrete */
  @keyframes holoShudder{
    0%  { translate:-2px 0 } 33% { translate:1.5px 0 }
    66% { translate:-1px 0 } 100%{ translate:0 0 }
  }
  @keyframes holoScanIn{ from{ opacity:0 } to{ opacity:.55 } }
  @keyframes holoSplit{
    0%  { --holo-split:5px } 45%{ --holo-split:2.5px } 100%{ --holo-split:1px }
  }
}

/* A guest who asked for stillness gets it standing there, already on. */
@media (prefers-reduced-motion:reduce){
  html.js .holo-proj-beam{ opacity:.28 }
  html.js .holo-proj-pool{ opacity:.40 }
}

@media (max-width:26rem){
  html.js .holo-proj{ width:clamp(84px,32%,104px); padding:20px 0 18px }
}

/* --holo-split has to be a REGISTERED property or the keyframe above cannot
   interpolate it — an unregistered custom property animates in discrete jumps
   and the split would snap rather than close. @property is ignored wholesale
   by a browser that does not know it, which leaves the fallback 1px in the
   drop-shadows and a projection that simply does not shudder its colour. */
@property --holo-split{ syntax:'<length>'; inherits:true; initial-value:1px }
