/*
 * Positioning contract: every Field object is placed with a single
 * compositor transform in normalised stage coordinates (0..1).
 * --sw / --sh are written once per resize by app.js.
 */

:root {
  font-family: -apple-system, BlinkMacSystemFont, "Hiragino Sans", "Yu Gothic UI", "Segoe UI", sans-serif;
  font-synthesis: none;
  --ink: 17, 17, 17;
  --paper: #f7f7f4;
  --hairline: rgba(17, 17, 17, .08);
  color-scheme: light dark;
}

* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
  min-height: 100dvh;
  background: var(--paper);
  color: rgb(var(--ink));
}
button { font: inherit; color: inherit; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.app-shell {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  padding: max(18px, env(safe-area-inset-top)) 18px max(12px, env(safe-area-inset-bottom));
}

/* ---------- brand: the resonance mark stays a quiet anchor --------------- */

.brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  width: max-content;
  margin: 0 auto;
}
.brand-name {
  display: flex;
  align-items: center;
  font-size: clamp(26px, 6.4vw, 36px);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -.07em;
  opacity: .82;
}
.brand-resonance {
  margin: 0 .13em;
  font-size: .38em;
  font-weight: 450;
  letter-spacing: -.14em;
  opacity: .72;
}
.pronunciation {
  font-size: 9px;
  letter-spacing: .22em;
  color: rgba(var(--ink), .6);
}

/* ---------- field layout: reserved rows so the Field never reflows -------- */

.field {
  display: flex;
  flex-direction: column;
  width: min(100%, 620px);
  flex: 1;
  margin: 0 auto;
}

.anchor {
  margin: clamp(18px, 5vh, 40px) auto 0;
  width: min(90%, 30em);
  min-height: 3.2em;
  text-align: center;
  font-size: clamp(14px, 3.7vw, 17px);
  line-height: 1.6;
  letter-spacing: .02em;
  color: rgba(var(--ink), .64);
  transition: opacity 500ms ease;
}
.field[data-variant="pure"] .anchor { font-size: 13px; color: rgba(var(--ink), .6); }
.field[data-variant="semantic"] .anchor { color: rgba(var(--ink), .82); }
/* the anchor is stale once the phenomenon has spoken for itself; hide it
   rather than leaving an illegible ghost of it on screen */
.field[data-focus] .anchor { opacity: 0; visibility: hidden; }

/* ---------- stage --------------------------------------------------------- */

/* The composition and the language share one surface. The stage never
   resizes: touching a phenomenon must not move the others. Objects occupy
   the upper band (--top/--span); the language surfaces in the lower one. */
.canvas {
  position: relative;
  flex: 1 1 auto;
  min-height: 300px;
}

.stage {
  position: absolute;
  inset: 0;
  contain: layout paint;
  --sw: 320px;
  --sh: 320px;
  --top: .06;
  --span: .5;
  --haze: clamp(88px, 25vw, 128px);
}

.stage > .layer {
  position: absolute;
  inset: 0;
}
#dot-layer, #unobserved, #connector { pointer-events: none; }

/* every object shares one placement rule */
.mark, .haze, .dot {
  position: absolute;
  left: 0;
  top: 0;
  translate:
    calc(var(--x) * var(--sw) - 50%)
    calc((var(--top) + var(--y) * var(--span)) * var(--sh) - 50%);
}

/* ---------- usual position: the reference "いつも" ----------------------- */

.mark {
  width: 9px;
  height: 9px;
  border: 1px solid rgba(var(--ink), .5);
  border-radius: 50%;
  opacity: .24;
  transition: opacity 600ms ease;
}
/* the drift track whispers "it moved from here"; it is stage-wide and
   scaled by the signed drift, so no per-object length maths is needed */
.mark::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 4px;
  width: var(--sw);
  height: 1px;
  transform-origin: left center;
  scale: var(--drift) 1;
  background: linear-gradient(to right, rgba(var(--ink), 0), rgba(var(--ink), .8));
  opacity: .5;
  transition: scale 1500ms cubic-bezier(.2, .7, .2, 1);
}
.mark[data-related] { opacity: .72; }

/* ---------- haze ---------------------------------------------------------- */

.haze {
  z-index: 2;
  width: var(--haze);
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: translate 1500ms cubic-bezier(.2, .7, .2, 1), opacity 700ms ease;
}
.haze:disabled { cursor: default; }

.haze-body,
.haze-core {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  border-radius: 50%;
  pointer-events: none;
}

.haze-body {
  width: 100%;
  height: 100%;
  background:
    radial-gradient(circle at 42% 40%, rgba(var(--ink), .15) 0 10%, rgba(var(--ink), .07) 30%, rgba(var(--ink), 0) 58%),
    radial-gradient(circle at 63% 57%, rgba(var(--ink), .13) 0 9%, rgba(var(--ink), .055) 27%, rgba(var(--ink), 0) 52%),
    radial-gradient(circle at 51% 63%, rgba(var(--ink), .1) 0 8%, rgba(var(--ink), .04) 24%, rgba(var(--ink), 0) 47%),
    radial-gradient(circle at 50% 50%, rgba(var(--ink), .13) 0 12%, rgba(var(--ink), .07) 32%, rgba(var(--ink), .025) 52%, rgba(var(--ink), 0) 70%);
  opacity: calc(.30 + var(--w) * .82);
  scale: calc(.62 + var(--w) * .62);
  transition: scale 900ms cubic-bezier(.2, .7, .2, 1), opacity 700ms ease;
}

/* Before it is touched the centre is only a soft thickening. The definite
   point, and the loose points around it, arrive with the condensation. */
.haze-core {
  width: 7px;
  height: 7px;
  background: rgba(var(--ink), .55);
  opacity: calc(.1 + var(--w) * .3);
  filter: blur(2px);
  box-shadow:
    calc(var(--haze) * .13) calc(var(--haze) * .07) 0 -2px rgba(var(--ink), .3),
    calc(var(--haze) * -.10) calc(var(--haze) * .12) 0 -2px rgba(var(--ink), .22);
  transition: opacity 500ms ease, box-shadow 900ms cubic-bezier(.2, .7, .2, 1);
}

/* a change that is settling loses its centre and spreads out */
.haze[data-phase="dispersing"] .haze-core { opacity: calc(.04 + var(--w) * .1); filter: blur(3px); }
.haze[data-phase="dispersing"] .haze-body {
  scale: calc(1 + var(--w) * .62);
  opacity: calc(.16 + var(--w) * .4);
  animation: dispersing 13s ease-in-out infinite alternate;
}

/* a change that is beginning drifts unsettled — the only motion that
   distinguishes a live phenomenon from the rest of the Field */
.haze[data-phase="gathering"] .haze-body { animation: unsettled 11s ease-in-out infinite alternate; }

/* affordance: responds to touch before anything is explained */
.haze[data-reveals]:hover .haze-core,
.haze[data-reveals]:focus-visible .haze-core { opacity: .9; filter: blur(1px); }
.haze[data-reveals]:hover .haze-body,
.haze[data-reveals]:focus-visible .haze-body { scale: calc(.55 + var(--w) * .62); opacity: 1; }
.haze[data-reveals]:active .haze-body { scale: calc(.5 + var(--w) * .62); }
.haze:focus-visible { outline: 1px solid rgba(var(--ink), .45); outline-offset: 8px; }

/* condensation: the touched phenomenon gains a temporary centre,
   everything else goes quiet rather than disappearing */
.stage[data-focus] .haze:not([data-related]),
.stage[data-focus] .mark:not([data-related]) { opacity: .1; }
.stage[data-focus] .dot:not([data-related]) { opacity: .08; }

.haze[data-related] .haze-body {
  scale: calc(.4 + var(--w) * .42);
  opacity: 1;
  animation: none;
}
.haze[data-related][data-phase="dispersing"] .haze-body {
  scale: calc(1.15 + var(--w) * .62);
  opacity: calc(.3 + var(--w) * .4);
}
.haze[data-related][data-phase="dispersing"] .haze-core {
  opacity: .35;
  filter: blur(2px);
  box-shadow: none;
}
.haze[data-related] .haze-core {
  opacity: 1;
  filter: none;
  background: rgba(var(--ink), .72);
  box-shadow:
    calc(var(--haze) * .07) calc(var(--haze) * .04) 0 -1px rgba(var(--ink), .5),
    calc(var(--haze) * -.05) calc(var(--haze) * .06) 0 -1px rgba(var(--ink), .38);
}
.haze[data-bounded] { opacity: .35; }

/* ---------- motion engine ------------------------------------------------- */
/* While the engine drives a property per frame, the stylesheet must not also
   be easing it — otherwise every spring fights a transition. */

.stage[data-motion="spring"] .haze,
.stage[data-motion="canvas"] .haze,
.stage[data-motion="spring"] .dot,
.stage[data-motion="canvas"] .dot,
.stage[data-motion="spring"] .haze-body,
.stage[data-motion="canvas"] .haze-body {
  transition: none;
  animation: none;
}

/* Condensation is a spring value now, so the resting/related scale pair in
   the stylesheet no longer applies to the body. Everything else about being
   related — the marks, the quieting of the rest of the Field — still does. */
.stage[data-motion="spring"] .haze[data-related] .haze-body,
.stage[data-motion="canvas"] .haze[data-related] .haze-body {
  scale: unset;
  opacity: unset;
}

.haze-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.haze-canvas[hidden] { display: none; }

/* ---------- weak traces --------------------------------------------------- */

.dot {
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  background: rgba(var(--ink), .58);
  opacity: .5;
  transition: translate 1300ms cubic-bezier(.2, .7, .2, 1), opacity 700ms ease;
}
.dot[data-related] { opacity: .85; }

/* ---------- the relation itself ------------------------------------------ */
/* One hairline between the two phenomena. Geometry separates the relation
   types on its own: a convergence lands near vertical (two flows, same
   time), a counterpoint lands slanted (one settles, then the other begins). */

#connector {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  width: var(--len);
  height: 1px;
  transform-origin: left center;
  translate: calc(var(--x) * var(--sw)) calc((var(--top) + var(--y) * var(--span)) * var(--sh));
  rotate: var(--angle);
  background: linear-gradient(
    to right,
    rgba(var(--ink), 0),
    rgba(var(--ink), .3) 22% 78%,
    rgba(var(--ink), 0)
  );
}
#connector[hidden] { display: none; }

/* ---------- honest gap: time we did not observe -------------------------- */

/* kept inside the composition band so it never intrudes on the language */
#unobserved {
  position: absolute;
  top: 0;
  bottom: 42%;
  left: calc(var(--from) * var(--sw));
  width: calc((var(--to) - var(--from)) * var(--sw));
  background: linear-gradient(
    to right,
    rgba(var(--ink), 0),
    rgba(var(--ink), .028) 30% 70%,
    rgba(var(--ink), 0)
  );
}
#unobserved[hidden] { display: none; }
#unobserved span {
  position: absolute;
  left: 0;
  bottom: 6px;
  width: 100%;
  text-align: center;
  font-size: 9.5px;
  letter-spacing: .1em;
  color: rgba(var(--ink), .62);
}

/* ---------- verbalization: in the Field, not in a card ------------------- */

.words {
  position: absolute;
  left: 0;
  right: 0;
  top: 58%;
  bottom: 0;
  padding: 4px 2px 0;
  overflow-y: auto;
  text-align: center;
}
/* visibility, not display: the space stays reserved, and while closed the
   contents stay out of the tab order and the accessibility tree */
.words[data-open="false"] { visibility: hidden; transition: visibility 0s linear 420ms; }

/* language should surface out of the phenomenon rather than be printed
   over it: the span settles first, then the sentence, then the rest */
.words > * {
  opacity: 0;
  translate: 0 7px;
  transition: opacity 620ms ease, translate 620ms cubic-bezier(.2, .7, .2, 1);
}
.words[data-open="true"] > * { opacity: 1; translate: 0 0; }
.words[data-open="true"] .words-span { transition-delay: 140ms; }
.words[data-open="true"] h2 { transition-delay: 300ms; }
.words[data-open="true"] .words-body { transition-delay: 620ms; }
.words[data-open="true"] .words-privacy { transition-delay: 800ms; }
.words[data-open="true"] .responses { transition-delay: 1150ms; }
.words[data-open="true"] .words-release { transition-delay: 1250ms; }
#words-title:focus { outline: none; }
#words-title:focus-visible { outline: 1px solid rgba(var(--ink), .4); outline-offset: 6px; }
.words-span {
  margin: 0;
  font-size: 11px;
  letter-spacing: .09em;
  color: rgba(var(--ink), .62);
}
.words h2 {
  max-width: 15em;
  margin: 8px auto 0;
  font-size: clamp(17px, 4.6vw, 22px);
  font-weight: 430;
  line-height: 1.55;
  letter-spacing: -.02em;
}
.words-body {
  max-width: 24em;
  margin: 10px auto 0;
  font-size: 12.5px;
  line-height: 1.85;
  color: rgba(var(--ink), .7);
}
.words-privacy {
  margin: 6px auto 0;
  font-size: 10.5px;
  line-height: 1.7;
  color: rgba(var(--ink), .62);
}

/* deliberately quiet: calibration must not read as an engagement prompt */
.responses {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px 4px;
  margin-top: 16px;
}
.responses button,
.words-release,
#boundary-undo {
  min-height: 38px;
  padding: 6px 10px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  font-size: 11.5px;
  color: rgba(var(--ink), .66);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: rgba(var(--ink), .18);
  text-underline-offset: 4px;
}
.responses button:hover,
.words-release:hover,
#boundary-undo:hover { color: rgba(var(--ink), .85); }
.responses button:focus-visible,
.words-release:focus-visible,
#boundary-undo:focus-visible,
.scene-buttons button:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; }
.words-release { display: block; margin: 2px auto 0; }

/* ---------- standing state lines ----------------------------------------- */

.status,
.freshness {
  margin: 0 auto;
  width: min(92%, 34em);
  text-align: center;
}
.status { min-height: 2.4em; padding-top: 6px; font-size: 11.5px; line-height: 1.6; color: rgba(var(--ink), .64); }
.freshness { font-size: 9.5px; letter-spacing: .08em; color: rgba(var(--ink), .62); }

.boundary-notice {
  margin: 0 auto;
  width: min(94%, 30em);
  padding: 8px 0 0;
  text-align: center;
  font-size: 11px;
  line-height: 1.7;
  color: rgba(var(--ink), .66);
}
.boundary-notice[hidden] { display: none; }
.boundary-notice p { margin: 0; }

/* ---------- development-only controls ------------------------------------ */

.prototype-controls {
  width: min(100%, 560px);
  margin: 10px auto 0;
  font-size: 10.5px;
  color: rgba(var(--ink), .66);
}
.prototype-controls summary { min-height: 34px; cursor: pointer; text-align: center; }
.prototype-group { margin-top: 8px; text-align: center; }
.prototype-group > span { display: block; margin-bottom: 5px; letter-spacing: .1em; }
.scene-buttons { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; }
.scene-buttons button {
  min-height: 34px;
  padding: 5px 9px;
  border: 1px solid rgba(var(--ink), .14);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
}
.scene-buttons button[aria-pressed="true"] { background: rgb(var(--ink)); color: var(--paper); }
.prototype-note { margin: 9px 0 0; text-align: center; line-height: 1.5; }

/* ---------- motion: transform/opacity only ------------------------------- */

@keyframes unsettled {
  from { translate: -50% -50%; }
  to { translate: calc(-50% + 7px) calc(-50% - 5px); }
}

@keyframes dispersing {
  from { translate: -50% -50%; }
  to { translate: calc(-50% - 4px) calc(-50% + 3px); }
}

@media (max-width: 420px) {
  .words { top: 55%; }
}

/* On a short screen there is not enough surface to hold both the composition
   and the language. Compressing the band was worse than it looked — the two
   phenomena ended up overlapping, so neither could be reliably aimed at — and
   an internal scroll hid the release and calibration controls. Give the
   composition a fixed height instead and let the language extend the page:
   the Field still does not move when something is grasped, and nothing that
   matters ends up unreachable. */
@media (max-height: 740px) {
  .app-shell { padding-top: max(12px, env(safe-area-inset-top)); }
  .brand-name { font-size: 26px; }
  .anchor { margin-top: 10px; min-height: 2.7em; }
  .canvas { flex: 0 0 auto; min-height: 0; }
  .stage { position: relative; inset: auto; height: min(44dvh, 300px); }
  .words {
    position: static;
    top: auto;
    bottom: auto;
    padding-top: 12px;
    overflow: visible;
  }
  .words h2 { margin-top: 6px; }
  .responses { margin-top: 12px; }
  .status { min-height: 1.6em; }
  .prototype-controls { margin-top: 4px; }
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: 241, 241, 237;
    --paper: #11110f;
    --hairline: rgba(241, 241, 237, .1);
  }
}

/* Change is encoded as position, not as motion, so the notice signal
   survives with animation switched off. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    transition-delay: 0s !important;
  }
}
