/* ============================================================================
   PTR launch-site core.

   The brand layer is transplanted from the live People Tech Revolution
   homepage (PTR-Labs/ptr-website-html @ design, public/Home.dc.html). Every
   colour, gradient, blur radius, keyframe and box-shadow below is copied from
   that file rather than re-invented:

     base #050507                Home.dc.html:38,77,79   (shader base is #020208)
     light volumes               Home.dc.html:92-96  + keyframes L54-56
     caustic streaks             Home.dc.html:97-98  + keyframes L57-58
     spectral beam ramp          Home.dc.html:126-128 (the mobile DOM port of
                                 the three.js additive caustics, L1102-1121)
     vignette                    Home.dc.html:176
     film grain                  Home.dc.html:177
     smoked optical glass        Home.dc.html:150 (button), :148/:184 (plate),
                                 :180 (menu), :187 (lens)
     brand ramp                  #80EDEF #41BBF5 #6D57FF #A567F1 #D16BFF
                                 #F00EF9 #FFC48C

   Difference from the homepage, on purpose: there the light volumes ship at
   opacity:0 because the desktop three.js layer supplies the colour. These
   sites carry no three.js, so the volumes are lit at every breakpoint.

   Typography is Outfit, which the homepage does NOT use - it is a launch-site
   decision that predates this rebuild and is kept deliberately.
   ============================================================================ */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  width: 100%;
  max-width: 100%;
  /* NO WHOLE-PAGE SCROLL. Both products are applications, not pages: the
     document is exactly one viewport tall and never scrolls. Anything that
     needs more room scrolls INSIDE itself (.panel-body, .drawer nav), which
     is why every such element declares its own overflow. `100%` on both
     html and body is what makes a percentage height inside them resolve;
     100dvh on <main> is what survives a mobile URL bar collapsing. */
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  background: #050507;
  color: #fff;
  font-family: "Outfit", system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}
body { height: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }
img { max-width: 100%; }

/* --- brand ramp ---------------------------------------------------------- */
:root {
  --ptr-cyan:    #80EDEF;
  --ptr-blue:    #41BBF5;
  --ptr-indigo:  #6D57FF;
  --ptr-violet:  #A567F1;
  --ptr-orchid:  #D16BFF;
  --ptr-magenta: #F00EF9;
  --ptr-warm:    #FFC48C;
  --ptr-base:    #050507;
  --ptr-plate:   rgba(10, 11, 16, .58);
  --ptr-hair:    rgba(255, 255, 255, .12);
  --ptr-ink-2:   rgba(255, 255, 255, .64);
  --ptr-ink-3:   rgba(255, 255, 255, .44);
}

/* ============================================================================
   1. BACKDROP  (fixed, behind everything, never interactive)
   ============================================================================ */
.ptr-field {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background: #050507;
}
/* The raw-WebGL field. Textureless by design - see backdrop.js. */
#cinBg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.ptr-field > .lv {
  position: absolute;
  mix-blend-mode: screen;
  will-change: transform;
  border-radius: 50%;
}
#lvCyan {
  left: -18%; top: -24%; width: 58%; height: 70%;
  background: radial-gradient(closest-side, rgba(128,237,239,.85), rgba(65,187,245,.45) 48%, transparent 72%);
  filter: blur(90px);
  opacity: .18;
  animation: lvA 14s ease-in-out infinite;
}
#lvViolet {
  right: -20%; top: -10%; width: 54%; height: 74%;
  background: radial-gradient(closest-side, rgba(109,87,255,.9), rgba(109,87,255,.4) 50%, transparent 74%);
  filter: blur(95px);
  opacity: .16;
  animation: lvB 12s ease-in-out infinite;
}
#lvPink {
  left: 14%; bottom: -30%; width: 60%; height: 64%;
  background: radial-gradient(closest-side, rgba(165,103,241,.85), rgba(209,107,255,.4) 48%, transparent 72%);
  filter: blur(85px);
  opacity: .13;
  animation: lvC 16s ease-in-out infinite;
}
/* Caustic streaks. The full spectral ramp is the one the three.js caustic
   texture paints (Home.dc.html:1102-1113), ported to a linear-gradient. */
#caus1 {
  position: absolute;
  left: -15%; top: 10%; width: 130%; height: 9%;
  background: linear-gradient(90deg,
    rgba(128,237,239,0) 0%, rgba(128,237,239,.55) 14%, rgba(65,187,245,.62) 34%,
    rgba(165,103,241,.62) 55%, rgba(240,14,249,.55) 74%, rgba(255,196,140,.28) 86%,
    rgba(255,196,140,0) 100%);
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 100%);
  transform: rotate(-16deg);
  filter: blur(24px);
  mix-blend-mode: screen;
  opacity: .5;
  animation: lvD 11s ease-in-out infinite;
  will-change: transform;
}
#caus2 {
  position: absolute;
  right: -20%; bottom: 16%; width: 130%; height: 7%;
  background: linear-gradient(90deg,
    rgba(128,237,239,0) 0%, rgba(65,187,245,.5) 18%, rgba(109,87,255,.6) 40%,
    rgba(209,107,255,.55) 62%, rgba(240,14,249,.4) 80%, rgba(255,196,140,0) 100%);
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 100%);
  transform: rotate(12deg);
  filter: blur(20px);
  mix-blend-mode: screen;
  opacity: .44;
  animation: lvE 13s ease-in-out infinite;
  will-change: transform;
}
/* Vignette - Home.dc.html:176 */
.ptr-vignette {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(120% 100% at 50% 50%, transparent 55%, rgba(0,0,0,.42) 100%);
}
/* Film grain - Home.dc.html:177, 160x160 fractalNoise tile, baseFrequency .9 */
.ptr-grain {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  opacity: .05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
}

@keyframes lvA { 0%,100% { transform: translate3d(-6%,-4%,0) scale(1); } 50% { transform: translate3d(7%,5%,0) scale(1.12); } }
@keyframes lvB { 0%,100% { transform: translate3d(5%,6%,0) scale(1.08); } 50% { transform: translate3d(-7%,-5%,0) scale(.96); } }
@keyframes lvC { 0%,100% { transform: translate3d(3%,-6%,0) scale(.98); } 50% { transform: translate3d(-5%,6%,0) scale(1.1); } }
@keyframes lvD { 0%,100% { transform: rotate(-16deg) translate3d(-6%,0,0); opacity: .22; } 50% { transform: rotate(-14deg) translate3d(8%,2%,0); opacity: .34; } }
@keyframes lvE { 0%,100% { transform: rotate(12deg) translate3d(6%,0,0); opacity: .2; } 50% { transform: rotate(10deg) translate3d(-8%,-2%,0); opacity: .3; } }
@keyframes lensBreath { 0%,100% { transform: scale(1); } 50% { transform: scale(1.015); } }
/* THE 12px HERE IS LOAD BEARING FOR THE SHELL, not just decoration: a .panel is
   a whole viewport tall, so while this runs the panel's bottom edge is 12px
   below where it lands. The shell's bottom padding must be at least this much
   or the panel pokes out of the viewport for the length of the animation - see
   the laptop band's `main` padding. Change one, change the other. */
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes spin { to { transform: rotate(360deg); } }

/* ============================================================================
   2. SMOKED OPTICAL GLASS  (Home.dc.html:150 / :148 / :180 / :184 / :187)
   ============================================================================ */
.glass-btn, .enter-btn, .primary-btn, .icon-btn, .mic-btn {
  border: 1px solid rgba(255,255,255,.16);
  border-radius: 16px;
  background:
    radial-gradient(130% 100% at 28% 18%, rgba(128,237,239,.18), transparent 55%),
    radial-gradient(140% 110% at 78% 88%, rgba(109,87,255,.28), transparent 62%),
    rgba(8,9,14,.76);
  -webkit-backdrop-filter: blur(16px);
          backdrop-filter: blur(16px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.18),
    inset 0 -8px 20px rgba(109,87,255,.14),
    0 0 22px rgba(65,187,245,.12);
  color: #fff;
  cursor: pointer;
  transition: box-shadow .3s ease, transform .3s ease, opacity .2s ease;
}
.glass-btn:hover:not(:disabled),
.enter-btn:hover:not(:disabled),
.primary-btn:hover:not(:disabled),
.icon-btn:hover:not(:disabled),
.mic-btn:hover:not(:disabled) {
  transform: translateY(-2px) scale(1.02);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.28),
    inset 0 -10px 26px rgba(128,237,239,.20),
    0 0 36px rgba(109,87,255,.30);
}
.glass-btn:disabled, .enter-btn:disabled, .primary-btn:disabled,
.icon-btn:disabled, .mic-btn:disabled { opacity: .38; cursor: not-allowed; }
.glass-btn { padding: 10px 15px; font-size: 14px; }
.primary-btn { padding: 12px 20px; font-weight: 600; }
.icon-btn { width: 42px; height: 42px; display: grid; place-content: center; }
:where(button, a, input, textarea, select):focus-visible {
  outline: 2px solid rgba(128,237,239,.75);
  outline-offset: 2px;
}

/* Smoked plate: the container form of the same material, no gradients. */
.plate {
  border: 1px solid var(--ptr-hair);
  border-radius: 24px;
  background: var(--ptr-plate);
  -webkit-backdrop-filter: blur(18px);
          backdrop-filter: blur(18px);
  box-shadow: 0 18px 44px rgba(0,0,0,.42);
}

/* ============================================================================
   3. CHROME  (menu affordance, drawer, dock)
   ============================================================================ */
/* The menu affordance and the overlay are COPIED from the People Tech
   Revolution site (ptr-website-html @ design, public/TopNav.dc.html) so the
   three sites open the same menu. Sizes, radii, colours, type ramp, dividers
   and the legal row are that file's, not new ones:
     button   44x44, radius 14, rgba(255,255,255,.06) on a .16 hairline,
              two 18x2 bars with a 5px gap                     (TopNav :30-33)
     overlay  inset 0, z-index 890, rgba(6,6,9,.9), blur(28px) (TopNav :38)
     close    44x44, radius 14, transparent, 1px .2 hairline   (TopNav :40)
     links    clamp(30px, 5.6vw, 60px), weight 600, -0.02em    (TopNav :43)
     rules    1px rgba(255,255,255,.14), max-width 360px       (TopNav :46)
     legal    clamp(16px, 1.2vw, 18px), rgba(255,255,255,.78)  (TopNav :53) */
/* THE APP TOP ROW: Digital Twin at the left, PTR + product centred, menu at
   the right. A three-column grid with fixed 44px rails rather than a flex row,
   because the centre column must be centred against the VIEWPORT, not against
   whatever the two side controls happen to measure - with flex, a wider left
   control silently pushes the title off centre. */
.topbar {
  position: fixed; inset: 0 0 auto 0; z-index: 30;
  padding: max(10px, env(safe-area-inset-top)) clamp(14px, 3vw, 32px) 0;
  display: grid; grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center; gap: 10px;
  pointer-events: none;
}
/* The bar itself is click-through so the field behind it stays interactive;
   each control opts back in. */
.topbar > * { pointer-events: auto; }
/* The centred title is the APP's identity, so it appears only once the visitor
   is inside. Before that the landing already carries the brand at hero size and
   printing the product name twice on one screen looks like a mistake. */
.topbrand {
  display: none; justify-self: center;
  align-items: center; gap: 9px;
  text-decoration: none; color: #fff;
  min-width: 0;
}
body.unlocked .topbrand { display: flex; }
.topbrand img { width: 22px; height: 22px; object-fit: contain; flex: none; }
.topbrand span {
  font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  font-size: clamp(11px, 2.4vw, 14px);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.menu-btn {
  /* GRID-COLUMN 3 IS LOAD-BEARING. .topbrand is display:none until the
     visitor is inside, so auto-placement drops this into the middle 1fr
     track and `justify-self:end` then ends 54px short of the right edge,
     visibly out of line with the Didymo mark opposite it. */
  pointer-events: auto; justify-self: end; grid-column: 3;
  width: 44px; height: 44px;
  border-radius: 14px;
  border: 1px solid rgba(255,255,255,.16);
  background: rgba(255,255,255,.06);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 5px;
  padding: 0;
  cursor: pointer;
}
.menu-btn span { display: block; width: 18px; height: 2px; background: #fff; }

/* The overlay IS the menu: one full-screen element, no side panel and no
   separate backdrop, exactly as the main site does it. */
.drawer {
  position: fixed; inset: 0; z-index: 890;
  background: rgba(6,6,9,.9);
  -webkit-backdrop-filter: blur(28px); backdrop-filter: blur(28px);
  color: #fff;
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none;
  transition: opacity .25s ease;
}
body.menu-open .drawer { opacity: 1; pointer-events: auto; }
/* The overlay takes focus as a container so a screen reader lands inside it.
   It is not an interactive control, so it does not draw a focus ring. */
.drawer:focus { outline: none; }
/* The head carries the way home on the left and the close on the right, the
   same arrangement the main site's bar uses (logo left, controls right). Putting
   the PTR mark here rather than adding a ninth big link is what keeps the item
   list, and therefore the overlay's height, identical to the main site's. */
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px clamp(20px, 4vw, 56px);
}
.drawer-home { display: flex; align-items: center; }
.drawer-home img { width: 46px; height: 46px; object-fit: contain; display: block; }
.drawer-home:hover { opacity: .75; }
.drawer-close {
  width: 44px; height: 44px;
  border-radius: 14px;
  border: 1px solid rgba(255,255,255,.2);
  background: transparent;
  color: #fff; font-size: 18px; line-height: 1; cursor: pointer;
}
.drawer nav {
  flex: 1;
  display: flex; flex-direction: column;
  /* SAFE centring, not plain centring. A centred flex column that overflows
     spills equally at BOTH ends, and the overflow above the start is
     unreachable by scrolling - the first item is simply cut off and cannot be
     got back. Measured at 1440x900 with one extra item: the top link was
     clipped and no amount of scrolling revealed it. `safe center` falls back to
     flex-start the moment the content stops fitting, so nothing is ever
     unreachable. */
  justify-content: safe center;
  gap: clamp(8px, 1.8vh, 18px);
  padding: 0 clamp(24px, 8vw, 120px) clamp(12px, 2vh, 20px);
  overflow-y: auto;
}
.drawer nav a,
.drawer nav .menu-link {
  font-size: clamp(30px, 5.6vw, 60px);
  /* The main site's ramp assumes Helvetica. Outfit's default line box is taller
     at the same size (76px vs 72px per link), which is 32px across eight links
     and was enough to push the legal row past the fold at 1440x900. Pinning the
     line height keeps the ramp identical and the block the same height. */
  line-height: 1.08;
  font-weight: 600; letter-spacing: -.02em;
  color: #fff; text-decoration: none;
  background: none; border: 0; padding: 0; text-align: left;
  font-family: inherit; cursor: pointer;
  transition: opacity .2s ease;
}
.drawer nav a:hover, .drawer nav .menu-link:hover { opacity: .75; }
.drawer nav a[aria-current="page"] { opacity: .55; cursor: default; }
.drawer .ext { font-size: .5em; vertical-align: .35em; opacity: .6; }
.drawer .rule {
  height: 1px; background: rgba(255,255,255,.14);
  margin: clamp(6px, 1.2vh, 12px) 0; max-width: 360px;
}
.drawer .rule.wide { margin: clamp(8px, 1.6vh, 16px) 0; }
.drawer .legal { display: flex; gap: 24px; flex-wrap: wrap; align-items: center; }
.drawer .legal a {
  font-size: clamp(16px, 1.2vw, 18px);
  font-weight: 400; letter-spacing: normal;
  color: rgba(255,255,255,.78); text-decoration: none;
}
.drawer .legal a:hover { opacity: .75; }

/* THE DIDYMO LENS, and nothing else. peopletechrevolution.com floats a single
   round lens bottom-right that opens a four-option rail; it does not carry a
   nav bar. The Play/Practice/Create buttons that used to sit here duplicated
   the hero words, which already switch panels.
   Geometry copied from Home.dc.html:187 - 44x44, round, the two-gradient
   smoked glass over rgba(8,9,14,.6), a 24px mark. */
/* ONE Digital Twin entry point, and it lives at the top left of the app row.
   It used to float at the bottom right like the homepage lens; the app shell
   has a real header now, and two twin surfaces on one screen would be two
   entry points to the same thing. The rail drops DOWN from the button because
   there is nothing above it. */
/* THE LENS MOVED RIGHT AND THE MENU MOVED LEFT, on the owner's call. The rail
   below has to follow: anchored `left: 0` it would hang off the right edge of a
   phone once its button sits in the last grid column. */
.lens-wrap { position: relative; justify-self: start; grid-column: 1; }
.lens {
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.14);
  background:
    radial-gradient(130% 100% at 28% 18%, rgba(128,237,239,.18), transparent 55%),
    radial-gradient(140% 110% at 78% 88%, rgba(109,87,255,.28), transparent 62%),
    rgba(8,9,14,.6);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18),
              inset 0 -8px 20px rgba(109,87,255,.14),
              0 0 22px rgba(65,187,245,.12);
  animation: lensBreath 4.5s ease-in-out infinite;
  display: flex; align-items: center; justify-content: center;
  padding: 0; cursor: pointer;
  transition: box-shadow .35s ease, transform .35s ease;
}
.lens:hover {
  transform: translateY(-1px) scale(1.04);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.24),
              inset 0 -10px 22px rgba(128,237,239,.2),
              0 0 38px rgba(109,87,255,.36);
}
.lens img { width: 24px; height: 24px; object-fit: contain; }

.lens-rail {
  position: absolute; top: 54px; left: 0; right: auto; z-index: 31;
  display: flex; gap: 8px;
  padding: 8px;
  border: 1px solid var(--ptr-hair);
  border-radius: 20px;
  background: rgba(10,11,16,.72);
  -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px);
  box-shadow: 0 20px 60px rgba(0,0,0,.45), inset 0 0 24px rgba(128,237,239,.04);
  animation: rise .25s ease;
}
.lens-rail[hidden] { display: none; }
.lens-opt {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  min-width: 60px; padding: 10px 8px;
  border-radius: 14px;
  color: #fff; text-decoration: none;
  font-size: 12px; letter-spacing: .04em;
  transition: background .2s ease;
}
.lens-opt:hover { background: rgba(255,255,255,.07); }
.lens-ico {
  width: 17px; height: 17px; flex: none; background: currentColor;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain;   mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.lens-ico[data-lens-key="chat"]   { -webkit-mask-image: var(--ptr-icon-chat);   mask-image: var(--ptr-icon-chat); }
.lens-ico[data-lens-key="voice"]  { -webkit-mask-image: var(--ptr-icon-voice);  mask-image: var(--ptr-icon-voice); }
.lens-ico[data-lens-key="avatar"] { -webkit-mask-image: var(--ptr-icon-avatar); mask-image: var(--ptr-icon-avatar); }
.lens-ico[data-lens-key="book"]   { -webkit-mask-image: var(--ptr-icon-book);   mask-image: var(--ptr-icon-book); }
@media (max-width: 420px) {
  .lens-opt { min-width: 54px; padding: 9px 6px; font-size: 11px; }
}

/* ============================================================================
   4. HERO
   ============================================================================ */
/* THE SHELL. One viewport, never taller. Before the gate this is a centred
   hero; after it, a fixed grid of tab row + stage. `100dvh` and not `100vh`
   because on mobile the small viewport unit is what stays correct while the
   URL bar collapses - `100vh` there is taller than the screen and reintroduces
   exactly the scroll this shell exists to remove. */
main {
  position: relative; z-index: 2;
  /* 100vh FIRST as the fallback, then 100dvh for browsers that have it. A probe
     found this had only the dvh line: a browser without dvh dropped the whole
     declaration, main fell back to auto height, and the entire no-scroll shell
     came apart. Measured with a 60px URL bar emulated, svh=752 against
     lvh=dvh=812, so the vh fallback stands 60px past the screen - imperfect, and
     enormously better than no height at all. */
  height: 100vh;
  height: 100dvh;
  padding: calc(58px + env(safe-area-inset-top)) clamp(14px, 3vw, 24px)
           max(14px, env(safe-area-inset-bottom));
  display: grid; place-items: center; align-content: center;
  overflow: hidden;
}
/* The landing can be taller than a short phone in landscape. It scrolls
   INSIDE the shell rather than making the document scroll. */
.landing { max-height: 100%; overflow-y: auto; overscroll-behavior: contain; }

/* --- unlocked: the shell becomes an app ---------------------------------- */
/* `minmax(0, 1fr)` and not `1fr`: a grid track's default minimum is its
   content, so a plain `1fr` stage grows to fit its contents and pushes the
   shell past one viewport. The zero minimum is what actually pins it. */
body.unlocked main { align-content: stretch; place-items: stretch; }
body.unlocked .landing {
  height: 100%; min-height: 0; overflow: hidden;
  grid-template-rows: auto minmax(0, 1fr); gap: 10px;
  /* THE APP IS NOT THE HERO. `min(1120px, 100%)` is a good measure for the
     landing, where the text wants a readable line length. Once the visitor is
     inside, the same cap left roughly 800px of empty backdrop on a 1920 screen
     while the conversation sat in a 1120px column. Measured before this: the
     panel was 1120px wide at 1920, 1440 AND 1280, identical at all three.
     A wider cap for the working surface only; the landing keeps its measure. */
  width: min(1600px, 100%);
}
body.unlocked .explore { min-height: 0; height: 100%; }
/* The tab row sits immediately under the header and stays compact: it is
   chrome now, not the hero it was on the landing. */
body.unlocked .modes { font-size: clamp(12px, 2.9vw, 16px); gap: 6px 14px; }
body.unlocked .mode-word.active::after { bottom: -5px; }
.landing { width: min(1120px, 100%); margin-inline: auto; display: grid; justify-items: center; gap: 22px; text-align: center; }
.brand { display: flex; align-items: center; justify-content: center; gap: 14px; }
.brand img { width: 58px; height: 58px; object-fit: contain; }
.brand-name {
  font-size: clamp(42px, 9vw, 88px);
  font-weight: 600; letter-spacing: -.045em; line-height: .92;
}
.modes {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 12px 20px;
  font-size: clamp(17px, 3.2vw, 28px);
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,.34);
}
.mode-word {
  position: relative; border: 0; background: none; color: inherit;
  text-transform: uppercase; letter-spacing: inherit; padding: 0; cursor: pointer;
}
.mode-word.active { color: #fff; text-shadow: 0 0 22px rgba(128,237,239,.2); }
.mode-word.active::after {
  content: ""; position: absolute; left: 8%; right: 8%; bottom: -8px; height: 2px;
  background: linear-gradient(90deg, #80EDEF, #41BBF5, #6D57FF, #A567F1);
  border-radius: 99px; box-shadow: 0 0 18px rgba(65,187,245,.25);
}
.typed { min-height: 34px; font-size: clamp(18px, 3.3vw, 26px); font-weight: 300; color: rgba(255,255,255,.82); }

/* ============================================================================
   5. ENTRY GATE
   ============================================================================ */
.gate { width: min(650px, 100%); padding: 18px; text-align: left; }
.gate .fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.gate input {
  height: 54px;
  border: 1px solid rgba(255,255,255,.16);
  border-radius: 14px;
  background: rgba(255,255,255,.04);
  color: #fff; padding: 0 16px; outline: none; width: 100%;
}
.gate input::placeholder { color: rgba(255,255,255,.38); }
.gate input:focus { border-color: rgba(128,237,239,.7); box-shadow: 0 0 0 3px rgba(65,187,245,.15); }
.gate .wide { grid-column: 1 / -1; }
.enter-btn { width: 100%; height: 55px; margin-top: 12px; font-weight: 700; }
.micro { margin: 10px 0 0; color: var(--ptr-ink-3); font-size: 12px; line-height: 1.5; }

/* The authorized privacy notice inside the entry gate.
   It is a LEGAL NOTICE that has to be readable, not decoration, so it does not
   inherit .micro's dimmest ink: it takes ink-2 at 13px, matching the corporate
   lead forms it is copied from. The link is underlined for the same reason it
   is there at all - it has to look like a link without a hover.
   Never set display:none, visibility:hidden, opacity:0 or a zero height on
   this. functions/lib/crm.js reports that this notice was PRESENTED, and a
   rule that hides it would turn that report into a false statement. */
.gate-notice { margin: 12px 0 0; color: var(--ptr-ink-2); font-size: 13px; }
.gate-notice a { color: inherit; text-decoration: underline; }
.gate-notice a:hover { color: #fff; }

.gate-error {
  margin: 10px 0 0; padding: 10px 12px;
  border: 1px solid rgba(240,14,249,.35); border-radius: 12px;
  background: rgba(240,14,249,.08); color: #fff; font-size: 14px;
}
.gate-error:empty { display: none; }

/* ============================================================================
   6. PANELS
   ============================================================================ */
.explore { width: 100%; display: none; text-align: left; }
body.unlocked .explore { display: block; }
/* Inside the app the header carries the identity, so the hero lockup goes with
   the gate and the typed line. It used to merely shrink, which printed the
   product name twice on one screen - once at 64px in the body and again in the
   top row - and ate the vertical space the stage needs. */
body.unlocked .gate, body.unlocked .typed, body.unlocked .brand { display: none; }

.panel { display: none; animation: rise .35s ease; }
/* An active panel is itself a two-row grid: a fixed head and a stage that
   takes the rest. Only .panel-body scrolls. */
.panel.active { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; height: 100%; }
/* THE STAGE IS A COLUMN THAT FITS.
   A flex column is what lets every stage be sized from the space that is LEFT
   instead of from its own content, and a child sized from what remains cannot
   overshoot its parent by construction. It replaces the `height: 100%` the
   stages used to carry, which was measured against whatever happened to be
   above it and silently became `auto` whenever a wrapper in between had no
   height of its own (#createHost on PTR Labs was exactly that: an unstyled
   div, so `.create-layout { height: 100% }` resolved to auto and the panel
   grew to 1596px inside a 900px body at 1920x1080).
   `overflow-y: auto` stays as the last-resort safety net, not the plan: with
   the stages sized from the remainder it should never have anything to
   scroll, which is what the alltabs probe asserts. */
.panel-body {
  display: flex; flex-direction: column;
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  /* A default platform scrollbar is a grey system chrome bar down the middle of
     a dark glass app. Keep the scroll, lose the bar. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,.18) transparent;
  padding-right: 2px;
}
.panel-body::-webkit-scrollbar { width: 8px; }
.panel-body::-webkit-scrollbar-track { background: transparent; }
.panel-body::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.16);
  border-radius: 99px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.panel-body::-webkit-scrollbar-thumb:hover { background-color: rgba(255,255,255,.28); background-clip: content-box; }
.panel-head { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 6px 2px 10px; }
.panel-head h2 { margin: 4px 0 0; font-size: clamp(19px, 3vw, 30px); letter-spacing: -.035em; }
.panel-head p { margin: 0; color: var(--ptr-ink-2); max-width: 520px; text-align: right; font-size: 14px; }
/* Below this the descriptive line is competing with the stage for rows it
   cannot spare, and the stage is the product. */
@media (max-width: 900px), (max-height: 720px) {
  .panel-head p { display: none; }
  .panel-head h2 { font-size: clamp(17px, 4.4vw, 22px); }
}
.eyebrow { font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: rgba(255,255,255,.6); }

.card {
  border: 1px solid rgba(255,255,255,.09);
  border-radius: 24px;
  background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02));
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  padding: 20px;
  box-shadow: 0 16px 40px rgba(0,0,0,.22);
}
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
/* A tab row is chrome: it takes exactly the height it needs and never gives any
   of it back to the stage, and never takes any of the stage's either. */
.panel-body > .tabs { flex: none; }
.tabs button, .tabs .tab-link {
  border: 1px solid rgba(255,255,255,.1); border-radius: 14px;
  background: rgba(255,255,255,.035); color: rgba(255,255,255,.62);
  padding: 11px 14px; cursor: pointer;
}
.tabs button.active { background: rgba(255,255,255,.075); color: #fff; border-color: rgba(255,255,255,.2); }
/* An action in a tab row that LEAVES for the shared twin. Same shape as its
   neighbours because it is the same kind of choice, but it is an anchor and it
   carries the external mark, so it never pretends to be an in-page tab. */
.tabs .tab-link {
  display: inline-flex; align-items: center; gap: 6px;
  text-decoration: none;
}
.tabs .tab-link .ext { font-size: .8em; opacity: .7; }
.stage-copy h3 { font-size: 26px; margin: 8px 0; }
.stage-copy p { margin: 0 0 18px; color: var(--ptr-ink-2); }
.status-row {
  display: flex; align-items: center; gap: 14px; justify-content: space-between;
  flex-wrap: wrap; margin-bottom: 12px; color: rgba(255,255,255,.65);
}
.status-row strong { color: #fff; }

/* Media surfaces */
/* AN ASPECT RATIO IS A PREFERENCE, NOT A FLOOR.
   `aspect-ratio` alone makes the height a function of the WIDTH, so a surface
   in a wide column demands a height nobody has: measured at 1920x1080 the PLAY
   iframe asked for 966px and the CREATE sprite preview for 1445px inside a
   900px stage. `flex: 0 1 auto` plus `min-height: 0` lets the column it sits in
   shrink it to what is actually left; the ratio still decides the shape
   whenever there IS room. The content letterboxes instead of stretching (see
   the object-fit rule below), so a shrunk surface reads as a viewer with
   margins rather than a squashed picture. */
.surface {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border-radius: 18px; border: 1px solid rgba(255,255,255,.1);
  background: #08090e; overflow: hidden; display: block;
  flex: 0 1 auto; min-height: 0;
}
.surface > video, .surface > canvas, .surface > iframe {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block;
}
.surface > video { object-fit: cover; }
/* A canvas carries its own intrinsic size from its width/height attributes, so
   `contain` centres the drawing and letterboxes the rest the moment the surface
   is shrunk below the ratio. Without it the drawing stretches, which on the
   CREATE sprite (32:40, a PORTRAIT box in a landscape column) is the difference
   between a character and a smear. `object-fit` has no effect on an <iframe>,
   which is not a replaced element with an intrinsic ratio: a framed product
   simply gets a wider, shorter window and lays itself out for it. */
.surface > canvas { object-fit: contain; }
.mirror > video { transform: scaleX(-1); }

/* Permission / loading / error states */
.state-overlay {
  position: absolute; inset: 0; z-index: 3;
  display: grid; place-content: center; justify-items: center; gap: 12px;
  padding: 24px; text-align: center;
  background: rgba(5,5,7,.82);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
.state-overlay[hidden] { display: none; }
.state-overlay h4 { margin: 0; font-size: 19px; font-weight: 600; }
.state-overlay p { margin: 0; max-width: 420px; color: var(--ptr-ink-2); font-size: 14px; line-height: 1.5; }
.spinner {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.18); border-top-color: var(--ptr-cyan);
  animation: spin .8s linear infinite;
}

/* Just-in-time capture consent */
.consent-backdrop {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center; padding: 20px;
  background: rgba(5,5,7,.72);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.consent-card {
  width: min(460px, 100%);
  padding: 22px; border-radius: 20px;
  border: 1px solid rgba(255,255,255,.12);
  background: rgba(18,18,24,.96);
  box-shadow: 0 24px 60px rgba(0,0,0,.55);
}
.consent-card h4 { margin: 0 0 10px; font-size: 18px; font-weight: 600; }
.consent-card p { margin: 0; color: var(--ptr-ink-2); font-size: 14px; line-height: 1.55; }
.consent-card a { color: var(--ptr-cyan); }
.consent-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; flex-wrap: wrap; }

/* Chat surfaces */
.messages {
  /* 340px is the FALLBACK for a browser without :has(). Where :has() works, the
     rule below makes this pane flexible instead, which is what fixes both ends
     of the same bug: on a desktop the fixed height left a wide band of dead
     space under the conversation, and on a phone it forced the card taller than
     the panel and made the whole app scroll. */
  height: 340px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  margin: 16px 0; padding-right: 4px;
}
/* The card that holds a conversation becomes a column: head, flexible message
   pane, pinned composer. The pane takes `flex: 1 1 0` so it FILLS a tall
   desktop card and COLLAPSES on a short phone rather than pushing the composer
   out of view. Its own overflow-y keeps the scrolling inside the pane, which is
   where it belongs, instead of on the document. */
.card:has(> .messages) { display: flex; flex-direction: column; min-height: 0; }
.card:has(> .messages) > .messages { flex: 1 1 0; height: auto; min-height: 0; margin-top: 0; }
.messages > :first-child { margin-top: auto; }
.msg { max-width: 84%; padding: 12px 14px; border-radius: 16px; line-height: 1.45; white-space: pre-wrap; }
.msg.ai { align-self: flex-start; background: rgba(255,255,255,.065); border: 1px solid rgba(255,255,255,.08); }
.msg.user { align-self: flex-end; background: rgba(128,237,239,.09); border: 1px solid rgba(128,237,239,.12); }
.msg.pending { opacity: .6; font-style: italic; }
.msg.err { background: rgba(240,14,249,.09); border-color: rgba(240,14,249,.28); }
.composer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; }
.composer input, .composer textarea {
  height: 50px; border-radius: 15px;
  border: 1px solid rgba(255,255,255,.12);
  background: rgba(255,255,255,.04); color: #fff; padding: 0 14px; outline: none;
  /* MIN-WIDTH 0 IS LOAD BEARING. A grid track of `1fr` will not shrink below its
     item's intrinsic minimum, and a text input's intrinsic minimum is its size
     attribute, roughly 20 characters. Without this the composer row refused to
     get narrower than about 300px, so on a 390px phone the Send button was
     pushed off the right edge and the page scrolled sideways. */
  min-width: 0;
}
.composer input:focus { border-color: rgba(128,237,239,.55); }
.mic-btn { width: 50px; height: 50px; border-radius: 15px; display: grid; place-content: center; }
.mic-btn.live {
  border-color: rgba(240,14,249,.5);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.28), 0 0 30px rgba(240,14,249,.35);
}
/* A MICROPHONE, NOT A DOT. This was a 12px filled circle, which reads as a
   record-light or a bullet and told nobody what the button does. The glyph is a
   CSS mask so it inherits `currentColor` and keeps working in both the idle and
   the live state without a second asset, and so the markup stays
   `<span class="mic-dot">` in every surface that already ships it. */
.mic-dot {
  width: 20px; height: 20px;
  background: currentColor;
  border-radius: 0;
  -webkit-mask: var(--ptr-icon-mic) center / contain no-repeat;
          mask: var(--ptr-icon-mic) center / contain no-repeat;
}
.mic-btn.live .mic-dot { background: #F00EF9; animation: lensBreath 1s ease-in-out infinite; }

/* ---------------------------------------------------------------- icons ----
   Inline SVG masks rather than an icon font or four <img> requests: they
   inherit currentColor, so a tab that is active, hovered or disabled gets the
   right colour for free, and they add no network cost. The geometry is copied
   VERBATIM from peopletechrevolution.com (public/Didymo.dc.html: msgCircle,
   phone, video, cal, mic) so the launch sites and the corporate site cannot
   drift apart: same 24 box, stroke weight 2, round caps and joins. */
:root {
  --ptr-icon-mic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z'/%3E%3Cpath d='M19 10v2a7 7 0 0 1-14 0v-2M12 19v3'/%3E%3C/svg%3E");
  --ptr-icon-chat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z'/%3E%3C/svg%3E");
  --ptr-icon-voice: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z'/%3E%3C/svg%3E");
  --ptr-icon-avatar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m22 8-6 4 6 4V8z'/%3E%3Crect x='1' y='6' width='14' height='12' rx='2'/%3E%3C/svg%3E");
  --ptr-icon-book: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E");
}
.tabs [data-connect-key]::before {
  content: ""; width: 17px; height: 17px; flex: none;
  background: currentColor;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain;   mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.tabs [data-connect-key="chat"]::before   { -webkit-mask-image: var(--ptr-icon-chat);   mask-image: var(--ptr-icon-chat); }
.tabs [data-connect-key="voice"]::before  { -webkit-mask-image: var(--ptr-icon-voice);  mask-image: var(--ptr-icon-voice); }
.tabs [data-connect-key="avatar"]::before { -webkit-mask-image: var(--ptr-icon-avatar); mask-image: var(--ptr-icon-avatar); }
.tabs [data-connect-key="book"]::before   { -webkit-mask-image: var(--ptr-icon-book);   mask-image: var(--ptr-icon-book); }
.tabs button, .tabs .tab-link { display: inline-flex; align-items: center; gap: 8px; }


.notice {
  margin-top: 12px; padding: 12px 14px;
  border: 1px solid rgba(255,255,255,.1); border-radius: 16px;
  background: rgba(255,255,255,.04); color: rgba(255,255,255,.76); font-size: 14px; line-height: 1.55;
}
.notice h4 { margin: 0 0 6px; font-size: 15px; }
.notice ul { margin: 6px 0 0; padding-left: 18px; }
.notice li { margin: 4px 0; }
.notice[hidden] { display: none; }

.toast {
  position: fixed; left: 50%; bottom: 96px; z-index: 70;
  transform: translate(-50%, 18px); opacity: 0; pointer-events: none;
  background: #fff; color: #0b0d12; border-radius: 13px; padding: 10px 14px;
  font-size: 14px; max-width: calc(100vw - 40px); text-align: center;
  transition: opacity .2s ease, transform .2s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

.hidden { display: none !important; }

/* ============================================================================
   7. RESPONSIVE
   ============================================================================ */
@media (max-width: 850px) {
  main { padding-left: 14px; padding-right: 14px; }
  .gate .fields { grid-template-columns: 1fr; }
  .gate .wide { grid-column: auto; }
  .panel-head { display: block; }
  .panel-head p { text-align: left; margin-top: 7px; }
  .brand img { width: 46px; height: 46px; }
  .messages { height: 300px; }
  .card { padding: 16px; border-radius: 20px; }
}
/* ---------------------------------------------------------------------------
   WIDE BUT SHORT: the laptop band.

   This site had two responsive categories and needed a third. Mobile
   compaction keys off `max-width: 850px`, and the only meaningful short-height
   treatment began at `max-height: 720px`. A 1366x768 laptop is NEITHER, so it
   received the full large-monitor layout with nowhere to put it, which is why
   people were dropping the browser to about 75% zoom to compensate.

   `min-width: 851px` is load-bearing: it keeps this clear of the phone rules,
   so a laptop is never given the mobile treatment. This compacts vertical
   rhythm and the two largest type scales only. Nothing horizontal changes, and
   there is no zoom, no transform and no global font reduction anywhere.

   The 850px ceiling was chosen from measurement rather than taste: the nav is
   clipped by 74px at 1280x720 and 39px at 1366x768, and not at all from
   1440x900 upward, so the band covers exactly the heights that actually fail
   and leaves the large-monitor design untouched.
   --------------------------------------------------------------------------- */
@media (min-width: 851px) and (max-height: 850px) {
  main {
    /* 12px AND NOT 10px, AND THE NUMBER IS NOT A MATTER OF TASTE.
       The panel's entry animation is `rise`, which starts the panel 12px lower
       than its final position (@keyframes rise, :167). A panel is a full
       viewport tall, so for those 350ms its bottom edge sits at
       (shell bottom padding - 12px) BELOW the viewport. Every other breakpoint
       reserves max(14px, ...) and absorbs it; this band reserved 10px, and the
       measured overshoot was exactly 12 - 10 = 2px, on every panel, at 1366x768,
       1280x720 and 980x700, on both sites, and nowhere else. Nine viewports
       agreed with the arithmetic. Keep this at or above the rise distance. */
    padding: calc(46px + env(safe-area-inset-top)) clamp(14px, 3vw, 24px)
             max(12px, env(safe-area-inset-bottom));
  }
  .landing { gap: 14px; }
  .brand img { width: 46px; height: 46px; }
  .brand-name { font-size: clamp(38px, 6.4vw, 62px); }
  .modes { gap: 8px 16px; font-size: clamp(15px, 2.2vw, 20px); }
  .typed { min-height: 28px; font-size: clamp(16px, 2.2vw, 21px); }
  .card { padding: 16px; }
  .panel-head { margin: 2px 2px 8px; }
  .tabs { margin-bottom: 10px; }
}

/* ---------------------------------------------------------------------------
   NARROW PHONE: the mode row is ONE LINE.
   CONNECT CONSULT COLLABORATE wrapped to three lines at 375px, which cost
   about 48px of vertical rhythm and pushed the landing past the viewport.
   Width-driven sizing plus nowrap keeps it on one line without shrinking
   anything on a larger screen.
   --------------------------------------------------------------------------- */
@media (max-width: 520px) {
  .modes { flex-wrap: nowrap; gap: 6px 10px; font-size: clamp(11px, 4vw, 17px); }
  .mode-word { white-space: nowrap; }
}

/* SHORT PHONE: an iPhone SE is 667 tall, not 844. Same idea as the laptop
   band, at the other end of the range. */
@media (max-width: 850px) and (max-height: 700px) {
  main { padding-top: calc(46px + env(safe-area-inset-top)); }
  .landing { gap: 12px; }
  .brand img { width: 38px; height: 38px; }
  .brand-name { font-size: clamp(30px, 8vw, 46px); }
  .typed { min-height: 24px; font-size: clamp(14px, 3.6vw, 18px); }
}

@media (max-width: 380px) {
  main { padding-left: 10px; padding-right: 10px; }
  .card { padding: 13px; }
  /* The lens is already 44px, which is the minimum comfortable touch target.
     It does not shrink further. */
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* A GAME CANVAS MUST NOT BE A DOUBLE-TAP ZOOM TARGET. Nothing in the stylesheet
   claimed the canvas's gestures, so rapid tapping to jump could trigger zoom,
   the text-selection magnifier, or scroll the panel instead. `manipulation`
   drops the double-tap behaviour while leaving ordinary panning alone. */
.surface canvas { touch-action: manipulation; }
/* The ready overlay says "Tap, click, or press Space to start", and it was a
   full-bleed element over the canvas, so a tap anywhere except the button was
   swallowed and nothing happened. The overlay stops intercepting; its own
   controls opt back in. */
.surface > .state-overlay { pointer-events: none; }
.surface > .state-overlay button, .surface > .state-overlay a { pointer-events: auto; }
