/* ============================================================================
   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) auto;
  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;
}
/* THE RIGHT-HAND CELL. It used to be the hamburger alone, pinned to column 3;
   it now holds the twin button and the hamburger side by side, so the cell
   carries the placement and the third track is `auto` rather than a fixed
   44px. The reason column 3 was load-bearing has not changed: .topbrand is
   display:none until the visitor is inside, so anything left to auto-placement
   lands in the middle 1fr track and `justify-self:end` then stops short of the
   right edge. */
.topbar-actions {
  grid-column: 3; justify-self: end; pointer-events: auto;
  display: flex; align-items: center; gap: 10px;
}
/* THE TWIN, IN THE BAR, AT EVERY WIDTH AND IN EVERY STATE.
   OWNER, 2026-08-28: "the didymo icon is missing from header navigation that
   is regression". This rule is the fix, and the two notes it replaces are kept
   below in summary because both were right about their own moment.

   IT WAS PHONES-ONLY FIRST (32fd813, `@media (max-width: 700px)`), because the
   canonical widget still drew its own floating corner launcher - 60px of
   permanent furniture over the content, affordable on a laptop and not on a
   390px screen. That split worked only because THE WIDGET TESTS THE ANCHOR'S
   BOX AND NOT ITS EXISTENCE (ptr-digital-twin-widget#29): above 700px this
   button had no box, so the widget handed the corner launcher back.

   THE MEDIA QUERY CAME OFF (7bd3ae7 / PR #100) when the owner had the corner
   launcher removed outright - `launcher="none"` in ptr-shell.js. With no
   corner launcher anywhere, a phones-only bar button would have left every
   desktop visitor with no way to reach the twin at all.

   AND NOW THE `body.unlocked` GATE COMES OFF TOO. That gate was this repo's
   own judgement, not an owner instruction: the reasoning was that before the
   gate "the product is not open, and a twin button painted there would be the
   single control on that screen that did something". The owner has overruled
   it. Three things make that the right call rather than merely the instructed
   one:
     - `launcher="none"` is UNCONDITIONAL, so while this button was
       `body.unlocked`-gated a logged-out visitor had NO route to the twin on
       either launch site. The twin is the one thing on the entry screen that
       can answer a question about what is behind the gate, and it was the one
       thing switched off there.
     - The twin panel genuinely WORKS with no session. It is a self-contained
       widget against didymo, not this site's gated API; it carries its own
       lead gate. Proven, not assumed - see tools/twin-in-the-bar.mjs, which
       opens it from a logged-out page and measures the panel.
     - peopletechrevolution.com already does exactly this. Its #ptrTopNavTwin
       (TopNav.dc.html) is hard-coded `display:flex` with no gate of any kind,
       so a visitor moving between the three PTR surfaces now sees one bar that
       behaves the same way instead of two that hide a control.
   `.topbrand` is still `body.unlocked`-gated and that is NOT an inconsistency:
   it is a wordmark, and the locked screen already prints the wordmark twice. */
.topbar-twin {
  display: flex;
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.16); padding: 0; cursor: pointer;
  align-items: center; justify-content: center;
  background: radial-gradient(circle at 30% 25%, rgba(255,255,255,.18), transparent 46%),
              radial-gradient(circle at 72% 78%, rgba(109,87,255,.42), transparent 62%),
              radial-gradient(circle at 30% 70%, rgba(65,187,245,.3), transparent 58%),
              rgba(8,9,14,.66);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 0 22px rgba(109,87,255,.28);
}
.topbar-twin img { display: block; width: 24px; height: 24px; }
/* NO `body.unlocked .topbar-twin` RULE LIVES HERE ANY MORE, and its absence is
   load-bearing rather than an omission: the base rule above is now the whole
   story, so there is exactly ONE place that decides whether this button is on
   screen. The previous pair (display:none here, display:flex under
   body.unlocked) is what produced the reported regression, and a reinstated
   override would reproduce it silently.
   tools/twin-in-the-bar.mjs fails if this button is not visible on a
   logged-out page, so restoring the gate cannot pass unnoticed. */
.menu-btn {
  pointer-events: auto;
  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; }

/* DRAWER HOVER / TAP UNDERLINE. The same visual the landing carousel already
   shows under its active mode word (.mode-word.active::after above): a 2px
   gradient rule with a 99px radius, an 18px colour glow and an 8% inset. Two
   things differ, and both are the point.
     1. The carousel's rule just appears with .active. Here it FADES in and
        grows from scaleX(.6) to 1 over 550ms, matching the sticky header and
        menu on peopletechrevolution.com so the three launch surfaces read as
        one site.
     2. The carousel uses one four-stop ramp for every word. Here the ramp is
        per link, taken from that site's hero: XR is the cyan pair, AI the
        purple pair, Fast Builds the magenta pair, and everything else carries
        the house ramp - the two ends of the same PTR ramp those pairs are cut
        from. The colours are matched by href, so the markup does not have to
        carry them.
   :active is listed because it is the TAP state - on a touchscreen :hover is
   emulated or never fires at all. */
.drawer nav a,
.drawer nav .menu-link,
.drawer .legal a {
  --hm-a: #80EDEF; --hm-b: #6D57FF; --hm-glow: rgba(109,87,255,.28);
  position: relative;
  /* The nav is a stretched flex column, so without this the rule would run the
     full width of the drawer instead of the width of the word. fit-content
     rather than align-self:flex-start, because .legal is a ROW flex with
     align-items:center that flex-start would silently re-align. */
  width: fit-content;
  transition: opacity .2s ease, text-shadow .55s ease;
}
/* SHRINKING THE BOX TO THE WORD ALSO SHRANK THE TAP TARGET. These links were
   stretched flex children, so the whole row was tappable; fit-content alone
   would have handed that back and left "XR" as a ~39px-wide target on a phone.
   The padding buys the reach back and the negative margin cancels the indent
   it would otherwise add, so every label starts on the same left edge it
   always did. HORIZONTAL ONLY: vertical padding on eight links is what the
   line-height comment above says pushed the legal row past the fold at
   1440x900, and `justify-content: safe center` would then start clipping.
   The underline is inset by the SAME 14px so it still hugs the word rather
   than the padding.
   DIRECT CHILDREN ONLY. `.drawer nav a` also selects the two links inside
   `.legal`, which are a centred ROW and were never stretched, so they need no
   reach back and the negative margin would have pulled the first one out of
   line with the column above it. */
.drawer nav > a,
.drawer nav > .menu-link {
  padding: 0 14px;
  margin-left: -14px;
}
.drawer nav a[href$="/xr"]          { --hm-a: #80EDEF; --hm-b: #41BBF5; --hm-glow: rgba(128,237,239,.28); }
.drawer nav a[href$="/ai"]          { --hm-a: #6D57FF; --hm-b: #A567F1; --hm-glow: rgba(109,87,255,.30); }
.drawer nav a[href$="/fast-builds"] { --hm-a: #D16BFF; --hm-b: #F00EF9; --hm-glow: rgba(240,14,249,.26); }
.drawer nav a::after,
.drawer nav .menu-link::after,
.drawer .legal a::after {
  content: ""; position: absolute; left: 8%; right: 8%; bottom: -2px;
  height: 2px; border-radius: 99px;
  background: linear-gradient(90deg, var(--hm-a), var(--hm-b));
  box-shadow: 0 0 18px var(--hm-glow);
  opacity: 0; transform: scaleX(.6); transform-origin: 50% 50%;
  transition: opacity .55s ease, transform .55s ease;
  pointer-events: none;
}
/* Sits AFTER the base ::after on purpose: same specificity, so ORDER is what
   decides. Inset by the padding, not by 8% of a box the padding widened. */
.drawer nav > a::after,
.drawer nav > .menu-link::after { left: 14px; right: 14px; }
/* The current page's link is already inert - .55 opacity, cursor default. It
   is not a destination, so it gets no underline and no hover lift either. */
.drawer nav a[aria-current="page"]::after { content: none; }
.drawer nav a:not([aria-current="page"]):hover,
.drawer nav a:not([aria-current="page"]):focus-visible,
.drawer nav a:not([aria-current="page"]):active,
.drawer nav .menu-link:hover,
.drawer nav .menu-link:focus-visible,
.drawer nav .menu-link:active,
.drawer .legal a:hover,
.drawer .legal a:focus-visible,
.drawer .legal a:active {
  opacity: 1;
  text-shadow: 0 0 22px var(--hm-glow);
}
.drawer nav a:not([aria-current="page"]):hover::after,
.drawer nav a:not([aria-current="page"]):focus-visible::after,
.drawer nav a:not([aria-current="page"]):active::after,
.drawer nav .menu-link:hover::after,
.drawer nav .menu-link:focus-visible::after,
.drawer nav .menu-link:active::after,
.drawer .legal a:hover::after,
.drawer .legal a:focus-visible::after,
.drawer .legal a:active::after {
  opacity: 1; transform: scaleX(1);
}
@media (prefers-reduced-motion: reduce) {
  .drawer nav a, .drawer nav .menu-link, .drawer .legal a,
  .drawer nav a::after, .drawer nav .menu-link::after, .drawer .legal a::after {
    transition: none;
  }
}

/* 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; }
