* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
  margin: 0; height: 100%;
  background: #0d141c;
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: #dce6f2;
  overflow: hidden;
  overscroll-behavior: none;
}
body.preview-bg {
  background: radial-gradient(120% 90% at 50% 15%, #26313d 0%, #0b0f14 70%);
}

/* layers: camera feed < WebGL canvas < UI overlay */
#camFeed {
  position: fixed; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: none; z-index: 1;
}
#camFeed.show { display: block; }
#glCanvas {
  position: fixed; inset: 0; width: 100% !important; height: 100% !important;
  z-index: 2; touch-action: none;
}

/* ------------------------------------------------------------- landing
   A summoning, not a product page. Near-black, one turning sigil, two words.
   Every explanatory line has been cut: the sigil says what kind of thing this
   is faster than a paragraph could, and whatever is left unexplained is the
   point. Cold light emerging from black reads as a threat; the same blue on a
   lit card reads as a utility. Type is a tracked serif from the system stack —
   the site loads no external fonts and that stays true. */
:root {
  --rite: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  --glow: 130, 205, 255;
}
#landing {
  position: fixed; inset: 0; z-index: 30;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: #05070a;
  overflow-y: auto;
}
/* Something lit, far beneath the floor. Sits under the card, never centred on
   it — the glow rising from below is what makes the frame feel occupied. */
#landing::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(58% 44% at 50% 78%, rgba(var(--glow), 0.13) 0%, rgba(5, 7, 10, 0) 70%),
    radial-gradient(120% 90% at 50% 50%, rgba(5, 7, 10, 0) 38%, rgba(0, 0, 0, 0.92) 100%);
  animation: emberBreath 9s ease-in-out infinite;
}
/* Grain, inlined as a data URI so the page still makes zero outside requests. */
#landing::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.32'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}
@keyframes emberBreath { 50% { opacity: 0.62; } }
#landing.hidden { display: none; }
.landing-card {
  position: relative; z-index: 1;
  max-width: 420px; width: 100%; text-align: center; padding: 28px 8px;
}

/* The idol: the sigil he burns into the floor, arriving early as a promise.
   Seven layers turning at seven rates, none of them a whole multiple of
   another, so the figure never visibly repeats — that is what stops it
   reading as a spinner and starts it reading as a mechanism. */
.sigil {
  position: relative;
  width: 168px; height: 168px; margin: 0 auto 44px;
  animation: rise 1.5s cubic-bezier(.2, .7, .2, 1) both;
}
.sigil .ring, .sigil .sweep, .sigil .tri {
  position: absolute; inset: 0; border-radius: 50%;
}
/* Fine graduations, like a measuring instrument for something unpleasant. */
.graduations {
  background: repeating-conic-gradient(from 0deg,
    rgba(var(--glow), 0.55) 0deg 0.7deg, transparent 0.7deg 5.625deg);
  -webkit-mask: radial-gradient(circle, transparent 0 62%, #000 62% 67%, transparent 67%);
          mask: radial-gradient(circle, transparent 0 62%, #000 62% 67%, transparent 67%);
  animation: spin 61s linear infinite;
}
/* Longer marks, fewer of them, turning the other way against the graduations. */
.runes {
  background: repeating-conic-gradient(from 3deg,
    rgba(var(--glow), 0.78) 0deg 1.6deg, transparent 1.6deg 30deg);
  -webkit-mask: radial-gradient(circle, transparent 0 68%, #000 68% 80%, transparent 80%);
          mask: radial-gradient(circle, transparent 0 68%, #000 68% 80%, transparent 80%);
  animation: spin 43s linear infinite reverse;
}
/* A broken ring — the gap is what makes it feel drawn rather than rendered. */
.arc {
  background: conic-gradient(from 200deg,
    rgba(var(--glow), 0) 0deg, rgba(var(--glow), 0.65) 40deg,
    rgba(var(--glow), 0.65) 250deg, rgba(var(--glow), 0) 290deg);
  -webkit-mask: radial-gradient(circle, transparent 0 46%, #000 46% 48%, transparent 48%);
          mask: radial-gradient(circle, transparent 0 46%, #000 46% 48%, transparent 48%);
  animation: spin 27s linear infinite;
}
/* Slow radar wash. Very faint — it should register as light moving, not as
   a shape of its own. */
.sweep {
  background: conic-gradient(from 0deg,
    rgba(var(--glow), 0.16) 0deg, rgba(var(--glow), 0) 55deg,
    rgba(var(--glow), 0) 360deg);
  -webkit-mask: radial-gradient(circle, #000 0 62%, transparent 62%);
          mask: radial-gradient(circle, #000 0 62%, transparent 62%);
  animation: spin 7.5s linear infinite;
}
/* Two counter-rotating triangles — the hexagram never quite completes. */
.tri {
  inset: 24%;
  border: 1px solid rgba(var(--glow), 0.32);
  border-radius: 0;
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
  animation: spin 39s linear infinite;
}
.tri.alt {
  inset: 26%;
  border-color: rgba(var(--glow), 0.20);
  clip-path: polygon(50% 100%, 0% 0%, 100% 0%);
  animation: spin 33s linear infinite reverse;
}
/* The ember at the centre — the only thing on the page that is truly bright,
   and only just. */
.sigil .core {
  position: absolute; left: 50%; top: 50%; width: 13px; height: 13px;
  margin: -6.5px 0 0 -6.5px; border-radius: 50%;
  background: radial-gradient(circle, #f4fcff 0%, rgba(var(--glow), 0.85) 42%, rgba(var(--glow), 0) 72%);
  box-shadow: 0 0 28px rgba(var(--glow), 0.72), 0 0 74px rgba(var(--glow), 0.34);
  animation: emberBreath 4.4s ease-in-out infinite;
}
/* Embers caught in the field, each on its own orbit and phase. */
.motes { position: absolute; inset: 0; }
.motes b {
  position: absolute; left: 50%; top: 50%; width: 2.5px; height: 2.5px;
  margin: -1.25px 0 0 -1.25px; border-radius: 50%;
  background: rgba(var(--glow), 0.9);
  box-shadow: 0 0 7px rgba(var(--glow), 0.8);
  animation: orbit linear infinite;
}
.motes b:nth-child(1) { --r: 46px; animation-duration: 11s;   animation-delay: -1.2s;  opacity: .85; }
.motes b:nth-child(2) { --r: 62px; animation-duration: 17s;   animation-delay: -6.4s;  opacity: .6; }
.motes b:nth-child(3) { --r: 71px; animation-duration: 23s;   animation-delay: -13.1s; opacity: .75; }
.motes b:nth-child(4) { --r: 55px; animation-duration: 14.5s; animation-delay: -9.7s;  opacity: .5; }
.motes b:nth-child(5) { --r: 79px; animation-duration: 29s;   animation-delay: -3.3s;  opacity: .65; }
.motes b:nth-child(6) { --r: 38px; animation-duration: 9s;    animation-delay: -5.8s;  opacity: .45; }
@keyframes orbit {
  from { transform: rotate(0deg) translateX(var(--r)) rotate(0deg); }
  to   { transform: rotate(360deg) translateX(var(--r)) rotate(-360deg); }
}

/* Charging: the whole idol winds up while the rig is being built, so the wait
   is part of the summoning instead of a word blinking at you. */
#landing.summoning .graduations { animation-duration: 4.5s; }
#landing.summoning .runes       { animation-duration: 3.2s; }
#landing.summoning .arc         { animation-duration: 2.1s; }
#landing.summoning .sweep       { animation-duration: 0.85s; }
#landing.summoning .tri         { animation-duration: 2.8s; }
#landing.summoning .tri.alt     { animation-duration: 2.4s; }
#landing.summoning .motes b     { animation-duration: 1.6s; }
#landing.summoning .sigil {
  animation: charge 2.6s cubic-bezier(.4, 0, .7, 1) infinite;
}
#landing.summoning .core {
  animation: coreCharge 2.6s cubic-bezier(.4, 0, .7, 1) infinite;
}
#landing.summoning::before { animation-duration: 2.6s; }
@keyframes charge {
  0%   { transform: scale(1);    filter: brightness(1); }
  70%  { transform: scale(1.045); filter: brightness(1.55); }
  100% { transform: scale(1);    filter: brightness(1); }
}
@keyframes coreCharge {
  0%   { transform: scale(1);   opacity: .85; }
  70%  { transform: scale(2.1); opacity: 1; }
  100% { transform: scale(1);   opacity: .85; }
}

h1 { margin: 0 0 42px; font-weight: 400; }
h1 span { display: block; font-family: var(--rite); text-transform: uppercase; }
.rite {
  font-size: clamp(23px, 7vw, 30px);
  letter-spacing: 0.52em; text-indent: 0.52em;
  color: #e8f4ff;
  animation: rise 1.5s cubic-bezier(.2, .7, .2, 1) 0.18s both;
}
.subject {
  margin-top: 13px;
  font-size: clamp(10px, 3vw, 11.5px);
  letter-spacing: 0.62em; text-indent: 0.62em;
  color: rgba(var(--glow), 0.42);
  animation: rise 1.5s cubic-bezier(.2, .7, .2, 1) 0.3s both;
}

/* Square, hairline, wide-tracked. A pill would undo the whole page. */
#startBtn {
  appearance: none; cursor: pointer;
  font-family: var(--rite); font-size: 12px; font-weight: 400; line-height: 1;
  letter-spacing: 0.44em; text-indent: 0.44em; text-transform: uppercase;
  color: #dcefff;
  background: transparent;
  border: 1px solid rgba(var(--glow), 0.26);
  border-radius: 0;
  padding: 19px 36px;
  transition: color .45s, border-color .45s, box-shadow .45s;
  animation: rise 1.5s cubic-bezier(.2, .7, .2, 1) 0.44s both;
}
#startBtn:hover:not([disabled]) {
  color: #fff; border-color: rgba(var(--glow), 0.7);
  box-shadow: 0 0 38px rgba(var(--glow), 0.16), inset 0 0 28px rgba(var(--glow), 0.09);
}
#startBtn:active:not([disabled]) { transform: translateY(1px); }
#startBtn[disabled] { opacity: 0.3; cursor: default; }
/* A ring that blooms out of the button when the rite is accepted, so the
   press has a consequence beyond the button changing colour. */
#startBtn { position: relative; }
#startBtn::after {
  content: ''; position: absolute; inset: -1px; pointer-events: none;
  border: 1px solid rgba(var(--glow), 0.55);
  opacity: 0; transform: scale(1);
}
#landing.summoning #startBtn::after { animation: bloom 2.6s cubic-bezier(.2, .6, .3, 1) infinite; }
@keyframes bloom {
  0%   { opacity: .7; transform: scale(1); }
  70%  { opacity: 0;  transform: scale(1.5, 2.1); }
  100% { opacity: 0;  transform: scale(1.5, 2.1); }
}
#landing.summoning #startBtn { color: rgba(var(--glow), .55); border-color: rgba(var(--glow), .12); }

#walkAroundBtn {
  appearance: none; cursor: pointer; display: block;
  margin: 14px auto 0;
  font-family: var(--rite); font-size: 10.5px; line-height: 1;
  letter-spacing: 0.34em; text-indent: 0.34em; text-transform: uppercase;
  color: rgba(var(--glow), 0.5);
  background: transparent; border: 0; border-bottom: 1px solid rgba(var(--glow), 0.18);
  border-radius: 0; padding: 11px 4px;
  transition: color .45s, border-color .45s;
}
#walkAroundBtn:hover { color: #cfe9ff; border-color: rgba(var(--glow), 0.5); }

.mode-note { margin: 18px 0 0; font-size: 11px; letter-spacing: 0.16em; color: #3f5468; }
.mode-note.error {
  color: #e0806c; font-size: 12.5px; letter-spacing: 0.04em; font-family: var(--rite);
}
.loading {
  display: none; margin: 20px auto 0;
  font-family: var(--rite); font-size: 10.5px;
  letter-spacing: 0.5em; text-indent: 0.5em; text-transform: uppercase;
  color: rgba(var(--glow), 0.45);
}
/* The idol now carries the wait, so the word underneath only has to breathe —
   it opens its tracking as the charge builds instead of blinking. */
.loading.show { display: block; animation: loadingDraw 2.6s cubic-bezier(.4, 0, .7, 1) infinite; }
@keyframes loadingDraw {
  0%   { opacity: .35; letter-spacing: 0.5em;  text-indent: 0.5em; }
  70%  { opacity: .95; letter-spacing: 0.78em; text-indent: 0.78em; }
  100% { opacity: .35; letter-spacing: 0.5em;  text-indent: 0.5em; }
}

.qr-panel { display: none; margin: 34px auto 0; }
.qr-panel.show { display: block; animation: rise 1.5s cubic-bezier(.2, .7, .2, 1) 0.6s both; }
/* Kept fully opaque and near-white on purpose. Dimming it sits better on the
   page, but this is a code a phone camera has to read off a screen — scan
   reliability beats the composition here. */
.qr-panel img {
  width: 168px; height: 168px; padding: 9px;
  background: #eef4f8; border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(var(--glow), 0.16), 0 0 44px rgba(var(--glow), 0.10);
}
.qr-panel p {
  margin: 14px 0 0;
  font-family: var(--rite); font-size: 10px;
  letter-spacing: 0.42em; text-indent: 0.42em; text-transform: uppercase;
  color: #3f5468;
}
.fine {
  margin: 12px 0 0; font-family: var(--rite); font-size: 10px;
  letter-spacing: 0.2em; color: #35485a;
}

@keyframes spin { to { transform: rotate(360deg); } }
@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .sigil, .sigil .ring, .sigil .sweep, .sigil .tri, .sigil .core, .motes b,
  #landing::before, #startBtn::after, .rite, .subject, #startBtn,
  .qr-panel.show, .loading.show {
    animation: none !important;
  }
  /* Still show the state, just without the motion. */
  .loading.show { opacity: .8; }
}

/* -------------------------------------------------------- in-experience */
#overlay { position: fixed; inset: 0; z-index: 20; pointer-events: none; display: none; }
#overlay.active { display: block; }
#hint {
  position: absolute; top: max(18px, env(safe-area-inset-top)); left: 50%;
  transform: translateX(-50%);
  max-width: 86%;
  padding: 10px 18px; border-radius: 999px;
  background: rgba(9, 18, 28, 0.72); backdrop-filter: blur(10px);
  color: #e7f2fd; font-size: 14px; text-align: center; white-space: nowrap;
  opacity: 0; transition: opacity 0.25s;
}
#hint.show { opacity: 1; }
.controls {
  position: absolute; left: 0; right: 0;
  bottom: max(26px, env(safe-area-inset-bottom));
  display: flex; align-items: center; justify-content: center; gap: 34px;
  pointer-events: none;
}
.controls button { pointer-events: auto; }
#shutter {
  appearance: none; cursor: pointer;
  width: 74px; height: 74px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  border: 4px solid #fff;
  padding: 0;
  display: none;
}
#shutter span {
  display: block; width: 54px; height: 54px; margin: 0 auto; border-radius: 50%;
  background: #fff; transition: transform 0.1s;
}
#shutter:active span { transform: scale(0.86); }
.round.small {
  appearance: none; cursor: pointer;
  width: 46px; height: 46px; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.4);
  background: rgba(9, 18, 28, 0.55); backdrop-filter: blur(8px);
  color: #fff; font-size: 19px; line-height: 1;
}
#replayBtn { display: none; }
#overlay.placed #shutter { display: block; }
#overlay.placed #replayBtn { display: block; }

/* --------------------------------------------------------- photo modal */
.panel {
  position: absolute; inset: 0;
  display: none; flex-direction: column; align-items: center; justify-content: center;
  gap: 16px; padding: 22px;
  background: rgba(5, 10, 16, 0.88); backdrop-filter: blur(12px);
  pointer-events: auto;
}
.panel.show { display: flex; }
#photoImg {
  max-width: 100%; max-height: 72vh; border-radius: 12px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
}
.panel .row { display: flex; gap: 12px; }
.panel button {
  appearance: none; cursor: pointer;
  font: 600 15px/1 inherit; color: #06121f;
  background: #57c0ff; border: 0; border-radius: 999px;
  padding: 13px 26px;
}
.panel button.ghost {
  background: transparent; color: #cfe0f2;
  border: 1px solid rgba(255, 255, 255, 0.35);
}

/* --------------------------------------------------------------- toast */
#toast {
  position: absolute; left: 50%; bottom: 132px;
  transform: translateX(-50%) translateY(8px);
  max-width: 86%;
  padding: 11px 18px; border-radius: 12px;
  background: rgba(9, 18, 28, 0.85); backdrop-filter: blur(10px);
  color: #e7f2fd; font-size: 13.5px; text-align: center;
  opacity: 0; transition: opacity 0.25s, transform 0.25s;
  pointer-events: none;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
