/* ================= PW PORTAL — the house transition =================
   The ouroboros portal-unlock, recovered from the pre-tower landing
   (git 6470028^) and extracted into a standalone pair so any page can
   wear it: load /pw-portal.css + /pw-portal.js and the intro, the exit
   wipe and the room-change pulse all come along. D's ruling 2026-08-14:
   this is a HOUSE transition style, not a one-off intro — "be liberal
   with the logo animation."

   Intro (~4.2s): seal stamps in → teal ring draws around it like a lock
   dial → the dial "clicks" open with a flash → you fly through the ring
   as the overlay dissolves and the page rises beneath. Pure CSS
   timeline: even if JS dies the overlay still ends hidden (pwp-out
   fills forwards).

   Theming: every color funnels through --pwp-* below, which read the
   optional pack-facing hooks (--pw-portal-*) first, then the page's own
   tokens, then the Desert Signal defaults. A theme pack re-skins the
   transition by setting --pw-portal-accent & co. — the base image is a
   PARAMETER too (window.pwPortalConfig.image), never re-welded to one
   PNG. Sits above the hubnav launcher (2147483200). */

#portal{
  --pwp-accent:var(--pw-portal-accent, var(--accent-deep, #54c8c1));
  --pwp-soft:var(--pw-portal-soft, var(--accent-soft, #123430));
  --pwp-bg:var(--pw-portal-bg, var(--bg, #0c1316));
  --pwp-faint:var(--pw-portal-faint, var(--faint, #758785));
  --pwp-mono:var(--pw-portal-mono, var(--mono, var(--font-mono, ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace)));
  display:none;
}
html.portal-on #portal{
  display:flex; position:fixed; inset:0; z-index:2147483400;
  flex-direction:column; align-items:center; justify-content:center; gap:30px;
  cursor:pointer; -webkit-tap-highlight-color:transparent;
  background:
    radial-gradient(760px 560px at 50% 44%, color-mix(in srgb,var(--pwp-soft) 80%,transparent), transparent 62%),
    var(--pwp-bg);
  animation:pwp-out .7s cubic-bezier(.6,0,.85,.6) 4.15s forwards;
}
@keyframes pwp-out{to{opacity:0; visibility:hidden}}

.portal-stage{
  position:relative; width:min(66vw,300px); aspect-ratio:1;
  animation:pwp-fly .85s cubic-bezier(.72,-0.05,.9,.4) 4.05s forwards;
}
/* fly THROUGH the ring — scale past the viewport while dissolving */
@keyframes pwp-fly{to{transform:scale(8); opacity:0}}

.portal-seal{
  position:absolute; inset:8.5%; width:83%; height:83%;
  animation:
    pwp-stamp .75s cubic-bezier(.2,.85,.3,1.15) .05s both,
    pwp-click .38s cubic-bezier(.4,0,.4,1) 3.58s;
}
@keyframes pwp-stamp{
  from{opacity:0; transform:scale(1.4) rotate(-11deg); filter:blur(7px)}
  to{opacity:1; transform:none; filter:none}
}
/* the lock dial turning over — small counter-rotation, then it gives */
@keyframes pwp-click{
  0%{transform:rotate(0)} 32%{transform:rotate(-7deg)}
  68%{transform:rotate(3.5deg)} 100%{transform:rotate(0)}
}

.portal-ring{position:absolute; inset:0; width:100%; height:100%; transform:rotate(-90deg); overflow:visible}
.portal-ring .draw{
  fill:none; stroke:var(--pwp-accent); stroke-width:2.6; stroke-linecap:round;
  stroke-dasharray:603.2; stroke-dashoffset:603.2;
  filter:drop-shadow(0 0 9px color-mix(in srgb,var(--pwp-accent) 65%,transparent));
  animation:pwp-ring-draw 2.4s cubic-bezier(.55,.1,.3,1) .35s forwards;
}
@keyframes pwp-ring-draw{to{stroke-dashoffset:0}}
.portal-ring .dial{
  fill:none; stroke:var(--pwp-accent); stroke-width:1.3; opacity:0;
  stroke-dasharray:2 11;
  transform-box:view-box; transform-origin:50% 50%;
  animation:pwp-dial-in .6s ease .9s forwards, pwp-dial-spin 9s linear .9s infinite;
}
@keyframes pwp-dial-in{to{opacity:.45}}
@keyframes pwp-dial-spin{to{transform:rotate(360deg)}}

.portal-flash{
  position:absolute; inset:-42%; border-radius:50%; opacity:0; pointer-events:none;
  background:radial-gradient(circle, color-mix(in srgb,var(--pwp-accent) 60%,#fff) 0%, color-mix(in srgb,var(--pwp-accent) 55%,transparent) 34%, transparent 62%);
  animation:pwp-flash .55s cubic-bezier(.2,.6,.4,1) 3.85s;
}
@keyframes pwp-flash{18%{opacity:.95}100%{opacity:0}}

.portal-tag{
  margin:0; font-family:var(--pwp-mono); font-size:11px; letter-spacing:.3em;
  text-transform:uppercase; color:var(--pwp-accent); opacity:0;
  animation:pwp-tag-in .55s ease .95s forwards;
}
.portal-hint{
  margin:-18px 0 0; font-family:var(--pwp-mono); font-size:9.5px; letter-spacing:.14em;
  text-transform:uppercase; color:var(--pwp-faint); opacity:0;
  animation:pwp-tag-in .6s ease 1.5s forwards;
}
@keyframes pwp-tag-in{to{opacity:1}}

/* Hold the page back until the portal opens, then rise as usual.

   `.shell` is the historical name; `[data-pw-shell]` is the declared one, for
   pages whose wrapper is called something else. The front door needed it the
   day it became the navigator chart, whose wrapper is `.wrap` — and `.wrap`
   is far too common a class for this stylesheet to claim site-wide, so the
   page opts in by attribute instead. pw-portal.js looks up the same pair. */
html.portal-on .shell,
html.portal-on [data-pw-shell]{animation:pwp-rise .6s cubic-bezier(.2,.8,.2,1) 4.25s both}
@keyframes pwp-rise{from{opacity:0; transform:translateY(12px)} to{opacity:1; transform:none}}

/* ---- PORTAL EXIT — the intro in reverse, compressed to ~0.9s so leaving
   never feels slow: the page sinks away, the seal + completed ring rush up
   around you, flash as you step through, then the destination loads. The
   overlay is injected by pw-portal.js; `html #portal.exit` outweighs the
   intro's `html.portal-on #portal` rules. */
.shell.sink,
[data-pw-shell].sink{animation:pwp-sink .45s cubic-bezier(.55,0,.8,.4) both}
@keyframes pwp-sink{to{opacity:0; transform:translateY(14px)}}
html #portal.exit{
  display:flex; position:fixed; inset:0; z-index:2147483400;
  flex-direction:column; align-items:center; justify-content:center; gap:30px;
  cursor:pointer; -webkit-tap-highlight-color:transparent;
  background:
    radial-gradient(760px 560px at 50% 44%, color-mix(in srgb,var(--pwp-soft) 80%,transparent), transparent 62%),
    var(--pwp-bg);
  animation:pwp-exit-in .22s ease both;
}
@keyframes pwp-exit-in{from{opacity:0}to{opacity:1}}
html #portal.exit .portal-stage{animation:pwp-exit-rush .78s cubic-bezier(.18,.75,.25,1) both}
@keyframes pwp-exit-rush{from{transform:scale(5.5); opacity:0} 35%{opacity:1} to{transform:scale(1); opacity:1}}
#portal.exit .portal-seal{animation:none}
#portal.exit .portal-ring .draw{stroke-dashoffset:0; animation:none}
#portal.exit .portal-ring .dial{opacity:.45; animation:pwp-dial-spin 9s linear infinite}
#portal.exit .portal-flash{animation:pwp-flash .45s cubic-bezier(.2,.6,.4,1) .5s}
#portal.exit .portal-tag{animation:pwp-tag-in .4s ease .22s forwards}

/* ---- PULSE — the drill-down beat. A same-page room change (hash nav)
   gets a compressed echo of the exit: the completed ring blinks up around
   the content for ~.6s, no background, no pointer capture. Enough to make
   moving between rooms feel like stepping through the same portal without
   ever getting in the way of the click. */
html #portal.pulse{
  display:flex; position:fixed; inset:0; z-index:2147483400;
  flex-direction:column; align-items:center; justify-content:center;
  pointer-events:none; background:none;
  animation:pwp-out .3s ease .45s forwards;
}
html #portal.pulse .portal-stage{animation:pwp-pulse-rush .6s cubic-bezier(.18,.75,.25,1) both}
@keyframes pwp-pulse-rush{from{transform:scale(3.4); opacity:0} 30%{opacity:.9} to{transform:scale(.9); opacity:0}}
#portal.pulse .portal-seal{animation:none; opacity:.9}
#portal.pulse .portal-ring .draw{stroke-dashoffset:0; animation:none}
#portal.pulse .portal-ring .dial{opacity:.45; animation:pwp-dial-spin 9s linear infinite}
#portal.pulse .portal-flash{animation:pwp-flash .5s cubic-bezier(.2,.6,.4,1) .12s}

/* ---- DOOR — the gated-area beat. The exit read backwards: instead of the
   seal rushing AT you as you step out, it comes up small and the ring DIALS
   IN around it, flashes, and lifts — being processed through a door rather
   than leaving by one. ~1.25s, because this fires on arrival at a page the
   member asked for and anything longer reads as a slow load, not a threshold.
   Carries the tag (INTAKE / VISITATION / FRONT OFFICE), which is the whole
   point of the mode; `pulse` has no room for one and `exit` navigates away.

   Every base animation below is re-declared: the defaults are the ARRIVAL
   intro's, timed at 3.5-4.2s, and anything left unset leaks those timings
   into a beat that is over in one. */
html #portal.door{
  display:flex; position:fixed; inset:0; z-index:2147483400;
  flex-direction:column; align-items:center; justify-content:center; gap:30px;
  cursor:pointer; -webkit-tap-highlight-color:transparent;
  background:
    radial-gradient(760px 560px at 50% 44%, color-mix(in srgb,var(--pwp-soft) 80%,transparent), transparent 62%),
    var(--pwp-bg);
  animation:pwp-exit-in .16s ease both,
            pwp-out .34s cubic-bezier(.6,0,.85,.6) .9s forwards;
}
html #portal.door .portal-stage{animation:pwp-door-in .8s cubic-bezier(.18,.75,.25,1) both}
@keyframes pwp-door-in{from{transform:scale(.42); opacity:0} 45%{opacity:1} to{transform:scale(1); opacity:1}}
#portal.door .portal-seal{animation:pwp-click .38s cubic-bezier(.4,0,.4,1) .5s}
#portal.door .portal-ring .draw{animation:pwp-ring-draw .72s cubic-bezier(.55,.1,.3,1) .06s forwards}
#portal.door .portal-ring .dial{opacity:.45; animation:pwp-dial-spin 9s linear infinite}
#portal.door .portal-flash{animation:pwp-flash .42s cubic-bezier(.2,.6,.4,1) .52s}
#portal.door .portal-tag{animation:pwp-tag-in .34s ease .18s forwards}

/* ---- CARD — the inmate card unlocking a special-access module ------------
   D's ask, first made 2026-08-14 and repeated 2026-08-19: the membership card
   is already a digital inmate ID, so the way into a gated room is to run it
   through a reader. The card drops in, feeds into the slot stripe-first, the
   lamp goes amber while a read line sweeps it, it clicks green, and the card
   ejects as the overlay lifts off content that stayed painted underneath.

   ~1.9s, against `.door`'s 1.25s, and only in gated rooms. That is not the
   door beat with more budget: this one has something to READ — your filed
   name, your P#, your BK#, the clearance you just spent. A second of the seal
   beat is a stall because there is nothing on screen to look at; a second of
   this is the joke landing.

   NOTHING ON THE CARD IS DERIVED HERE. pw-portal.js is handed a finished
   {name,p,bk,initials,photo,tag} by whoever calls it, and every caller gets
   those from the one jailbird derivation the profile card and Town Square
   already share. Two implementations would print one member two different
   booking numbers on two pages, and that reads as a bug in the joke rather
   than as a joke. See web/lib/jailbird.mjs.

   The card stock is deliberately NOT themed. --pc-* below are the literal
   values from static/profile/index.html, because a laminated card does not
   change color when you change the site's wardrobe — it is the same object
   you were looking at on /profile/. Only the room around it (--pwp-*) and
   the tag re-skin. The lamp is likewise fixed: amber and green are what a
   card reader does, in any theme.

   Same re-declaration rule as `.door`, and it bites harder here: .portal-tag
   and .portal-hint carry the ARRIVAL intro's .95s and 1.5s delays by default,
   so on a 1.9s beat the tag would arrive after the read finished and the hint
   would land with 0.4s left to act on it. Both are re-timed below. */
#portal, #portal *, #portal *::before, #portal *::after{box-sizing:border-box}

html #portal.card{
  --pc-stock:#f4f2ed; --pc-edge:#d8d4ca; --pc-ink:#14120e;
  --pc-grey:#5c5344; --pc-mag:#8c8880;
  --pc-amber:#e0a458; --pc-green:#63d29a;
  display:flex; position:fixed; inset:0; z-index:2147483400;
  flex-direction:column; align-items:center; justify-content:center; gap:20px;
  cursor:pointer; -webkit-tap-highlight-color:transparent;
  background:
    radial-gradient(760px 560px at 50% 44%, color-mix(in srgb,var(--pwp-soft) 80%,transparent), transparent 62%),
    var(--pwp-bg);
  animation:pwp-exit-in .18s ease both,
            pwp-out .42s cubic-bezier(.6,0,.85,.6) 1.46s forwards;
}

/* The machine. Height is the card plus the deck it feeds into, so the whole
   assembly is one box the flex column can center — the card is positioned
   against this, never against the viewport.

   min() is already load-bearing elsewhere in this file (.portal-stage), so it
   is house practice, not a new dependency. The 38vh term is the one that
   matters on a short landscape phone: without it a 224px card plus a 92px
   deck is 447px of a 400px viewport and the tag goes off the bottom. */
#portal.card .pc-reader{
  --pc-w:min(58vw,224px,38vh);
  /* How far the card feeds in. Measured against the card's own furniture: it
     has to swallow the magnetic stripe and the barcode — the machine-readable
     end, which is what a reader is for — and STOP ABOVE THE CLEARANCE LINE,
     because the clearance is the one thing this beat exists to show you. At
     64px it ate the clearance and the card became an anonymous rectangle
     going into a box. Proportional, so it holds at every card size. */
  --pc-sink:calc(var(--pc-w) * .21);
  --pc-deck-h:calc(var(--pc-w) * .41);
  position:relative; width:var(--pc-w);
  height:calc(var(--pc-w) * 1.586 + var(--pc-deck-h));
}

/* THE CARD. Same 1.586 ratio and same furniture order as .mc-card on
   /profile/ — imprint, mugshot, name, numbers, block, barcode, stripe — so
   the thing going into the reader is recognizably the card you were just
   editing, not a picture of one. */
#portal.card .pc-card{
  position:absolute; top:0; left:0; z-index:1;
  width:100%; height:calc(var(--pc-w) * 1.586);
  display:flex; flex-direction:column; gap:calc(var(--pc-w) * .022);
  padding:calc(var(--pc-w) * .05) calc(var(--pc-w) * .055);
  border-radius:calc(var(--pc-w) * .048);
  background:var(--pc-stock); color:var(--pc-ink);
  border:1px solid var(--pc-edge);
  box-shadow:0 18px 40px rgba(0,0,0,.5), 0 2px 0 rgba(255,255,255,.35) inset;
  font-family:var(--pwp-mono); overflow:hidden;
  animation:
    pwp-card-drop .44s cubic-bezier(.2,.85,.3,1.1) both,
    pwp-card-feed .34s cubic-bezier(.5,0,.5,1) .46s both,
    pwp-card-eject .5s cubic-bezier(.3,.1,.2,1) 1.24s both;
}
/* Held in the air above the slot, tilted the way a hand holds one. */
@keyframes pwp-card-drop{
  from{opacity:0; transform:translateY(-52px) rotate(-7deg) scale(1.06)}
  to{opacity:1; transform:translateY(0) rotate(0) scale(1)}
}
/* Fed in, stripe-first, as far as --pc-sink and no further. */
@keyframes pwp-card-feed{ to{transform:translateY(var(--pc-sink))} }
/* Out, and through you — the house exit, at card scale. */
@keyframes pwp-card-eject{
  0%{transform:translateY(var(--pc-sink)); opacity:1}
  38%{transform:translateY(-6px); opacity:1}
  100%{transform:translateY(-26px) scale(1.9); opacity:0}
}

#portal.card .pc-imprint{
  flex:none; text-align:center; line-height:1.14;
}
#portal.card .pc-imprint b{
  display:block; font-size:calc(var(--pc-w) * .057); font-weight:700;
  letter-spacing:-.01em;
}
#portal.card .pc-imprint span{
  display:block; font-size:calc(var(--pc-w) * .052); font-weight:400;
}

/* The mugshot on the height chart, in miniature. The chart is repeating-
   linear-gradient rather than eleven elements: at this size the rules are
   texture, and the real chart's job (reading your height off it) belongs to
   the card you can actually hold still. */
#portal.card .pc-photo{
  flex:1 1 auto; min-height:0; position:relative; align-self:center;
  width:70%; border:1px solid var(--pc-ink); overflow:hidden;
  background:
    repeating-linear-gradient(to bottom,
      rgba(20,18,14,.30) 0 1px, transparent 1px calc(var(--pc-w) * .062)),
    #cfcabf;
}
#portal.card .pc-photo img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  filter:grayscale(1) contrast(1.06);
}
#portal.card .pc-photo i{
  position:absolute; inset:0; display:flex; align-items:center;
  justify-content:center; font-style:normal; font-weight:700;
  font-size:calc(var(--pc-w) * .17); color:#6d675c; letter-spacing:.04em;
}

#portal.card .pc-name{
  flex:none; text-align:center; font-weight:700; letter-spacing:.01em;
  font-size:calc(var(--pc-w) * .066); line-height:1.1;
  /* Pinned like .mc-name is pinned. On the full card, two pixels of overflow
     once squeezed the name to a 2px sliver and printed a card with nobody on
     it; the same flex column is here, so the same defense is here. Only the
     mugshot above is allowed to give up pixels. */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#portal.card .pc-nums{
  flex:none; text-align:center; font-size:calc(var(--pc-w) * .049);
  font-variant-numeric:tabular-nums; color:var(--pc-grey);
}

/* One cell, and it is the point of the whole beat: the clearance you just
   spent to be here. The full card's three-up block (HOUSING / CLASS / ISSUED)
   does not survive the shrink legibly, and none of it is what this moment is
   about. */
#portal.card .pc-block{
  flex:none; margin:0; padding-top:calc(var(--pc-w) * .026);
  border-top:1px solid var(--pc-ink);
}
#portal.card .pc-block dt{
  margin:0 0 1px; font-size:calc(var(--pc-w) * .038); letter-spacing:.16em;
  color:var(--pc-grey); text-transform:uppercase;
}
/* Uppercased in CSS, not by the caller. The real card uppercases its values in
   JS on the way in; doing it here means a tag reads the same on the card
   whether it arrived as "Visitation" from a React prop or "VISITATION" from a
   hand-written call, and the tag under the reader stays the caller's casing. */
#portal.card .pc-block dd{
  margin:0; font-size:calc(var(--pc-w) * .055); font-weight:700;
  letter-spacing:.06em; text-transform:uppercase; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis;
}

/* Bar widths are read off the BK#'s own digits by the same rule the profile
   card uses, so this is a picture of THIS booking number and not a texture
   that happens to sit near one. Fewer bars only because the card is smaller. */
#portal.card .pc-barcode{
  flex:none; display:flex; gap:1px; align-items:stretch;
  height:calc(var(--pc-w) * .075); overflow:hidden;
}
#portal.card .pc-barcode span{display:block; height:100%; background:var(--pc-ink)}

/* The magnetic stripe. It is at the bottom edge because that is the end that
   goes in — the feed animation is stripe-first, which is how you actually
   swipe a card, and getting it upside down would be the kind of detail that
   quietly makes the whole thing feel fake. */
#portal.card .pc-stripe{
  flex:none; height:calc(var(--pc-w) * .052); margin:auto -8% 0;
  background:var(--pc-mag); box-shadow:inset 0 1px 2px rgba(0,0,0,.5);
}

/* The read sweep. Fires while the card is in the slot and travels the part
   still showing, so it reads as the machine looking at the card rather than
   as a shine on it. */
#portal.card .pc-scan{
  position:absolute; left:0; right:0; top:0;
  height:calc(var(--pc-w) * .34); opacity:0; pointer-events:none;
  background:linear-gradient(to bottom,
    rgba(224,164,88,0) 0%, rgba(224,164,88,.55) 55%, rgba(255,255,255,.85) 100%);
  animation:pwp-card-scan .58s cubic-bezier(.4,0,.5,1) .62s both;
}
@keyframes pwp-card-scan{
  0%{opacity:0; transform:translateY(-34%)}
  12%{opacity:1}
  88%{opacity:1}
  100%{opacity:0; transform:translateY(280%)}
}

/* THE DECK — the reader body. Wider than the card so it reads as a machine
   the card belongs to, and OPAQUE, because occluding the fed end is its
   actual job: there is no clip-path and no overflow trick here, the deck is
   simply painted over the card from the slot line down. Anything that makes
   this translucent puts the bottom third of the card back on screen sticking
   out through the machine. */
#portal.card .pc-deck{
  position:absolute; z-index:2; left:-9%; right:-9%;
  top:calc(var(--pc-w) * 1.586); height:var(--pc-deck-h);
  border-radius:0 0 10px 10px;
  background:var(--pwp-bg);
  box-shadow:0 14px 30px rgba(0,0,0,.45);
  animation:pwp-card-deck .3s ease both;
}
@keyframes pwp-card-deck{from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:none}}
/* A panel face, in rgba so no color-mix() sits in front of the opacity that
   the occlusion depends on. */
#portal.card .pc-deck::after{
  content:""; position:absolute; inset:0; border-radius:inherit;
  background:linear-gradient(to bottom, rgba(255,255,255,.09), rgba(255,255,255,.02) 40%, rgba(0,0,0,.22));
  border-top:1px solid rgba(255,255,255,.14);
}
/* The mouth. A dark inset line at the deck's top edge, lit along its lower
   lip so the card looks like it is going under something. */
#portal.card .pc-deck .pc-mouth{
  position:absolute; z-index:1; left:8%; right:8%; top:9px; height:7px;
  border-radius:4px; background:#05090b;
  box-shadow:inset 0 2px 4px rgba(0,0,0,.9), 0 1px 0 rgba(255,255,255,.16);
}

/* The lamp. All three labels ship in the DOM and cross-fade, so the state
   changes need no timer of their own — skipping the beat removes the node and
   there is nothing left running to clean up. */
#portal.card .pc-deck .pc-lamp{
  position:absolute; z-index:1; left:0; right:0; bottom:14px;
  display:flex; align-items:center; justify-content:center; gap:8px;
  font-family:var(--pwp-mono); font-size:10px; letter-spacing:.22em;
  text-transform:uppercase;
}
#portal.card .pc-deck .pc-lamp b{
  width:8px; height:8px; border-radius:50%; flex:none; background:var(--pwp-faint);
  animation:pwp-lamp .95s steps(1,end) .5s both;
}
@keyframes pwp-lamp{
  0%{background:var(--pc-amber); box-shadow:0 0 9px var(--pc-amber)}
  /* .5s + 63% ~= 1.10s, the click: the read finishes and the bolt gives. */
  63%{background:var(--pc-green); box-shadow:0 0 12px var(--pc-green)}
  100%{background:var(--pc-green); box-shadow:0 0 12px var(--pc-green)}
}
#portal.card .pc-deck .pc-lamp i{display:grid; font-style:normal}
#portal.card .pc-deck .pc-lamp span{grid-area:1/1; opacity:0; white-space:nowrap}
#portal.card .pc-deck .pc-lamp .l1{color:var(--pwp-faint); animation:pwp-lamp-l1 .5s linear both}
#portal.card .pc-deck .pc-lamp .l2{color:var(--pc-amber); animation:pwp-lamp-l2 .6s linear .5s both}
#portal.card .pc-deck .pc-lamp .l3{color:var(--pc-green); animation:pwp-tag-in .18s ease 1.1s both}
@keyframes pwp-lamp-l1{0%{opacity:1} 99%{opacity:1} 100%{opacity:0}}
@keyframes pwp-lamp-l2{0%{opacity:1} 99%{opacity:1} 100%{opacity:0}}

/* The unlock itself: one flash across the whole machine at the click. */
#portal.card .pc-reader::after{
  content:""; position:absolute; inset:-18% -22%; border-radius:50%;
  opacity:0; pointer-events:none;
  background:radial-gradient(circle, rgba(99,210,154,.55) 0%, rgba(99,210,154,.18) 38%, transparent 66%);
  animation:pwp-flash .5s cubic-bezier(.2,.6,.4,1) 1.06s;
}

/* Re-timed for a 1.9s beat. Left at their defaults the tag lands at .95s —
   after the read — and the hint at 1.5s, with 0.4s of the beat left to use
   it, which is the same as not offering it. */
#portal.card .portal-tag{animation:pwp-tag-in .3s ease .12s forwards}
#portal.card .portal-hint{animation:pwp-tag-in .3s ease .3s forwards}

/* Reduced motion never sees any of it — belt to the JS gate's braces. */
@media (prefers-reduced-motion:reduce){
  #portal, html.portal-on #portal, html #portal.exit, html #portal.pulse,
  html #portal.door, html #portal.card{display:none !important}
  html.portal-on .shell, html.portal-on [data-pw-shell]{animation:none}
  .shell.sink, [data-pw-shell].sink{animation:none}
}
