/* Katabasis, cozy game world skin. Night camp palette: deep indigo dark, warm
   amber light. Every region card carries its own lantern hue via --hue. */

:root {
  --night: #14101d;
  --night-2: #1c1629;
  --card: #251d36;
  --card-edge: #3a2f52;
  --ink: #e8e0d5;
  --ink-dim: #a99e91;
  --amber: #ffb454;
  --amber-deep: #e8894a;
  --moss: #9ac07a;
  --ember-red: #ff7854;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: radial-gradient(ellipse at 50% 120%, #2b1d33 0%, var(--night-2) 45%, var(--night) 100%);
  color: var(--ink);
  font-family: "Segoe UI", "Trebuchet MS", system-ui, sans-serif;
  line-height: 1.5;
}

h1, h2, .brand, .gate-title { font-family: Georgia, "Times New Roman", serif; letter-spacing: 0.02em; }
h1 { font-size: 1.9rem; margin: 0.4rem 0; }
h2 { font-size: 1.25rem; color: var(--amber); font-weight: normal; }

main { max-width: 900px; margin: 0 auto; padding: 1rem 1.2rem 4rem; position: relative; }

/* stars */
.stars {
  position: fixed; inset: 0; pointer-events: none; opacity: 0.7;
  background-image:
    radial-gradient(1px 1px at 12% 18%, #fff8 50%, transparent 51%),
    radial-gradient(1px 1px at 34% 8%, #fff6 50%, transparent 51%),
    radial-gradient(2px 2px at 58% 22%, #ffe9c455 50%, transparent 51%),
    radial-gradient(1px 1px at 76% 12%, #fff7 50%, transparent 51%),
    radial-gradient(1px 1px at 89% 30%, #fff5 50%, transparent 51%),
    radial-gradient(2px 2px at 22% 34%, #ffe9c444 50%, transparent 51%),
    radial-gradient(1px 1px at 47% 15%, #fff6 50%, transparent 51%),
    radial-gradient(1px 1px at 66% 38%, #fff4 50%, transparent 51%);
  animation: twinkle 7s ease-in-out infinite alternate;
}
@keyframes twinkle { from { opacity: 0.45; } to { opacity: 0.8; } }

/* top bar */
/* ⚠️⚠️ `env(safe-area-inset-top)`, and it is not decoration. Installed on an
   iPhone the app draws under the status bar, and on a Dynamic Island phone the
   first row of the page sits behind the island. `max()` keeps the ordinary
   padding everywhere else, where the inset is zero. */
.topbar {
  display: flex; justify-content: space-between; align-items: center;
  padding: max(0.7rem, env(safe-area-inset-top)) 1.2rem 0.7rem;
  position: relative;
}
.brand { color: var(--amber); text-decoration: none; font-size: 1.3rem; flex-shrink: 0; }
/* The mark rides in the strip with the links. It keeps its own size and a gap
   after it so it reads as the name of the place rather than as another door. */
.navlinks .brand { margin-inline-end: 0.5rem; }
.navlinks { display: flex; gap: 1rem; align-items: center; }
.navlinks a { color: var(--ink-dim); text-decoration: none; }
.navlinks a:hover { color: var(--amber); }
.inline { display: inline; }

/* ⚠️⚠️ Nine doors and a phone. The bar was a plain flex row that could neither
   wrap nor scroll, so on a narrow screen it simply ran off the side: the last
   links were unreachable AND the page itself grew wider than the viewport, so
   every page under it scrolled sideways. He reported it as the nav "fucking up
   the page", and the page part is the worse half.

   It is a scroll strip now rather than a wrapping row. Wrapping would have been
   fewer lines and is wrong here: a bar that silently becomes two or three rows
   pushes the content of every page down by a different amount depending on
   which links you happen to have, and on this app that depends on whether you
   are the owner.

   ⚠️ `min-width: 0` is what makes it work at all. A flex item's floor is its
   content, so without this the strip refuses to be narrower than nine links and
   overflows exactly as before, with `overflow-x: auto` on an element that is
   never actually too small to do anything. */
.navlinks {
  overflow-x: auto;
  overflow-y: hidden;
  flex-wrap: nowrap;
  min-width: 0;
  /* Room for the focus ring of the first and last link, which a scroll port
     clips flush against its own edge. */
  padding: 0.25rem 0.15rem;
  /* Firefox and the standard property. Everything else gets the rule below. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* iOS keeps momentum inside the strip instead of handing the gesture to the
     page, which is what makes a two centimetre flick usable at all. */
  -webkit-overflow-scrolling: touch;
  /* An overscroll here must not become a back-navigation gesture or a bounce of
     the whole page. */
  overscroll-behavior-x: contain;
}
.navlinks::-webkit-scrollbar { display: none; }
.navlinks > * { flex-shrink: 0; }

/* xp bar */
.xpbar-wrap { display: flex; align-items: center; gap: 0.7rem; padding: 0 1.2rem 0.4rem; max-width: 900px; margin: 0 auto; }
.xpbar-label { color: var(--amber); font-size: 0.85rem; white-space: nowrap; }
.xpbar { flex: 1; height: 8px; background: #ffffff14; border-radius: 6px; overflow: hidden; }
.xpbar-fill {
  height: 100%; border-radius: 6px;
  background: linear-gradient(90deg, var(--amber-deep), var(--amber));
  box-shadow: 0 0 8px var(--amber);
  transition: width 0.6s ease;
}
.xpbar-num { color: var(--ink-dim); font-size: 0.75rem; white-space: nowrap; }

/* ⚠️ The campfire hero used to live here: a flex row with an animated three
   layer CSS flame, a title and a day count. All of it was deleted at his word
   rather than hidden behind a flag. Dead rules that nothing renders are how a
   stylesheet becomes a place where you cannot tell what is load bearing, and
   the animation was also a `filter: blur` and three infinite keyframe loops
   painting forever on a page nobody was looking at the top of. */

/* energy picker */
.ember-picker { margin-top: 1.4rem; }
/* First thing on the page now, so it does not need to be pushed down. */
.ember-picker.top { margin-top: 0.2rem; }
.picker-title { font-size: 1.5rem; color: var(--amber); font-weight: normal; }
.energy-row { display: flex; gap: 0.8rem; flex-wrap: wrap; }
.energy-chip {
  flex: 1; min-width: 150px; text-align: center; text-decoration: none;
  padding: 0.7rem 0.6rem; border-radius: 14px; color: var(--ink);
  background: var(--card); border: 1px solid var(--card-edge);
  display: flex; flex-direction: column; gap: 0.1rem;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.energy-chip small { color: var(--ink-dim); }
.energy-chip:hover { transform: translateY(-2px); }
.energy-chip.spark.on, .energy-chip.spark:hover { box-shadow: 0 0 14px #ffd45455; border-color: #ffd454aa; }
.energy-chip.steady.on, .energy-chip.steady:hover { box-shadow: 0 0 14px #ff985455; border-color: #ff9854aa; }
.energy-chip.deep.on, .energy-chip.deep:hover { box-shadow: 0 0 14px #b678ff44; border-color: #b678ffaa; }

.ember-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; margin: 1rem 0 0; }
.ember a {
  display: flex; flex-direction: column; gap: 0.15rem; text-decoration: none;
  background: var(--card); border: 1px solid var(--card-edge); border-inline-start: 3px solid var(--amber);
  padding: 0.7rem 0.9rem; border-radius: 12px; color: var(--ink);
}
.ember a:hover { border-color: var(--amber); }
.ember-quest { font-weight: 600; }
.ember-step { color: var(--amber); font-size: 0.9rem; }

/* region map */
.region-map { margin-top: 2rem; }
.regions { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 1rem; }
.region {
  --hue: 38;
  position: relative;
  display: flex; flex-direction: column; gap: 0.25rem;
  background: var(--card);
  border: 1px solid hsl(var(--hue), 45%, 38%, 0.65);
  border-radius: 16px; padding: 1rem 1.1rem;
  color: var(--ink); text-decoration: none;
  box-shadow: 0 0 18px hsla(var(--hue), 80%, 60%, 0.10), inset 0 0 30px hsla(var(--hue), 80%, 60%, 0.05);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  animation: floaty 6s ease-in-out infinite alternate;
}
.region:nth-child(2n) { animation-delay: 1.6s; }
.region:nth-child(3n) { animation-delay: 3.1s; }
@keyframes floaty { from { transform: translateY(0); } to { transform: translateY(-4px); } }
.region:hover { box-shadow: 0 0 26px hsla(var(--hue), 90%, 62%, 0.28); transform: translateY(-3px); }
.region-sigil { font-size: 1.6rem; }
.region-name { font-weight: 600; font-size: 1.05rem; }
.region-state { color: var(--ink-dim); font-size: 0.82rem; }
.region-why { color: var(--ink-dim); font-size: 0.85rem; font-style: italic; }
.region.surfaced { opacity: 0.55; filter: saturate(0.6); }
.region.resting { opacity: 0.7; }
.ghost-slot {
  border: 1px dashed var(--card-edge); background: transparent; text-align: center;
  justify-content: center; color: var(--ink-dim); animation: none;
}

/* panels and forms */
.new-panel { margin-top: 1.2rem; }
.new-panel summary { cursor: pointer; color: var(--amber); }
.stack { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.8rem; }
.row { display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap; margin-top: 0.6rem; }
input, select, button, textarea {
  font: inherit; color: var(--ink);
  background: var(--night-2); border: 1px solid var(--card-edge);
  border-radius: 10px; padding: 0.55rem 0.8rem;
}
input::placeholder { color: #7d7466; }
input.tiny { width: 4rem; text-align: center; }
label { color: var(--ink-dim); font-size: 0.9rem; display: inline-flex; gap: 0.5rem; align-items: center; }
label.check { cursor: pointer; }
button { cursor: pointer; background: var(--card); transition: box-shadow 0.15s ease; }
button:hover { border-color: var(--amber); }
button.glow {
  background: linear-gradient(160deg, var(--amber-deep), var(--amber));
  color: #241505; border: none; font-weight: 600;
  box-shadow: 0 0 16px #ffb45444;
}
button.glow:hover { box-shadow: 0 0 24px #ffb45488; }
button.ghost { background: transparent; color: var(--ink-dim); }
button.ghost:hover { color: var(--ink); }
.surface-btn {
  background: linear-gradient(160deg, #7a9a5a, var(--moss)); border: none;
  color: #1c2410; font-weight: 600; box-shadow: 0 0 16px #9ac07a44;
}

/* quests */
.crumb { color: var(--ink-dim); text-decoration: none; font-size: 0.9rem; }
.crumb:hover { color: var(--amber); }
.why { color: var(--ink-dim); font-style: italic; margin: 0.2rem 0; }
.quest-list { margin-top: 1.4rem; display: flex; flex-direction: column; gap: 0.7rem; }
.quest-card {
  display: flex; flex-direction: column; gap: 0.3rem;
  background: var(--card); border: 1px solid var(--card-edge);
  border-radius: 14px; padding: 0.85rem 1rem; color: var(--ink); text-decoration: none;
}
.quest-card:hover { border-color: var(--amber); }
.quest-card.done { opacity: 0.5; }
.quest-card.resting { opacity: 0.72; }
.quest-card.tender { border-inline-start: 3px solid #d89ae0; }
.q-title { font-weight: 600; }
.q-meta { display: flex; gap: 0.7rem; flex-wrap: wrap; font-size: 0.82rem; color: var(--ink-dim); align-items: center; }
.q-next { color: var(--amber); font-size: 0.9rem; }
.energy-dot { padding: 0.1rem 0.5rem; border-radius: 8px; background: #ffffff10; }
.energy-dot.spark { color: #ffd454; }
.energy-dot.steady { color: #ff9854; }
.energy-dot.deep { color: #c99aff; }
.soft-tag { color: #d89ae0; }
.waypoint { color: var(--ink-dim); }
.done-fold { margin-top: 0.6rem; color: var(--ink-dim); }
.done-fold summary { cursor: pointer; }

/* the one next step */
.next-step-card {
  margin-top: 1.2rem; padding: 1.2rem 1.3rem; border-radius: 16px;
  background: linear-gradient(160deg, #2d2340, var(--card));
  border: 1px solid var(--amber-deep);
  box-shadow: 0 0 22px #e8894a33;
  display: flex; flex-direction: column; gap: 0.5rem;
}
.next-step-card.empty { border-style: dashed; box-shadow: none; }
.nsc-label { font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--amber); }
.nsc-text { font-size: 1.2rem; font-family: Georgia, serif; }
.addstep { margin-top: 0.9rem; align-items: flex-start; }
/* ⚠️ Both, because these rows carry a growing textarea now and `min-width: 0`
   is what lets one shrink to the width of the row rather than to the width its
   own content would like. */
.addstep input, .addstep textarea { flex: 1; min-width: 0; }
.step-trail { list-style: none; padding: 0.4rem 0 0 0.4rem; margin: 0; }
.step-trail li { padding: 0.2rem 0; color: var(--ink-dim); }
.step-trail li.done::before { content: "\1F702  "; color: var(--amber); }
.step-trail li.done { color: var(--ink); }
.step-trail li.open::before { content: "\25CB  "; }
.step-trail li.skipped { text-decoration: line-through; opacity: 0.6; }
.quest-actions { margin-top: 1.4rem; }

/* ⚠️ Something has to say a request is in flight now that pressing a button no
   longer visibly reloads anything. One thin amber line across the very top,
   and nothing else: no spinner over the content, no dimming, no disabling. The
   answer usually arrives in under a tenth of a second on a local network, and
   a treatment heavy enough to be noticed at that speed is a flicker. */
body.kata-working::before {
  content: ""; position: fixed; inset-inline: 0; top: 0; height: 2px; z-index: 90;
  background: linear-gradient(90deg, transparent, var(--amber), transparent);
  animation: kata-working 1.1s linear infinite;
  pointer-events: none;
}
@keyframes kata-working {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  body.kata-working::before { animation: none; opacity: 0.6; }
}

.mist { color: var(--ink-dim); font-style: italic; }
.foot { text-align: center; color: #5e5570; padding: 2rem 0 1rem; font-style: italic; font-family: Georgia, serif; }

/* ⚠️⚠️ These three rules used to live in `surfaces.css`, and `surfaces.css` is
   loaded by `base.html` and by nothing else. So the same footer markup, on the
   gate and the invitation and the Terms and the Privacy Policy and the licence
   notice, rendered as three full sized italic serif links stacked with no
   separator, while the identical markup one page later, once you were logged
   in, was one quiet faint row. He saw both and said so.

   They are here now because `style.css` is the one stylesheet every page in the
   app loads, and a rule for shared markup belongs wherever the markup is
   shared. Nothing else in `surfaces.css` is shared with the pages outside the
   gate, which is why only these moved. */
.foot .credits { margin-top: 0.35rem; }
.foot .credit { display: inline; font-size: 0.72rem; opacity: 0.55; font-style: normal; }
.foot .credit + .credit::before { content: " \00b7 "; }
.foot .credit a { color: inherit; }
/* ⚠️ The second footer line: who this belongs to and how to reach them, kept
   apart from the three documents above it because it is a different kind of
   fact. Quieter and smaller than that row, which is already quiet: the whole
   footer is meant to be findable rather than read. */
.foot .credits.fine { margin-top: 0.2rem; }
.foot .credits.fine .credit { font-size: 0.66rem; opacity: 0.4; }

/* ⚠️⚠️ The contact form's honeypot. NOT `display: none` and not `hidden`: the
   robots worth catching skip a field the browser would never submit, and a
   field that is off screen is one they cannot tell from a real one. It is out
   of the tab order and `aria-hidden` in the markup, so nobody using a keyboard
   or a screen reader ever meets it. */
.trap {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden; opacity: 0;
}

/* sentinel */
.sentinel .ledger { width: 100%; border-collapse: collapse; margin-top: 0.8rem; font-size: 0.85rem; }
.ledger th { text-align: start; color: var(--amber); font-weight: normal; padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--card-edge); }
.ledger td { padding: 0.3rem 0.5rem; border-bottom: 1px solid #ffffff0d; color: var(--ink-dim); }
.ledger tr.face_ok td, .ledger tr.recovery_ok td { color: var(--moss); }
.ledger tr.face_fail td, .ledger tr.recovery_fail td, .ledger tr.denied td { color: var(--ember-red); }
.verdicts { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.8rem; }
.verdict { padding: 0.5rem 0.8rem; border-radius: 10px; background: var(--card); border-inline-start: 3px solid var(--moss); }
.verdict.ok { border-color: var(--moss); }
.verdict.unverifiable { border-color: var(--amber); }
.verdict.altered, .verdict.chain_broken, .verdict.truncated,
.verdict.anchor_missing, .verdict.anchor_forged { border-color: var(--ember-red); }
.pager { justify-content: center; color: var(--ink-dim); font-size: 0.85rem; }
.pager a { color: var(--amber); text-decoration: none; }

/* gate */
.gatepage main.gate-main {
  max-width: 460px; text-align: center; padding-top: 8vh;
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
}
.gate-title { font-size: 2.4rem; color: var(--amber); margin: 0; text-shadow: 0 0 24px #ffb45444; }
.gate-sub { color: var(--ink-dim); margin: 0 0 1rem; }
.cam-frame { position: relative; width: 320px; height: 240px; }
.cam-frame video {
  width: 100%; height: 100%; object-fit: cover; border-radius: 18px;
  background: #000; filter: sepia(0.15) contrast(1.02);
}
.cam-ring {
  position: absolute; inset: -6px; border-radius: 24px; pointer-events: none;
  border: 2px solid var(--card-edge);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.cam-ring.snap { border-color: var(--amber); box-shadow: 0 0 24px #ffb45466; }
.gate-actions { display: flex; flex-direction: column; gap: 0.7rem; align-items: center; width: 100%; }
/* The one word on the page. It is the only button, so it gets to be a button
   and not a link with a border round it. */
.gate-descend {
  font-size: 1.15rem; padding: 0.7rem 2.4rem; letter-spacing: 0.04em;
  font-family: Georgia, "Times New Roman", serif;
}

/* --- the eerie door, and what is behind it ---------------------------------
   ⚠️⚠️ Written here and not in `surfaces.css`, where every other overlay in
   this app lives. The gate loads `style.css` and `controls.css` and nothing
   else, so a `.scry` on this page would be an unstyled block of text sitting in
   the middle of the login. Check what a page actually loads before reaching for
   a class that works elsewhere.

   ⚠️ The palette is the ERROR pages' palette, not the app's: cold greys on
   near black, Georgia, no amber anywhere. That is the whole point of it. Every
   other surface in Katabasis is warm; this one is the one place that is not
   trying to be, because what is behind it is not for whoever is reading. */
.eerie-door {
  background: none; border: none; box-shadow: none; padding: 0.9rem;
  cursor: pointer; opacity: 0.35; transition: opacity 0.6s ease;
  line-height: 0;
}
.eerie-door:hover, .eerie-door:focus-visible { opacity: 0.8; }
.eerie-eye {
  display: block; width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid #3a3a44;
  box-shadow: inset 0 0 12px #00000099, 0 0 18px #ffffff08;
  background: radial-gradient(circle at 50% 50%, #1a1920 0%, #0b0b0f 60%);
  animation: eerie-breathe 7s ease-in-out infinite;
}
.eerie-eye.big { width: 64px; height: 64px; box-shadow: inset 0 0 26px #00000099, 0 0 34px #ffffff08; }
@keyframes eerie-breathe {
  0%, 100% { opacity: 0.5; transform: scale(1); }
  50%      { opacity: 0.9; transform: scale(1.06); }
}

.eerie-veil {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: 1rem;
  background: radial-gradient(ellipse at 50% 40%, #14131aee 0%, #0a090df7 60%, #000000fa 100%);
  backdrop-filter: blur(4px);
  /* Nothing pops in this app. The door fades. */
  animation: eerie-open 0.7s ease both;
}
/* ⚠️ Load bearing next to the `hidden` attribute. `display: flex` above beats
   the browser's own rule for `[hidden]`, so without this the overlay is on
   screen from the moment the page loads, camera frame and all. */
.eerie-veil[hidden] { display: none; }
@keyframes eerie-open { from { opacity: 0; } to { opacity: 1; } }
.eerie-inner {
  display: flex; flex-direction: column; align-items: center; gap: 0.9rem;
  max-width: 420px; width: 100%; text-align: center;
  color: #b9b4c2;
}
.eerie-title {
  font-family: Georgia, "Times New Roman", serif; font-size: 1.5rem;
  color: #b9b4c2; margin: 0; letter-spacing: 0.04em; font-weight: normal;
}
.eerie-sub { color: #55525e; margin: 0; font-style: italic; min-height: 1.4rem; }
/* ⚠️ The camera keeps its own grey ring inside the door rather than the amber
   one the rest of the app draws, and it is deliberately drained: this is the
   coldest surface in the application and a warm sepia preview in the middle of
   it would look like a mistake. */
.eerie-veil .cam-frame video { filter: grayscale(0.55) contrast(1.04); }
.eerie-veil .cam-ring { border-color: #3a3a44; }
.eerie-veil .cam-ring.snap { border-color: #b9b4c2; box-shadow: 0 0 24px #ffffff22; }
.eerie-btn {
  background: none; border: 1px solid #3a3a44; color: #b9b4c2;
  border-radius: 12px; padding: 0.6rem 2rem; cursor: pointer;
  font-family: Georgia, "Times New Roman", serif; font-size: 1.05rem;
  letter-spacing: 0.06em; box-shadow: none;
  transition: border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
}
.eerie-btn:hover:not(:disabled) { border-color: #6d6878; color: #e4e0e8; box-shadow: 0 0 26px #ffffff10; }
.eerie-btn:disabled { opacity: 0.45; cursor: default; }
.eerie-close {
  background: none; border: none; box-shadow: none; cursor: pointer;
  color: #55525e; font-size: 0.85rem; text-decoration: none;
  border-bottom: 1px solid #2e2c36; border-radius: 0; padding: 0 0 0.15rem;
}
.eerie-close:hover { color: #8e8798; border-color: #6d6878; }
@media (prefers-reduced-motion: reduce) {
  .eerie-eye { animation: none; }
  .eerie-veil { animation: none; }
}
.recovery-fold { color: var(--ink-dim); }
.recovery-fold summary { cursor: pointer; }
.recovery-reveal[hidden] { display: none; }
.recovery-reveal {
  background: var(--card); border: 1px solid var(--amber-deep); border-radius: 16px;
  padding: 1.2rem 1.4rem; box-shadow: 0 0 28px #e8894a33;
  display: flex; flex-direction: column; gap: 0.7rem; align-items: center;
}
.recovery-reveal code {
  font-size: 1.25rem; letter-spacing: 0.08em; background: var(--night);
  padding: 0.6rem 1rem; border-radius: 10px; color: var(--amber);
}

/* toybox and campfire */
.region.toy { --hue: 300; }
.campfire-toy { text-align: center; }
.campfire-toy canvas {
  width: 100%; max-width: 900px; border-radius: 18px;
  background: radial-gradient(ellipse at 50% 100%, #241629 0%, #120e1a 70%);
  border: 1px solid var(--card-edge);
}
.burn-row { display: flex; gap: 0.7rem; justify-content: center; margin-top: 0.9rem; }
.burn-row input { flex: 1; max-width: 460px; }
.ash { min-height: 1.4rem; color: var(--ink-dim); font-style: italic; font-family: Georgia, serif; }

@media (max-width: 640px) {
  /* ⚠️ The strip is the whole bar now: the mark scrolls with the links rather
     than sitting on a row above them, where the Dynamic Island covers it in the
     installed app. Nothing is stacked any more, so nothing needs centring. */
  .topbar { padding-inline: 0.6rem; }
  .navlinks { width: 100%; justify-content: flex-start; }
  .navlinks .brand { font-size: 1.15rem; }
}

/* the page a dead invitation leads to.
   ⚠️ Cold on purpose. Every other surface in Katabasis is warm amber on
   indigo; this one drains to grey and the stars behind it stop twinkling. It
   is the only page in the app that is not trying to be kind, because it is the
   only page that might be looking at somebody who was never invited. */
.voidpage { background: radial-gradient(ellipse at 50% 40%, #14131a 0%, #0a090d 70%, #000 100%); }
.voidpage .stars { opacity: 0.15; animation: none; }
.voidpage main.void-main {
  max-width: 420px; text-align: center; padding-top: 22vh;
  display: flex; flex-direction: column; align-items: center; gap: 1rem;
}
.void-eye {
  width: 74px; height: 74px; border-radius: 50%;
  border: 1px solid #3a3a44;
  box-shadow: inset 0 0 30px #00000099, 0 0 40px #ffffff08;
  background: radial-gradient(circle at 50% 50%, #1a1920 0%, #0b0b0f 60%);
  animation: void-breathe 7s ease-in-out infinite;
}
@keyframes void-breathe {
  0%, 100% { opacity: 0.5; transform: scale(1); }
  50%      { opacity: 0.85; transform: scale(1.05); }
}
.void-title {
  font-family: Georgia, "Times New Roman", serif; font-size: 1.7rem;
  color: #b9b4c2; margin: 0; letter-spacing: 0.04em;
}
.void-sub { color: #55525e; margin: 0; font-style: italic; }

/* coming in on an invitation */
.joinpage main.join-main {
  max-width: 520px; padding-top: 6vh;
  display: flex; flex-direction: column; gap: 1rem;
}
.join-title { font-size: 2.2rem; color: var(--amber); margin: 0; text-align: center;
  text-shadow: 0 0 24px #ffb45444; font-family: Georgia, "Times New Roman", serif; }
.join-sub { color: var(--ink-dim); margin: 0; text-align: center; }
/* ⚠️ The invitation page is three lines and a button now, so this line has to
   carry the weight the two deleted cards used to. Serif, spaced out, amber:
   the same voice the wordmark speaks in, one step quieter. */
.join-creed {
  margin: 0 0 0.8rem; text-align: center;
  font-family: Georgia, "Times New Roman", serif;
  color: var(--amber); opacity: 0.75;
  letter-spacing: 0.08em; font-size: 1.05rem;
}
.join-card {
  background: var(--card); border: 1px solid var(--card-edge); border-radius: 16px;
  padding: 1rem 1.2rem;
}
.join-card h2 { margin-top: 0; }
.join-card p { margin: 0.5rem 0; }
/* ⚠️ Indented with padding-inline-start and not a left margin. The
   shorthand version of this hard coded the LEFT side, so in Arabic the
   bullets sat outside the card on the wrong edge with the text pushed
   away from the side it starts on. It survived the round 10 sweep because
   the sweep looks for the physical property by name and a shorthand does
   not contain it. */
.join-card ul { margin: 0.4rem 0; padding: 0; padding-inline-start: 1.2rem; }
.join-card li { margin: 0.25rem 0; }
/* --- the mark and the definition, on every page outside the gate ----------- */

/* ⚠️ These rules are duplicated, in a shortened form, inside the inline style
   of `_eerie.html`. That file loads no stylesheet on purpose, and the same
   wordmark has to render there. Two copies is the accepted cost; the alternative
   is an error page that shows a naked list of words when the static pipeline is
   what broke. */
.wordmark {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.wordmark-flame { width: 1.5em; height: 1.5em; fill: var(--amber); flex-shrink: 0; }
.wordmark-name {
  font-family: Georgia, "Times New Roman", serif; font-size: 1.6rem;
  color: var(--amber); letter-spacing: 0.02em;
}
/* A dictionary entry, set like one: the headword large, the pronunciation and
   the part of speech quiet beside it, the sense on its own line. */
.worddef {
  border-inline-start: 2px solid var(--card-edge);
  padding-inline-start: 0.9rem; margin: 0 auto 1.4rem; max-width: 32rem;
  text-align: start;
}
.wd-top { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.wd-word { font-family: Georgia, serif; font-size: 1.15rem; color: var(--ink); }
.wd-say { color: var(--ink-dim); font-size: 0.9rem; unicode-bidi: isolate; }
.wd-pos { color: var(--ink-dim); font-size: 0.85rem; font-style: italic; }
.wd-sense { margin: 0.25rem 0 0; color: var(--ink-dim); font-size: 0.9rem; }
.wd-origin { margin: 0.3rem 0 0; color: #6f6678; font-size: 0.8rem; font-style: italic; }

/* The terms and the privacy note. Wider and looser than an invitation card,
   because these are read rather than filled in, and a page of prose set at the
   width of a form is a page nobody finishes. */
/* ⚠️ `.joinpage main` in the selector, because the rule it has to beat is
   `.joinpage main.join-main` and a plain class selector loses to it
   silently: the page renders at the width of an invitation form and
   nothing anywhere says why. */
.joinpage main.join-main.legal { max-width: 680px; }
.legal .join-card { padding: 1.1rem 1.4rem; }
.legal .join-card h2 { color: var(--amber); font-size: 1.05rem; }
/* The clause number, quiet: it is there to be cited, not to be read. */
.legal-n { color: var(--ink-dim); font-family: inherit; margin-inline-end: 0.35rem; }
.legal .join-card p { color: var(--ink); }
.legal .join-card p, .legal .join-card li { line-height: 1.65; }

.join-consent { display: flex; gap: 0.6rem; align-items: flex-start; cursor: pointer; }
.join-consent input { margin-top: 0.35rem; }
.join-field { display: flex; flex-direction: column; gap: 0.3rem; }
.join-actions { display: flex; flex-direction: column; gap: 0.6rem; align-items: center; }

/* the gatehouse: his invitations and the people who used them */
.keep-row {
  display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center;
  padding: 0.6rem 0; border-bottom: 1px solid var(--card-edge);
}
.keep-row:last-child { border-bottom: none; }
.keep-name { font-weight: 600; min-width: 8rem; }
.keep-when { color: var(--ink-dim); font-size: 0.9rem; }
.keep-state {
  font-size: 0.8rem; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 0.1rem 0.5rem; border-radius: 999px; border: 1px solid var(--card-edge);
  color: var(--ink-dim);
}
.keep-state.open { color: var(--moss); border-color: #9ac07a55; }
.keep-state.revoked, .keep-state.expired, .keep-state.spent { color: var(--ember-red); border-color: #ff785455; }
.keep-uses { list-style: none; margin: 0.4rem 0 0; padding-inline: 0.8rem 0;
  border-inline-start: 2px solid var(--card-edge); width: 100%; }
.keep-uses li { color: var(--ink-dim); font-size: 0.88rem; }
.keep-link {
  background: var(--night); border: 1px solid var(--amber-deep); border-radius: 12px;
  padding: 0.8rem 1rem; margin: 0.6rem 0; word-break: break-all;
  box-shadow: 0 0 24px #e8894a22;
}
.keep-link code { color: var(--amber); }
.keep-link p { margin: 0 0 0.6rem; }
.keep-link-row { display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap; }
/* ⚠️ `min-width: 0` beside the `flex: 1`. The link is one unbroken token, so
   its intrinsic minimum width is the whole link; without this the code refuses
   to shrink and pushes the button off the side of the card, which is the one
   place the button must never be. */
.keep-link-row code { flex: 1; min-width: 0; }
.copy-btn { flex-shrink: 0; white-space: nowrap; }
/* ⚠️ Moss, not amber, and it fades back on its own. A copy button that stays
   green forever stops answering the question it was asked, which is "did that
   one work", not "has this ever worked". */
.copy-btn.done { color: var(--moss); border-color: var(--moss); box-shadow: 0 0 14px #9ac07a44; }

/* the six digits */
.code-input {
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.6rem; letter-spacing: 0.35em; text-align: center;
  width: 8.5rem; padding: 0.5rem 0.4rem;
}

/* --- right to left ----------------------------------------------------------
   Almost nothing is here, and that is the point: the stylesheets use logical
   properties (inline-start, inline-end, text-align: start) so one rule serves
   both directions. What is left below is the handful of things that are drawn
   rather than laid out, plus one thing that deliberately does not mirror.

   ⚠️⚠️ Do NOT add a blanket transform to flip containers. The moon disc already
   carries `scaleX(-1)` to show the southern hemisphere's view, and a second
   flip would cancel it and quietly draw the wrong moon for half the world. */

/* The fold marker is a CSS triangle: its borders are geometry, so it has to be
   redrawn pointing the other way rather than laid out the other way. */
[dir="rtl"] summary::before {
  border-inline-start: 0;
  border-right: 5px solid currentColor;
}
[dir="rtl"] details[open] > summary::before { transform: rotate(-90deg); }

/* The chevron on a select is a background image, and a background position is
   physical no matter what the padding does. */
[dir="rtl"] select { background-position: left 0.75rem center; }

/* ⚠️⚠️ The day bar does NOT mirror, in any language. It is a clock: sunrise on
   one end, sunset on the other, and the playhead between them is placed by
   JavaScript with `left: X%`. Mirroring it would mean the script computing a
   different property depending on the reading direction, which is a whole class
   of bug for the sake of a bar whose labels are numerals. It is pinned to LTR
   here so it looks the same everywhere rather than half mirrored. */
.alm-arc { direction: ltr; }

/* --- text that somebody else wrote ------------------------------------------
   ⚠️⚠️ `unicode-bidi: plaintext` is the CSS spelling of `dir="auto"`: each
   paragraph picks its own direction from its first strong character. It is here
   rather than as an attribute on forty elements because half of this content
   arrives from JavaScript, and an attribute that has to be remembered on every
   injection point is an attribute that will be forgotten on one of them.

   Why it matters: an Arabic interface will hold English quest titles (his own
   are English today) and an English one will hold Arabic. Without this, a
   sentence ending in a full stop renders with the full stop at the wrong end,
   which is the visible symptom of the browser having guessed the direction from
   the page instead of from the words. */
.region-name, .region-why, .q-title, .q-next, .move-title, .move-step,
.nsc-text, .j-text, .capture-text, .scry-text, .contact-name, .keep-name,
.ds-row td, .cl-text, .step-text, .await-list li,
input[type="text"], input:not([type]), textarea {
  unicode-bidi: plaintext;
}

/* ⚠️⚠️ The same list, plus the headings, wrapping their own newlines.
   Titles, whys and steps stopped being single-line `<input>`s and became boxes
   that grow, so the text in them can now contain real newlines. HTML collapses
   those to a single space, so a quest written as three lines rendered as one
   run-on sentence everywhere it was shown, and the paragraph breaks he had
   deliberately typed simply vanished between saving and reading.

   `pre-wrap` and not `pre`: his newlines are kept, and the line still wraps at
   the edge of the card instead of scrolling sideways off it. */
.quest-head h1, .region-head h1,
.why, .region-why, .q-title, .q-next, .move-title, .move-step,
.nsc-text, .j-text, .capture-text, .step-text {
  white-space: pre-wrap;
}

/* ⚠️ Not plaintext: `0 / 60` has no strong character at all, so it inherits the
   paragraph and comes out reading `60 / 0` in Arabic, which is a different and
   entirely wrong number. A ratio is an LTR construct in every language that
   writes Western digits, which is what this app writes. */
/* ⚠️ Values that are a number plus a Latin unit or separator, and nothing
   with a direction of its own: `8 km/h` comes out `km/h 8` and `0 / 60` comes
   out `60 / 0`, which is not a formatting nicety, it is a different number. */
.xpbar-num, .code-input, .arc-ends, .arc-top, .keep-uses li, .ft-value,
.wx-when { direction: ltr; }
/* ⚠️⚠️ And the exception, which he found: a value that contains a WORD is not
   a number, and forcing it left to right scrambles it. "219 من 365" came out
   reading `of 365 219`, because in a left to right paragraph the Arabic run and
   the two numbers around it resolve to three separate runs and get reordered
   against each other. `plaintext` takes the base direction from the first
   strong character in the value itself, which is the only thing that can be
   right for a value whose language changes with the interface.

   ⚠️ It cannot be the rule for all of them, which is the trap the block above
   exists for: `0 / 60` and `8 km/h` have no strong character at all, so
   plaintext leaves them inheriting the paragraph and puts the numbers back to
   front. Number plus unit stays forced; number plus word gets this. */
.ft-value.ft-words { unicode-bidi: plaintext; }
/* ⚠️ And put the alignment back. `direction: ltr` also redefines what
   `text-align: start` means, so a value that was tucked under its own
   right-aligned label jumped to the left of its cell while the label stayed
   put. The ORDER of the characters had to change; where the box sits did not. */
[dir="rtl"] .xpbar-num, [dir="rtl"] .keep-uses li, [dir="rtl"] .ft-value,
[dir="rtl"] .wx-when { text-align: right; }

/* A column of cards with room to breathe. The Today page had this all along in
   `.alm-strip`; Settings grew to six cards without it and read as one wall. */
.stack { display: flex; flex-direction: column; gap: 0.9rem; }
.stack > .prefs { margin-top: 0; }

/* the language chooser on the pages that come before an account */
/* ⚠️ It fades up from 0.55 rather than starting there, and now that it carries
   a visible label it starts a little brighter: a label you cannot read is worse
   than no label, and 0.55 on a dim grey was close to that. */
.lang-pick { margin-top: 1.4rem; text-align: center; opacity: 0.7; }
.lang-pick:hover, .lang-pick:focus-within { opacity: 1; }
.lang-pick select { font-size: 0.85rem; padding: 0.25rem 1.9rem 0.25rem 0.5rem; }
.lang-pick-label {
  display: block; margin-bottom: 0.35rem;
  font-size: 0.8rem; color: var(--ink-dim);
}
/* Reachable by a screen reader, invisible to everyone else. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* feedback */
.fb-text { font-size: 1.02rem; line-height: 1.55; margin: 0 0 0.6rem; unicode-bidi: plaintext; }
.fb-shot { margin: 0.6rem 0; }
.fb-shot img {
  max-width: 100%; border-radius: 12px; border: 1px solid var(--card-edge);
  display: block; margin-bottom: 0.4rem;
}
.fb-triage { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.8rem; }
.fb-triage textarea { width: 100%; }
.fb-triage .crumb input { margin-inline-end: 0.3rem; }
/* ⚠️ A count that only exists when it is not zero. A badge showing 0 is a badge
   that has stopped meaning anything, and the eye stops going to it. */
.badge {
  display: inline-block; min-width: 1.15rem; margin-inline-start: 0.3rem;
  padding: 0 0.3rem; border-radius: 999px; text-align: center;
  background: var(--amber); color: var(--night);
  font-size: 0.72rem; font-weight: 700; line-height: 1.15rem;
}

/* the second step's QR
   ⚠️ A white plate under it, and padding. A QR needs its quiet zone and it
   needs contrast; this app is very dark and a code drawn straight onto the
   night is a code half the phones in the room refuse to read. */
.totp-qr { margin: 0.6rem 0; }
.totp-qr svg { display: block; background: #fff; border-radius: 8px;
  width: 100%; max-width: 230px; height: auto; }
