/* The Smarty pages: the wiki, the journal, the profile, the legal pages.
 *
 * Not the reading flow — that is React and Framer Motion and lives in
 * front_dev/react. This file dresses the pages that are text, and it is
 * deliberately small: the wiki is the SEO surface and should be the fastest
 * thing on the site.
 *
 * Colours mirror theme.palette in app/zygote.json. They are duplicated here
 * because a stylesheet is static and the Zygote is not — when they disagree,
 * the Zygote is right and this file is stale. Anything that must follow the
 * edition exactly reads it from $THEME in the template instead.
 */

:root {
  --t-ink: #0d0b09;
  --t-table: #1a1410;
  --t-candle: #e8b661;
  --t-brass: #a8823c;
  --t-parchment: #e6dcc6;
  --t-muted: #8a7f6d;
  --t-accent: #6b2f2f;

  --t-measure: 68ch;
  --t-gap: 1.5rem;

  /* The site is dark everywhere, and this is what tells the browser so: the
     controls it draws itself — checkboxes, the scrollbar, the file picker,
     the autofill dropdown — follow it. Without it a checkbox is a white square
     on a candlelit table. */
  color-scheme: dark;
}

* { box-sizing: border-box; }

body.t-page {
  margin: 0;
  background: var(--t-table);
  color: var(--t-parchment);
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

.t-wrap {
  width: 100%;
  max-width: 72rem;
  margin: 0 auto;
  padding: 0 1.25rem;
}

/* ---- header ---- */

.t-header {
  border-bottom: 1px solid rgba(168, 130, 60, 0.25);
  padding: 1rem 0;
}
.t-header .t-wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--t-gap);
  flex-wrap: wrap;
}
.t-brand {
  font-family: var(--t-display);
  font-size: 1.375rem;
  letter-spacing: 0.02em;
  color: var(--t-parchment);
  text-decoration: none;
}
.t-nav {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  font-family: var(--t-ui);
  font-size: 0.9375rem;
}
.t-nav a { color: var(--t-muted); text-decoration: none; }
.t-nav a:hover { color: var(--t-parchment); }
.t-nav .t-cta {
  color: var(--t-table);
  background: var(--t-candle);
  padding: 0.4rem 0.9rem;
  border-radius: 2px;
}
.t-nav .t-cta:hover { color: var(--t-table); background: #f0c477; }

/* The header on a phone.

   Six links, a brand and a button do not fit across 320px, and the row was
   not allowed to wrap — so it simply overflowed, and the CTA left the screen
   sideways. It becomes two lines instead of a hamburger on purpose: this is a
   text site with at most six destinations, and a disclosure menu is a control
   somebody has to find, open and close to reach a link that would have fitted
   on screen anyway. Nothing here loads a bundle, so a menu that opens would
   have to be a checkbox pretending to be a button.

   The brand takes its own line and the links wrap centred underneath, tighter
   and a shade smaller, with the CTA last so the row reads as navigation
   ending in an action. */
@media (max-width: 46rem) {
  .t-header { padding: 0.85rem 0; }
  .t-header .t-wrap {
    flex-direction: column;
    align-items: center;
    gap: 0.7rem;
    text-align: center;
  }
  .t-nav {
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem 1rem;
    font-size: 0.875rem;
  }
  /* Big enough to hit with a thumb without turning the row into buttons. */
  .t-nav a { padding: 0.15rem 0; }
}

/* The bare header: no world chosen yet (site_header.htm's {else}). The
   brand and two controls fit one row even at 320px, so it stays a row. */
.t-header-bare .t-brand { display: flex; align-items: center; gap: 0.55rem; font-size: 1.2rem; }
.t-header-bare .t-brand-mark { border-radius: 7px; }
@media (max-width: 46rem) {
  .t-header.t-header-bare { padding: max(0.7rem, env(safe-area-inset-top, 0px)) 0 0.7rem; }
  .t-header-bare .t-wrap { flex-direction: row; flex-wrap: nowrap; justify-content: space-between; text-align: left; gap: 0.5rem; }
  .t-header-bare .t-brand { font-size: 1.05rem; min-width: 0; }
  .t-header-bare .t-brand span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
  .t-header-bare .t-nav { flex-wrap: nowrap; gap: 0.5rem; flex: none; }
  .t-header-bare .t-style-switch select { max-width: 5.5rem; }
  .t-header-bare .t-nav .t-cta { padding: 0.4rem 0.7rem; }
}

/* ---- body ---- */

.t-main { padding: 2.5rem 0 4rem; }

.longtext { max-width: var(--t-measure); }
.longtext h1 {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 5vw, 2.6rem);
  line-height: 1.15;
  margin: 0 0 1.5rem;
}
.longtext h3 {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: 1.3rem;
  color: var(--t-candle);
  margin: 2.25rem 0 0.6rem;
}
.longtext p, .longtext li { margin: 0 0 1rem; }
.longtext a { color: var(--t-candle); }
.longtext strong { color: #fff; font-weight: 600; }

/* The fan of cards under the button. Full width of .t-ask-inner, which is the
   width of the question box above it — they line up because they share a
   container, not because a number was matched by hand. */
.t-ask-fan {
  display: block;
  width: 100%;
  height: auto;
  margin: 1.5rem auto 0;
  opacity: 0.92;
  filter: drop-shadow(0 0.6rem 1.6rem rgba(0, 0, 0, 0.55));
}

/* ---- the wordmark ---- */

.t-wordmark {
  display: block;
  width: min(20rem, 78%);
  height: auto;
  margin: 0 auto;
  /* Gold leaf on a photograph of a dark room needs its own shadow to sit on,
     or it competes with whatever detail happens to be behind it — and across
     ten scenes, something always is. */
  filter: drop-shadow(0 0.15rem 1.6rem rgba(0, 0, 0, 0.9));
}
.t-brand { display: inline-flex; align-items: center; gap: 0.55rem; }
.t-brand-mark { display: block; width: 1.9rem; height: 1.9rem; }

/* ---- the backdrop ----

   Ten candlelit rooms, one at a time, changed every ten minutes. See
   app/zygote.json (theme.backdrops) for the list and App\Handlers\Backdrop for
   which one is showing.

   It sits behind everything the reading flow draws, dimmed by the layer that
   was already there: .t-ask::before and .t-reveal::before paint the candle
   glow, and under `t-has-backdrop` they stop painting the solid table colour
   and paint a scrim instead. Legibility comes first — the room is furniture
   for the question, not the other way round. */

.t-backdrop {
  position: fixed;
  inset: 0;
  z-index: -3;
  overflow: hidden;
  background: var(--t-ink);
}
.t-backdrop-layer {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  /* Slow, because a room should change the way a candle gutters and not the
     way a slide advances. Nobody should be able to point at the moment. */
  transition: opacity 2.4s ease-in-out;
}
.t-backdrop-layer.is-current { opacity: var(--t-backdrop-opacity, 0.5); }
.t-backdrop-layer img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Muted at the source rather than only buried under a scrim.
     Dimming with opacity alone fades the room toward the ink behind it, which
     flattens it and leaves the colour as strong as ever — a bright oil
     painting turned down is still a bright oil painting. Turning the
     photograph itself down keeps its contrast and its depth while taking it
     out of competition with the text. The values are the edition's: see
     theme.backdrops in app/zygote.json. */
  filter:
    brightness(var(--t-backdrop-brightness, 1))
    saturate(var(--t-backdrop-saturate, 1));
}

/* With a photograph behind it, the base layer stops being the table colour and
   becomes a scrim over the room: enough to keep parchment text at full
   contrast, little enough to still read as a lit interior. */
body.t-has-backdrop .t-ask::before,
body.t-has-backdrop .t-reveal::before {
  background:
    radial-gradient(60rem 40rem at 50% -8%, rgba(232, 182, 97, 0.12), transparent 60%),
    linear-gradient(180deg, rgba(13, 11, 9, 0.42) 0%, rgba(13, 11, 9, 0.66) 100%);
}
/* Two jobs in one layer: darken the corners so the room has depth, and put a
   soft pool of shadow under the middle where the question and the card are.
   Without the second half the text sits on whatever the photograph happens to
   be doing there, which across ten scenes is not something that can be relied
   on. */
body.t-has-backdrop .t-ask::after,
body.t-has-backdrop .t-reveal::after {
  background:
    radial-gradient(38rem 30rem at 50% 45%, rgba(13, 11, 9, 0.72) 0%, rgba(13, 11, 9, 0.35) 55%, transparent 78%),
    radial-gradient(120% 90% at 50% 40%, transparent 30%, rgba(0, 0, 0, 0.65) 100%);
}

@media (prefers-reduced-motion: reduce) {
  /* The swap still happens — it is a change of room, not an animation — but it
     happens at once rather than dissolving. */
  .t-backdrop-layer { transition: none; }
}

/* Cultural style selection. The cards use each style's own wordmark and copy;
   the flag remains a compact identifier in the persistent switcher. */
.t-style-picker { max-width: 68rem; margin: 4rem auto; text-align: center; }
.t-style-picker h1 { font-family: var(--t-display); font-weight: 500; }
.t-style-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; margin-top: 2rem; }
.t-style-grid form { margin: 0; }
.t-style-card { position: relative; isolation: isolate; overflow: hidden; width: 100%; min-height: 20rem; padding: 1.5rem; border: 1px solid var(--t-brass); border-radius: 0.4rem; color: var(--t-parchment); background: #15110e var(--t-style-preview) center / cover no-repeat; cursor: pointer; }
.t-style-card:hover, .t-style-card:focus-visible { border-color: var(--t-candle); transform: translateY(-2px); }
.t-style-card-scrim { position: absolute; z-index: -1; inset: 0; background: linear-gradient(rgba(8, 6, 5, 0.3), rgba(8, 6, 5, 0.88)); }
.t-style-card-content { min-height: 17rem; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 0.85rem; }
.t-style-card img { width: min(100%, 18rem); height: 8rem; object-fit: contain; }
.t-style-card strong { font: 1.45rem var(--t-display); }
.t-style-flag { font-size: 2rem; }
.t-worlds-brand { margin-top: 2.5rem; }
.t-worlds-brand img { width: 12rem; max-width: 100%; height: auto; }
.t-worlds-brand p { margin: 0.75rem 0 0; font: 1.5rem var(--t-display); color: var(--t-candle); }
.t-style-switch select, .t-reading-style-switch select { color: var(--t-parchment); background: var(--t-table); border: 1px solid var(--t-brass); border-radius: 3px; padding: 0.35rem; }
.t-style-switch button, .t-reading-style-switch button { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.t-reading-style-switch { position: fixed; z-index: 8; top: max(0.75rem, env(safe-area-inset-top)); right: 0.75rem; margin: 0; }
@media (max-width: 44rem) { .t-style-grid { grid-template-columns: 1fr; } .t-style-picker { margin: 2rem auto; } }

/* ---- the disclaimer ----

   Small and quiet, but never hidden: it sits under the answer on the reveal,
   under the question box on the ask screen, and in the footer of every page
   that is not the reading flow. Muted rather than shouted — a warning styled
   like a warning is read once and then filtered out. */

.t-disclaimer {
  max-width: var(--t-measure);
  margin: 1.75rem auto 0;
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.78rem;
  line-height: 1.6;
}
.t-disclaimer a { color: var(--t-brass); }
.t-disclaimer-quiet { margin-top: 0.6rem; opacity: 0.85; }

.t-footer .t-disclaimer { margin: 0.35rem 0 0; }

/* ---- the cookie dialog ----

   Drawn only where there is a measurement id to consent to, so on most
   deployments none of this is ever used — see google_analytics.htm.

   A bar along the bottom rather than a modal over the middle: nothing here is
   urgent enough to interrupt a reading, and a dialog that covers the card
   people came for gets dismissed unread, whichever button is nearest. The
   three buttons are the same size and the same weight on purpose. */

.t-consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  padding: 1rem 1.25rem;
  color: var(--t-parchment);
  background: rgba(13, 11, 9, 0.97);
  border-top: 1px solid rgba(168, 130, 60, 0.45);
  box-shadow: 0 -0.6rem 2rem rgba(0, 0, 0, 0.45);
  font-family: var(--t-ui);
  font-size: 0.85rem;
  line-height: 1.55;
}
.t-consent[hidden] { display: none; }

.t-consent-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  max-width: 72rem;
  margin: 0 auto;
}
.t-consent-words { max-width: 46rem; }
.t-consent-title {
  display: block;
  margin-bottom: 0.2rem;
  font-family: var(--t-display);
  font-size: 1.05rem;
  font-weight: 400;
  color: var(--t-candle);
}
.t-consent-text { margin: 0; color: var(--t-parchment); }
.t-consent-text a { color: var(--t-candle); }

.t-consent-buttons { display: flex; flex-wrap: wrap; gap: 0.6rem; }

.t-consent-btn {
  flex: 0 0 auto;
  padding: 0.55rem 1rem;
  font-family: var(--t-ui);
  font-size: 0.85rem;
  color: var(--t-table);
  background: var(--t-candle);
  border: 1px solid var(--t-candle);
  border-radius: 2px;
  cursor: pointer;
}
/* Equal weight, not equal colour: "no" must be as easy to press as "yes", and
   the ICO has said as much. Same size, same type, same row. */
.t-consent-btn-quiet {
  color: var(--t-parchment);
  background: transparent;
  border-color: rgba(168, 130, 60, 0.55);
}
.t-consent-btn:hover { border-color: #ffd79a; }

.t-consent-details {
  max-width: 72rem;
  margin: 1rem auto 0;
  padding-top: 1rem;
  border-top: 1px solid rgba(168, 130, 60, 0.25);
}
.t-consent-details[hidden] { display: none; }
.t-consent-group { margin-bottom: 1rem; max-width: 46rem; }
.t-consent-switch {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--t-candle);
  cursor: pointer;
}
.t-consent-switch input { accent-color: var(--t-brass); }
.t-consent-switch input[disabled] { cursor: default; }
.t-consent-group-text { margin: 0.3rem 0 0 1.7rem; color: var(--t-muted); }

@media (max-width: 46rem) {
  .t-consent { max-height: 80vh; overflow-y: auto; }
  .t-consent-inner { flex-direction: column; align-items: flex-start; gap: 0.9rem; }
  .t-consent-buttons { width: 100%; }
  .t-consent-btn { flex: 1 1 auto; text-align: center; }
}

/* ---- wiki ----

   The pages search brings people in on. Nothing here loads a bundle or a
   remote font, and the layout stays a single column: this is text, and the
   measure is the design. */

.t-wiki { max-width: 58rem; margin: 0 auto; }

.t-wiki-crumbs {
  margin: 0 0 1.75rem;
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
}
.t-wiki-crumbs a { color: var(--t-brass); text-decoration: none; }
.t-wiki-crumbs a:hover { color: var(--t-candle); }

.t-wiki-body h2 {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: 1.45rem;
  color: var(--t-candle);
  margin: 2.5rem 0 0.75rem;
}
.t-wiki-body ul { padding-left: 1.1rem; }
.t-wiki-body li { margin-bottom: 0.5rem; }
.t-wiki-body em { color: var(--t-muted); font-style: italic; }

/* The card itself. Floated beside the meanings where there is room for both,
   and above them where there is not — a 420px card next to a 68ch measure on a
   phone would leave a column two words wide. */
.t-wiki-card {
  display: block;
  width: min(15rem, 55%);
  height: auto;
  margin: 0 0 1.5rem;
  border-radius: 3px;
  box-shadow: 0 0.9rem 2rem rgba(0, 0, 0, 0.45);
}

@media (min-width: 46rem) {
  .t-wiki-card {
    float: right;
    width: 16rem;
    margin: 0.35rem 0 1.5rem 2rem;
  }
}

/* ---- product screens ----

   `wiki/how-it-works` shows the app rather than describing it. A grid rather
   than a float, and for the same reason `.t-wiki-card` stops floating below
   46rem: a 460px phone screen beside a 68ch measure leaves a column two words
   wide. `clear: both` because the section above one of these may have floated
   a card. */
.t-wiki-screens {
  clear: both;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1.5rem;
  margin: 2rem 0 2.5rem;
}
.t-wiki-screens figure { margin: 0; }
.t-wiki-screens img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  box-shadow: 0 0.9rem 2rem rgba(0, 0, 0, 0.45);
}
.t-wiki-screens figcaption {
  margin-top: 0.6rem;
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.85rem;
  line-height: 1.5;
}
.t-wiki-screens figcaption b { color: var(--t-parchment); font-weight: 600; }

/* ---- the stories ----

   `wiki/stories/*` — one reading told whole: somebody's question, the card that
   came up, the one word under it and the interpretation that followed. The
   verdict reuses `.t-verdict` from the reveal on purpose; a story that printed
   its Yes in a different face from the one the app prints would be describing a
   product this is not. */

/* A card that came out upside down is shown upside down, the same rule as
   `.t-card-reversed img` on the reveal. Spelled separately because a wiki page
   has no figure around its card — the class goes on the image itself. */
.t-wiki-card.is-reversed { transform: rotate(180deg); }

/* The question, as it was typed. */
.t-wiki-asked {
  margin: 0 0 1.5rem;
  padding: 0.35rem 0 0.35rem 1.15rem;
  border-left: 2px solid var(--t-brass);
  font-family: var(--t-display);
  font-size: 1.2rem;
  line-height: 1.45;
  color: var(--t-candle);
}

/* The standing line at the foot of every story: these people are invented.
   Quiet, and never quiet enough to miss — a page of fictional readings that
   did not say so would read as testimony. */
.t-wiki-note {
  clear: both;
  margin: 2rem 0 0;
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.6;
}

/* Where the page ends: the thing the site actually does. */
.t-wiki-ask {
  clear: both;
  max-width: var(--t-measure);
  margin: 3rem 0 0;
  padding: 1.5rem;
  border: 1px solid rgba(168, 130, 60, 0.32);
  background: rgba(0, 0, 0, 0.2);
}
.t-wiki-ask-lead {
  margin: 0 0 1rem;
  font-family: var(--t-display);
  font-size: 1.15rem;
  line-height: 1.45;
  color: var(--t-parchment);
}

.t-wiki-missing { max-width: var(--t-measure); }
.t-wiki-missing h1 {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 5vw, 2.6rem);
  margin: 0 0 1rem;
}
.t-wiki-missing-links { display: flex; align-items: center; gap: 1.25rem; }
.t-wiki-missing-links a:not(.t-button) { color: var(--t-candle); }

/* ---- journal ---- */

.t-journal { max-width: 58rem; margin: 0 auto; }
.t-journal a { color: var(--t-candle); }
.t-journal-head { max-width: var(--t-measure); margin-bottom: 2.5rem; }
.t-eyebrow {
  margin: 0 0 0.35rem;
  color: var(--t-brass);
  font-family: var(--t-ui);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.t-journal-head h1 {
  margin: 0 0 0.75rem;
  font-family: var(--t-display);
  font-size: clamp(2rem, 6vw, 3rem);
  font-weight: 400;
  line-height: 1.1;
}
.t-journal-guest, .t-journal-empty {
  padding: 1rem 1.15rem;
  border: 1px solid rgba(168, 130, 60, 0.32);
  background: rgba(0, 0, 0, 0.18);
}
.t-journal-list { display: grid; gap: 1.4rem; }
.t-journal-entry {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  gap: 1.4rem;
  padding: 1.25rem;
  border: 1px solid rgba(168, 130, 60, 0.25);
  background: rgba(0, 0, 0, 0.2);
}
.t-journal-card img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 3px;
  box-shadow: 0 0.7rem 1.5rem rgba(0, 0, 0, 0.35);
}
.t-journal-card img.is-reversed { transform: rotate(180deg); }
.t-journal-copy time {
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.82rem;
}
.t-journal-copy h2 {
  margin: 0.25rem 0 0.35rem;
  font-family: var(--t-display);
  font-size: 1.45rem;
  font-weight: 400;
  line-height: 1.25;
}
.t-journal-copy h2 a { color: var(--t-parchment); text-decoration: none; }
.t-journal-card-name, .t-journal-verdict { color: var(--t-candle); }
/* The interpretation and the offer of one both span the entry, at every width.

   They are grid children of `.t-journal-entry` rather than of the copy column,
   so an interpretation runs the full width under the card instead of being set
   in the strip beside it. `grid-column` on something that is not a grid child
   does nothing, which is what the old mobile rule here was quietly doing. */
.t-journal-deep,
.t-journal-interpret {
  grid-column: 1 / -1;
  margin: 0.4rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid rgba(168, 130, 60, 0.2);
}
.t-journal-deep { color: #d4c9b4; }
.t-journal-deep-text {
  margin: 0;
  line-height: 1.65;
}
.t-journal-more {
  display: inline-block;
  margin-top: 0.6rem;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  text-decoration: none;
  border-bottom: 1px solid rgba(168, 130, 60, 0.5);
}
.t-journal-more:hover { border-bottom-color: var(--t-candle); }
/* Phase 14. An entry that is a conversation has one obvious thing to do with
   it, and "read this again" and "carry this on" cannot look the same — the
   second is the only one of the two that goes anywhere new. Lit rather than
   boxed: a journal page holds twelve entries, and twelve buttons is a toolbar,
   not a record of what somebody asked. */
.t-journal-carryon {
  color: var(--t-candle);
  border-bottom-color: var(--t-candle);
}
/* The arrow goes on the line, not on the link, so it still ends the line when
   a price follows: "Carry on · uses a free reading →" rather than an arrow
   stranded in the middle of it. */
.t-journal-carryline {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.35rem;
}
.t-journal-carryline::after {
  content: "\2192";
  align-self: baseline;
  color: var(--t-candle);
  /* Matches .t-journal-more, which carries the space above this line. */
  margin-top: 0.6rem;
}
/* What the next block costs, beside the offer and only on a conversation that
   has run out. Quiet: it is a fact about the price, not a second action. */
.t-journal-price {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-muted);
  margin-top: 0.6rem;
}
.t-journal-interpret { font-family: var(--t-ui); font-size: 0.9rem; }
.t-journal-interpret a { text-decoration: none; border-bottom: 1px solid rgba(168, 130, 60, 0.5); }
.t-journal-interpret a:hover { border-bottom-color: var(--t-candle); }
.t-journal-pending { color: var(--t-muted); }
/* ---- what you have bought ---- */

.t-purchases { margin-top: 2.5rem; }
.t-purchases h2 {
  margin: 0 0 0.75rem;
  font-family: var(--t-display);
  font-size: 1.3rem;
  font-weight: 400;
}
.t-purchase-list { margin: 0; padding: 0; list-style: none; }
.t-purchase {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.9rem;
  padding: 0.7rem 0;
  border-top: 1px solid rgba(168, 130, 60, 0.2);
  font-family: var(--t-ui);
  font-size: 0.92rem;
}
.t-purchase time { color: var(--t-muted); min-width: 8.5rem; }
.t-purchase-what { flex: 1 1 8rem; }
.t-purchase-price { color: var(--t-candle); }
/* An attempt that never completed is history and not an achievement: it is
   here so somebody who thinks they paid can see what actually happened, and
   it should not read like a receipt. */
.t-purchase.is-unfinished { color: var(--t-muted); }
.t-purchase.is-unfinished .t-purchase-price { color: inherit; }
.t-purchase-state { flex-basis: 100%; color: var(--t-muted); font-size: 0.85rem; }
.t-purchase-test {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border: 1px solid rgba(168, 130, 60, 0.4);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.7rem;
}
@media (max-width: 30rem) {
  .t-purchase time { min-width: 0; flex-basis: 100%; }
}

.t-pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1.25rem;
  margin-top: 2rem;
  font-family: var(--t-ui);
}
.t-button {
  display: inline-block;
  padding: 0.55rem 1rem;
  color: var(--t-table) !important;
  background: var(--t-candle);
  text-decoration: none;
}
@media (max-width: 38rem) {
  .t-journal-entry { grid-template-columns: 5.5rem minmax(0, 1fr); gap: 1rem; padding: 0.9rem; }
  .t-journal-copy h2 { font-size: 1.25rem; }
}

/* ---- footer ---- */

.t-footer {
  border-top: 1px solid rgba(168, 130, 60, 0.25);
  padding: 2rem 0 3rem;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-muted);
}
.t-footer a { color: var(--t-muted); text-decoration: none; }
.t-footer a:hover { color: var(--t-parchment); }
.t-footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  margin-bottom: 1.5rem;
}
.t-footer-legal > div { margin-bottom: 0.35rem; }
/* A world's own channels (_shared/social_links.htm): small brass-ringed
 * medallions, the marks in parchment, lit by the candle on hover. Shown only
 * while a world with accounts is selected. */
.t-channels { margin: 0 0 1.5rem; }
.t-channels-title {
  margin: 0 0 0.6rem;
  font-family: var(--t-display, "EB Garamond", Georgia, serif);
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--t-brass);
}
.t-channels-row { display: flex; flex-wrap: wrap; gap: 0.7rem; }
.t-footer a.t-channel,
.t-channel {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  border: 1px solid rgba(168, 130, 60, 0.45);
  background: radial-gradient(circle at 50% 35%, rgba(232, 182, 97, 0.10), rgba(13, 11, 9, 0.35) 70%);
  color: var(--t-parchment);
  box-shadow: inset 0 0 0 3px rgba(13, 11, 9, 0.55), inset 0 0 0 4px rgba(168, 130, 60, 0.18);
  transition: color 0.2s, border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
.t-channel svg { width: 1.1rem; height: 1.1rem; fill: currentColor; }
.t-footer a.t-channel:hover,
.t-channel:hover,
.t-channel:focus-visible {
  color: #ffd79a;
  border-color: var(--t-candle);
  box-shadow: inset 0 0 0 3px rgba(13, 11, 9, 0.55), inset 0 0 0 4px rgba(232, 182, 97, 0.35), 0 0 14px rgba(232, 182, 97, 0.35);
  transform: translateY(-1px);
}
.t-channels-you { text-align: center; margin: 0; }
.t-channels-you .t-channels-row { justify-content: center; gap: 0.9rem; }
.t-channels-you .t-channel { width: 3rem; height: 3rem; }
.t-channels-you .t-channel svg { width: 1.25rem; height: 1.25rem; }
/* The artist's name. Public domain requires no attribution — this is here
 * because she went uncredited for most of a century. */
.t-deck-credit { font-style: italic; }

/* ---- the reading flow's shell ---- */
/* Only what has to exist before the bundle runs: the ground colour, so the
 * page is never a white flash, and the noscript fallback. Everything else is
 * the React app's, themed from the Zygote. */

body.t-reading {
  margin: 0;
  background: var(--t-table);
  color: var(--t-parchment);
  font-family: var(--t-body);
}
.t-noscript {
  max-width: var(--t-measure);
  margin: 4rem auto;
  padding: 0 1.25rem;
  text-align: center;
}
.t-noscript a { color: var(--t-candle); }

/* =========================================================================
   The ask screen.

   Server-rendered and styled here so the page is complete before any
   JavaScript runs. React hydrates this markup in Phase 3 and adds the shuffle
   and the reveal; it does not redraw it.

   The atmosphere is built from gradients rather than images on purpose — the
   theme's textures are not in the repository yet, and candlelight is a radial
   gradient before it is a photograph. When the artwork lands it layers on top
   of this rather than replacing it, so a missing file is never a blank page.
   ========================================================================= */

.t-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;
}

.t-ask {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.25rem;
  position: relative;
  isolation: isolate;
}

/* Candlelight: a warm pool from above, falling off into the dark of the table.
   Two stops rather than one, because a single radial reads as a spotlight and
   this wants to read as a flame in a room. */
.t-ask::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(60rem 40rem at 50% -8%, rgba(232, 182, 97, 0.16), transparent 60%),
    radial-gradient(30rem 24rem at 50% 4%, rgba(255, 215, 154, 0.10), transparent 70%),
    var(--t-table);
}

/* The vignette, and the grain the theme asks for. Both fixed, so scrolling a
   long question does not drag the atmosphere with it. */
.t-ask::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 40%, transparent 45%, rgba(0, 0, 0, 0.55) 100%);
}

.t-ask-inner {
  width: 100%;
  max-width: 34rem;
  text-align: center;
  margin: auto 0;
  /* A flex item defaults to min-width:auto, which refuses to shrink below its
     content's intrinsic width — and a <textarea> brings one with it, from its
     `cols` attribute. On a narrow phone that pushed the whole column wider
     than the viewport and the page scrolled sideways. This is the standard
     fix and it has to stay. */
  min-width: 0;
}

.t-ask-title {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(2.2rem, 8vw, 3.4rem);
  line-height: 1.05;
  letter-spacing: 0.01em;
  margin: 0 0 0.6rem;
  color: var(--t-parchment);
  /* The faintest glow, as if the page itself were near the candle. */
  text-shadow: 0 0 2.5rem rgba(232, 182, 97, 0.28);
}

.t-ask-sub {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  color: var(--t-muted);
  margin: 0 0 2.25rem;
}

.t-ask-form { margin: 0; }

.t-ask-input {
  width: 100%;
  max-width: 100%;
  display: block;
  /* 16px minimum, or iOS Safari zooms the whole composition on focus. */
  font-size: 1.0625rem;
  font-family: var(--t-body);
  line-height: 1.5;
  color: var(--t-parchment);
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(168, 130, 60, 0.35);
  border-radius: 3px;
  padding: 0.9rem 1rem;
  resize: none;
  text-align: center;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.t-ask-input::placeholder { color: rgba(138, 127, 109, 0.75); }
.t-ask-input:focus {
  outline: none;
  border-color: var(--t-candle);
  box-shadow: 0 0 0 1px rgba(232, 182, 97, 0.25), 0 0 2rem rgba(232, 182, 97, 0.12);
}

/* "Ask a slightly longer question" — a small line under the box, not a
   message at the top of the page (3 Nov 2026). main.jsx shows it, and it fades
   away on its own about three seconds later. */
.t-ask-hint {
  margin: 0.35rem 0 0;
  min-height: 1.1em;
  font-size: 0.78rem;
  line-height: 1.1;
  text-align: center;
  color: var(--t-candle);
  opacity: 0;
  transition: opacity 600ms ease;
  pointer-events: none;
}
.t-ask-hint.t-ask-hint-on { opacity: 0.9; transition: opacity 150ms ease; }

.t-ask-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.t-ask-submit {
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--t-table);
  background: var(--t-candle);
  border: 0;
  border-radius: 3px;
  padding: 0.75rem 1.75rem;
  cursor: pointer;
  transition: background 160ms ease, transform 160ms ease;
}
.t-ask-submit:hover { background: #f2c87d; }
.t-ask-submit:active { transform: translateY(1px); }

.t-ask-voice {
  font-size: 1.125rem;
  line-height: 1;
  color: var(--t-candle);
  background: transparent;
  border: 1px solid rgba(168, 130, 60, 0.35);
  border-radius: 3px;
  padding: 0.7rem 0.85rem;
  cursor: pointer;
}
.t-ask-voice:hover { border-color: var(--t-candle); }
.t-ask-voice svg { display: block; width: 1.25rem; height: 1.25rem; fill: currentColor; stroke: currentColor; }
/* Whichever face is not in use is not in the layout either, so the button does
   not change size when it starts listening. */
.t-ask-voice .t-ask-voice-stop { display: none; }
.t-ask-voice.is-listening .t-ask-voice-mic { display: none; }
.t-ask-voice.is-listening .t-ask-voice-stop { display: block; }
.t-ask-voice[disabled] { cursor: default; opacity: 0.6; }

/* Listening. The candle pulse is the only thing on the screen that says the
   microphone is open, so it is deliberately obvious — and it stops for anyone
   who asked not to be moved, who gets the steady lit border instead. */
.t-ask-voice.is-listening {
  color: var(--t-parchment);
  border-color: var(--t-candle);
  box-shadow: 0 0 0 0 rgba(232, 182, 97, 0.5);
  animation: t-voice-pulse 1.6s ease-out infinite;
}
.t-ask-voice.is-working { opacity: 0.7; }

@keyframes t-voice-pulse {
  70% { box-shadow: 0 0 0 0.55rem rgba(232, 182, 97, 0); }
  100% { box-shadow: 0 0 0 0 rgba(232, 182, 97, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .t-ask-voice.is-listening { animation: none; box-shadow: 0 0 0.6rem rgba(232, 182, 97, 0.35); }
}

/* What the microphone heard, while it is hearing it. Empty until there is
   something to say, so the ask screen keeps its one-thing-on-it shape. */
.t-ask-voice-status {
  margin: 0.85rem 0 0;
  min-height: 1.2em;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--t-muted);
}
.t-ask-voice-status:empty { margin: 0; }

.t-ask-notice {
  margin: 1.5rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-accent);
  background: rgba(107, 47, 47, 0.16);
  border: 1px solid rgba(107, 47, 47, 0.45);
  border-radius: 3px;
  padding: 0.6rem 0.9rem;
}

.t-ask-free {
  margin: 1.75rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-muted);
}
/* The way to buy more sits on the allowance line rather than in a panel of its
   own — the ask screen holds the question and almost nothing else. It has to be
   given the site's colour explicitly: this is one of two screens with no
   `.longtext` or `.t-footer` around it, so an unstyled link here is browser
   blue, and then browser purple once it has been followed. */
.t-ask-free a { color: var(--t-candle); }
.t-ask-free a:visited { color: var(--t-candle); }
.t-ask-free a:hover { color: #f0c477; }

.t-ask-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.25rem;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  padding-top: 2rem;
}
.t-ask-foot a { color: rgba(138, 127, 109, 0.8); text-decoration: none; }
.t-ask-foot a:hover { color: var(--t-parchment); }
.t-ask-foot .t-channels-ask { flex-basis: 100%; margin: 0.5rem 0 0; }
.t-channels-ask .t-channels-title { display: none; }
.t-channels-ask .t-channels-row { justify-content: center; gap: 0.6rem; }
.t-ask-foot a.t-channel { color: rgba(230, 220, 198, 0.75); width: 2.3rem; height: 2.3rem; }
.t-ask-foot a.t-channel:hover { color: #ffd79a; }
.t-channels-ask .t-channel svg { width: 1rem; height: 1rem; }

/* Spacing on both sides, and the bottom half is load-bearing.

   What follows the interpretation is "Ask another", and `.t-reveal-again` has
   no top margin of its own — it had always borrowed the gap from `.t-not-saved`
   above it. That notice is only drawn for a guest, so a signed-in reader got
   the button hard against this panel. A block that owns its own spacing cannot
   be broken by a neighbour that turns out to be conditional. */
.t-deep {
  margin: 2rem 0;
  /* DeepReading.jsx scrolls this panel to the top of the window when the orb
     appears. Flush against the edge reads as a page that has lost its top, so
     the scroll stops a little short of it. */
  scroll-margin-top: 1rem;
  padding: 1.5rem 1.25rem;
  border: 1px solid rgba(168, 130, 60, 0.28);
  border-radius: 4px;
  background: rgba(18, 13, 10, 0.42);
}
.t-deep-button { max-width: 100%; }
.t-deep-text {
  white-space: pre-wrap;
  text-align: left;
  font-family: var(--t-body);
  line-height: 1.7;
  color: var(--t-parchment);
}
.t-deep-working, .t-deep-error {
  margin: 0.9rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-muted);
}
.t-deep-error { color: var(--t-accent); }
.t-deep-retry {
  margin-top: 0.75rem;
  color: var(--t-candle);
  background: transparent;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}

/* A small scrying sphere while the model writes. Entirely CSS so the edition's
   candle/table palette carries it, and there is no extra image on the wait. */
.t-scrying {
  display: grid;
  justify-items: center;
  padding: 0.35rem 0 0;
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.8125rem;
}
.t-scrying-orb {
  position: relative;
  width: 6.75rem;
  aspect-ratio: 1;
  overflow: hidden;
  border: 1px solid rgba(232, 182, 97, 0.48);
  border-radius: 50%;
  background:
    radial-gradient(circle at 34% 27%, rgba(255, 239, 205, 0.48) 0 2%, transparent 10%),
    radial-gradient(circle at 48% 58%, rgba(111, 78, 132, 0.42), transparent 49%),
    radial-gradient(circle at 50% 50%, rgba(67, 46, 79, 0.72), rgba(10, 8, 13, 0.96) 72%);
  box-shadow:
    inset -1rem -1.2rem 2rem rgba(0, 0, 0, 0.55),
    inset 0.6rem 0.5rem 1.2rem rgba(255, 224, 172, 0.1),
    0 0 1.7rem rgba(232, 182, 97, 0.2),
    0 1.1rem 2.2rem rgba(0, 0, 0, 0.65);
  animation: t-orb-breathe 2.2s ease-in-out infinite;
}
.t-scrying-mist {
  position: absolute;
  width: 7rem;
  height: 2.6rem;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(226, 207, 235, 0.3), transparent 68%);
  filter: blur(5px);
  animation: t-orb-drift 3s ease-in-out infinite alternate;
}
.t-scrying-mist-one { top: 22%; left: -28%; transform: rotate(18deg); }
.t-scrying-mist-two {
  right: -32%;
  bottom: 18%;
  transform: rotate(-14deg);
  animation-delay: -2.2s;
  animation-direction: alternate-reverse;
}
.t-scrying-star {
  position: absolute;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #ffe6ad;
  box-shadow: 0 0 0.55rem #f3bd69;
  animation: t-orb-star 1.35s ease-in-out infinite;
}
.t-scrying-star-one { top: 37%; left: 31%; }
.t-scrying-star-two { right: 27%; bottom: 31%; animation-delay: -1.1s; }
.t-scrying-stand {
  width: 5.25rem;
  height: 1.15rem;
  margin-top: -0.25rem;
  border-radius: 50% 50% 35% 35%;
  border: 1px solid rgba(168, 130, 60, 0.48);
  background: linear-gradient(#5f4727, #21170e 68%);
  box-shadow: 0 0.7rem 1.2rem rgba(0, 0, 0, 0.55);
}
.t-scrying p { margin: 1.15rem 0 0; letter-spacing: 0.035em; }
.t-scrying-status { min-height: 1.2em; }
.t-scrying-progress {
  position: relative;
  width: min(15rem, 72vw);
  height: 2px;
  margin-top: 0.85rem;
  overflow: hidden;
  border-radius: 2px;
  background: rgba(168, 130, 60, 0.18);
}
.t-scrying-progress span {
  position: absolute;
  inset-block: 0;
  left: -42%;
  width: 42%;
  border-radius: inherit;
  background: linear-gradient(90deg, transparent, var(--t-candle) 45%, #ffe6ad 60%, transparent);
  box-shadow: 0 0 0.65rem rgba(232, 182, 97, 0.65);
  animation: t-reading-progress 1.15s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

@keyframes t-orb-breathe {
  0%, 100% { box-shadow: inset -1rem -1.2rem 2rem rgba(0,0,0,.55), 0 0 1.4rem rgba(232,182,97,.15), 0 1.1rem 2.2rem rgba(0,0,0,.65); }
  50% { box-shadow: inset -1rem -1.2rem 2rem rgba(0,0,0,.48), 0 0 2.5rem rgba(232,182,97,.31), 0 1.1rem 2.2rem rgba(0,0,0,.65); }
}
@keyframes t-orb-drift {
  from { translate: -0.6rem 0.15rem; opacity: 0.4; }
  to { translate: 1.2rem -0.25rem; opacity: 0.9; }
}
@keyframes t-orb-star {
  0%, 100% { scale: 0.5; opacity: 0.25; }
  50% { scale: 1.4; opacity: 1; }
}
@keyframes t-reading-progress {
  from { translate: 0; }
  to { translate: 340%; }
}

@media (prefers-reduced-motion: reduce) {
  .t-scrying-orb, .t-scrying-mist, .t-scrying-star, .t-scrying-progress span { animation: none; }
  .t-scrying-progress span { left: 29%; }
}

/* Someone who has asked not to be moved gets no glow transitions either. */
@media (prefers-reduced-motion: reduce) {
  .t-ask-input, .t-ask-submit { transition: none; }
}

/* =========================================================================
   The reveal.

   Shares the ask screen's ground and candlelight (.t-ask::before is fixed and
   this reuses the same treatment), so moving from one to the other feels like
   staying in the same room.
   ========================================================================= */

.t-reveal {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 2.5rem 1.25rem 2rem;
  position: relative;
  isolation: isolate;
  /* The table under the card is wider than the column it sits in — on a phone,
     wider than the screen. It is centred on the same axis as everything else,
     so what hangs off the sides is the baize falling into the dark and nothing
     a reader could scroll to. Without this the page gains a sideways scroll
     and the card drifts off-centre with it. */
  overflow-x: hidden;
}
.t-reveal::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(60rem 40rem at 50% -8%, rgba(232, 182, 97, 0.16), transparent 60%),
    radial-gradient(30rem 24rem at 50% 4%, rgba(255, 215, 154, 0.10), transparent 70%),
    var(--t-table);
}
.t-reveal::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 40%, transparent 45%, rgba(0, 0, 0, 0.55) 100%);
}

.t-reveal-inner {
  width: 100%;
  max-width: 34rem;
  text-align: center;
  margin: auto 0;
  min-width: 0;
}

.t-reveal-question {
  font-family: var(--t-body);
  font-style: italic;
  font-size: 1.0625rem;
  color: var(--t-muted);
  margin: 0 0 1.75rem;
}

/* ---- the table ----

   The card is dealt onto something. Without it the reveal is a card floating
   in the dark, which is the one thing a reading is not: it happens on a table,
   between two people, and the surface is half of why it feels like one.

   `resources/art/table.png`, rendered by `cli.php art-import` — green baize
   with the edition's rosette in the middle of it, photographed from where the
   reader sits. That angle is the whole point: the perspective is already in
   the picture, so the card only has to agree with it.

   It is not the page's ground. The room behind it stays the room — the baize
   is a pool under the card that fades out before it reaches anything, so the
   reveal is still the same lit interior as the ask screen it came from.

   A background rather than an <img>: it is decoration with nothing to say to a
   screen reader, and unlike the backdrop nothing has to cross-fade it. AVIF
   with WebP behind it, at 1x and 2x, which is what image-set() is for; the
   plain url() first is for a browser too old to have it. */
.t-card-table {
  position: relative;
  isolation: isolate;
  /* The box the card is centred in, and the width the baize is measured
     against — not the baize itself, which is the ::before below and reaches
     well past this. The padding is the table showing above and below the card:
     without it the card is the full height of its own surface, which is a mat
     cut to its shape rather than something it was put down on.

     It is generous because the card is painted well outside this box: scaled
     up and tipped back, it rises above the top of it, and its cast shadow
     falls below the bottom. The padding is what keeps the question clear of
     the one and the card's name clear of the other — a transform moves what is
     drawn and never the space it was given. */
  width: min(100%, 30rem);
  margin: 0 auto 1.5rem;
  padding: 6.5rem 0 5rem;
}

.t-card-table::before {
  content: "";
  position: absolute;
  z-index: -1;
  /* The master's own shape, laid out from the bottom of the card and wider
     than the column it sits in — a table does not end where the text does.
     Nothing is cropped: the photograph already has the far edge receding, and
     that perspective is the only reason the card's tilt reads as lying down
     rather than as a leaning picture. */
  left: 50%;
  bottom: -1.5rem;
  width: min(190%, 58rem);
  aspect-ratio: 1536 / 1024;
  transform: translateX(-50%);
  background-image: var(--t-table-1x, url("/images/table-760.webp"));
  /* The box is the master's own aspect-ratio, so filling it exactly is the one
     size that neither crops the table nor stretches it. */
  background-size: 100% 100%;
  background-repeat: no-repeat;
  /* Lit by the same candle as everything else. The master is exposed for a
     dark room and, dropped in behind a scrim, goes to black — enough to make
     the card look like it is on nothing at all. */
  filter: brightness(1.9) contrast(1.12) saturate(1.1);
  /* No hard edge anywhere. The master's own corners already fall into the
     dark; this carries that the rest of the way, so the baize ends in the
     room's shadow rather than in a rectangle somebody can point at. */
  -webkit-mask-image: radial-gradient(ellipse 72% 72% at 50% 52%, #000 55%, transparent 100%);
  mask-image: radial-gradient(ellipse 72% 72% at 50% 52%, #000 55%, transparent 100%);
}

/* The card. A warm rim rather than a hard border — it should look lit, not
   framed. The aspect-ratio matches what deck-import renders, so the space is
   reserved before the image arrives and nothing shifts.

   And it lies down. `rotateX` tips the top of the card away from the reader,
   so it is narrower and shorter up there — the same direction the table's own
   edges run. Twenty-eight degrees at a short perspective is a card put down in
   front of somebody rather than held up to them; the `scale` is what buys that
   without losing any of the picture, because a tilted card is a shorter card
   and 1.3 hands the height back.

   The pivot is 95% rather than the bottom edge: hinged exactly at the edge,
   the card grows only upwards and walks out of its own table.

   The shadows are the other half of it: a tight one at the contact edge and a
   longer, softer one cast beyond it. A single big glow reads as levitation. */
.t-card {
  margin: 0 auto;
  width: min(62vw, 20rem);
  aspect-ratio: 420 / 726;
  border-radius: 6px;
  overflow: hidden;
  transform: perspective(42rem) rotateX(28deg) scale(1.30);
  transform-origin: 50% 95%;
  box-shadow:
    0 0 0 1px rgba(168, 130, 60, 0.4),
    0 0.2rem 0.6rem rgba(0, 0, 0, 0.60),
    0 1.5rem 2.75rem -0.5rem rgba(0, 0, 0, 0.50),
    0 0 4rem rgba(232, 182, 97, 0.14);
}
.t-card img { display: block; width: 100%; height: 100%; }

/* A reversed card is shown reversed. Printing the reversed *meaning* beside an
   upright picture is the one thing a tarot reader would never do. */
.t-card-reversed img { transform: rotate(180deg); }

.t-card-name {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.8rem, 6vw, 2.5rem);
  line-height: 1.1;
  margin: 0 0 0.25rem;
  color: var(--t-parchment);
  text-shadow: 0 0 2.5rem rgba(232, 182, 97, 0.25);
}
.t-card-orientation {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin: 0 0 0.75rem;
}

.t-verdict {
  font-family: var(--t-display);
  font-size: clamp(1.6rem, 5vw, 2.1rem);
  letter-spacing: 0.04em;
  margin: 1rem 0 1.25rem;
}
/* Three colours for fifteen answers, and the grouping is the meaning: the
   candle for an answer that opens something, the muted red for one that closes
   it, and the room's own muted parchment for everything that neither opens nor
   closes. A reader learns the three in one reading and then knows at a glance
   which way a verdict fell before they have read the word.

   A token with no rule here inherits .t-verdict and is drawn in the body
   colour, which is legible but says nothing — so a new one in
   App\Constants::VERDICTS belongs in this block as well as in front.txt. */
.t-verdict-yes,
.t-verdict-likely,
.t-verdict-soon,
.t-verdict-go_ahead { color: var(--t-candle); }

.t-verdict-no,
.t-verdict-unlikely,
.t-verdict-not_yet,
.t-verdict-let_go { color: #c2706b; }

.t-verdict-maybe,
.t-verdict-in_time,
.t-verdict-wait,
.t-verdict-take_care,
.t-verdict-look_within,
.t-verdict-ask_again { color: var(--t-muted); }

/* A declined question, in the space the verdict would have had.

   Not styled as an error and not styled as an answer. It takes the verdict's
   position but the meaning's weight — a refusal set in the largest type on the
   page shouts at somebody who has just asked about their health, and a refusal
   in fine print looks like a bug. Muted, in the room's own parchment, sitting
   where they were already looking. */
.t-declined {
  font-family: var(--t-display);
  font-size: clamp(1.05rem, 3vw, 1.25rem);
  line-height: 1.5;
  letter-spacing: 0.02em;
  color: var(--t-muted);
  max-width: 26rem;
  margin: 1rem auto 0.6rem;
}
.t-declined-more {
  margin: 0 0 1.5rem;
}
.t-declined-more a {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--t-brass);
  border-bottom: 1px solid rgba(168, 130, 60, 0.35);
  text-decoration: none;
  padding-bottom: 2px;
}

.t-card-meaning {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--t-parchment);
  margin: 0 auto 1.5rem;
  max-width: 30rem;
  text-align: left;
}

.t-card-keywords {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin: 0 0 2rem;
}
.t-card-keywords span {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--t-brass);
  border: 1px solid rgba(168, 130, 60, 0.3);
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
}

/* What the reader sees in the card — Phase 14.

   It sits UNDER a finished reading, so it is styled as a quiet aside and not
   as a step: no panel border on the invitation, no accent colour on the
   heading, nothing that reads as "you are not done yet". The reading above it
   keeps the emphasis it had. */
.t-assoc {
  max-width: 30rem;
  margin: 0 auto 2rem;
  text-align: left;
}
.t-assoc-h {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin: 0 0 0.5rem;
}
.t-assoc-q {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--t-parchment);
  margin: 0 0 0.9rem;
}
.t-assoc-form textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
  font-family: var(--t-body);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--t-parchment);
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid rgba(168, 130, 60, 0.3);
  border-radius: 4px;
  padding: 0.7rem 0.8rem;
  resize: vertical;
}
.t-assoc-form textarea:focus {
  outline: none;
  border-color: rgba(232, 182, 97, 0.55);
}
.t-assoc-form button {
  margin-top: 0.7rem;
}

/* Their own words, set as a quotation because that is what they are. The rule
   down the left is the only ornament in this block. */
.t-assoc-said {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.6;
  font-style: italic;
  color: var(--t-parchment);
  border-left: 2px solid rgba(168, 130, 60, 0.4);
  margin: 0 0 1.4rem;
  padding: 0.1rem 0 0.1rem 0.9rem;
}
.t-assoc-reply {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--t-parchment);
}
.t-assoc-none {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--t-muted);
  margin: 0;
}

/* A reading that continues — Phase 14.

   One column: the cards and the conversation are the same scroll. A session
   that pushes the cards off the top and becomes a chat window has thrown away
   the thing it is a session *of*.

   There is no counter, no progress bar and no step indicator here, and there
   must never be one. See docs/Development/phase-14-sessions.md, part 1. */
/* The way on from a finished reading to the conversation it opened. */
.t-continue {
  max-width: 30rem;
  margin: 0 auto 2rem;
  text-align: left;
}
.t-continue-hint {
  display: block;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--t-muted);
  margin-top: 0.55rem;
}

.t-session { text-align: left; }

/* A clarification card: one card, beside the layout rather than in it — it was
   not dealt into a named place, and giving it one would say the spread had a
   position it never had.

   Qualified with `.t-spread-laid` because it borrows that class for the frame
   and the shadow, and `.t-spread-laid` sets `margin: 0` further down this
   file. One class each is the same specificity, so source order decided it and
   the card sat against the left edge of the page. */
.t-spread-laid.t-clarify {
  width: 5.5rem;
  margin: 0 auto 1.5rem;
}

.t-turns { max-width: 30rem; margin: 0 auto; }

.t-turn { margin: 0 0 1.6rem; }

/* What the cards said. Set as reading text, not as a chat bubble: this is an
   interpretation that happens to be part of a conversation. */
.t-turn-cards {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--t-parchment);
}

/* The suggested questions. Quiet, and visibly not the draw control beside
   them: a chip is a question and costs nothing, and the two must not look
   alike. */
.t-chips {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  max-width: 30rem;
  margin: 0 auto 1.4rem;
}
.t-chips form { margin: 0; width: 100%; }
.t-chip {
  display: block;
  width: 100%;
  text-align: left;
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--t-parchment);
  background: rgba(0, 0, 0, 0.22);
  border: 1px solid rgba(168, 130, 60, 0.3);
  border-radius: 4px;
  padding: 0.6rem 0.8rem;
  cursor: pointer;
}
.t-chip:hover { border-color: rgba(232, 182, 97, 0.55); }

/* The draw suggestion. Visibly not a chip: a chip is a question and costs
   nothing, this is an action that spends one of the session's cards, and the
   two must not look alike. */
.t-draw {
  max-width: 30rem;
  margin: 0 auto 1.4rem;
}
.t-draw form { margin: 0 0 0.6rem; }
.t-draw-decline {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-muted);
  text-decoration: none;
  border-bottom: 1px solid rgba(168, 130, 60, 0.3);
  padding-bottom: 2px;
}

/* ---- Branch B: the cards ask what you see ----

   Phase 14, part 3. Set apart from the turns around it, because it is the one
   place in a conversation where the app stops answering and asks. Wider than a
   chip and narrower than the reading, with the card above the words: the
   invitation is about *this* picture, and a sentence with no card under it is
   a form field with a nice question on it. */
.t-session-invite {
  max-width: 30rem;
  margin: 0 auto 1.6rem;
  padding: 1.2rem 1rem 0.2rem;
  border: 1px solid rgba(168, 130, 60, 0.28);
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.18);
  text-align: center;
}
/* The request, in words. Without it the block was a card and some buttons. */
.t-invite-eyebrow {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin: 0 0 0.35rem;
}
.t-invite-question {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: 1.5rem;
  line-height: 1.25;
  color: var(--t-parchment);
  margin: 0 0 1rem;
  text-wrap: balance;
}

/* Tap to enlarge: a checkbox and its label, so it needs no script. Bigger than
   a thumbnail at rest, and the full width of the conversation when open. */
.t-invite-zoom {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}
.t-invite-card-wrap { display: block; cursor: zoom-in; }
.t-invite-zoom:checked + .t-invite-card-wrap { cursor: zoom-out; }
.t-invite-zoom:focus-visible + .t-invite-card-wrap .t-invite-card img {
  outline: 2px solid var(--t-candle);
  outline-offset: 3px;
}
.t-invite-card {
  width: 11rem;
  max-width: 100%;
  margin: 0 auto 0.4rem;
  transition: width 280ms ease;
}
.t-invite-zoom:checked + .t-invite-card-wrap .t-invite-card { width: 100%; }
.t-invite-enlarge {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  color: var(--t-muted);
  margin: 0 0 1rem;
}
.t-invite-zoom:checked ~ .t-invite-enlarge { visibility: hidden; }
.t-invite-hint {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--t-muted);
  margin: -0.4rem 0 1rem;
}
.t-invite-sub {
  display: block;
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--t-brass);
  text-align: left;
  margin: 0 0 0.45rem;
}
@media (prefers-reduced-motion: reduce) {
  .t-invite-card { transition: none; }
}
.t-invite-card img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 2px;
  box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.55);
}
/* Same convention as everywhere else: a reversed card is drawn upside down,
   because that is what reversed means. */
.t-invite-card.t-card-reversed img { transform: rotate(180deg); }
.t-invite-label {
  font-family: var(--t-ui);
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  line-height: 1.35;
  color: var(--t-muted);
  margin-top: 0.4rem;
}
/* The quick answers. Chips, because that is what this application already
   calls a one-tap answer — the same control as a suggested question, doing the
   same job in the other direction. Left-aligned like the form under them: they
   are sentences to read, not buttons to aim at. */
.t-invite-options {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
  margin: 0 0 1rem;
  text-align: left;
}
.t-invite-options form { margin: 0; }
/* Answers, not questions. The suggested questions under a reply are chips
   too, and the two sat one above the other looking identical. These are set
   as things the reader might say: reading type, italic, a rule down the left. */
.t-invite-options .t-chip {
  font-family: var(--t-body);
  font-style: italic;
  font-size: 1.0625rem;
  border-left: 2px solid rgba(232, 182, 97, 0.55);
}
.t-invite-options .t-chip.t-chip-quiet {
  font-family: var(--t-ui);
  font-style: normal;
  font-size: 0.9375rem;
  border-left-color: rgba(168, 130, 60, 0.18);
}

/* The button. It had no base style at all, so every `.t-btn` on the session
   page — "Say what you see", "Ask something else", the recommended action —
   was the browser's own grey button. `.t-btn-quiet` and the locked state build
   on this. */
.t-btn {
  display: inline-block;
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--t-ink);
  background: var(--t-candle);
  border: 1px solid var(--t-candle);
  border-radius: 4px;
  padding: 0.7rem 1.2rem;
  cursor: pointer;
  text-decoration: none;
}
.t-btn:hover { background: #f0c477; border-color: #f0c477; }
.t-btn:focus-visible { outline: 2px solid var(--t-candle); outline-offset: 3px; }
.t-btn.t-btn-quiet:hover { background: rgba(0, 0, 0, 0.2); border-color: rgba(232, 182, 97, 0.55); }

/* The edition's neutral answer, always last. Quieter than the guesses above
   it, for the reason `.t-draw-decline` is quieter than the draw button: it has
   to be easy to take and must not look like the recommended one. Still a chip
   and still a real answer — it is stored and read like any other, so it is not
   dressed as a dismissal either. */
.t-chip-quiet {
  color: var(--t-muted);
  background: transparent;
  border-color: rgba(168, 130, 60, 0.18);
}
.t-chip-quiet:hover { border-color: rgba(168, 130, 60, 0.4); }

/* The form inside it is left-aligned even though the card above is centred: a
   textarea somebody is about to type a paragraph into should not be. */
.t-invite-form { text-align: left; }

.t-say { max-width: 30rem; margin: 0 auto 2rem; }

/* ---- a turn arriving, with the bundle ----

   Everything below applies only while session.js is streaming a reply. With no
   bundle a turn is a form POST and a whole new page, and none of this is ever
   drawn. */

/* The cards are answering. A caret at the end of the text rather than a
   spinner somewhere else on the page: the reply IS the progress, and a
   separate indicator would be a second thing claiming to say how it is going.

   `currentColor` and no animation until it is asked for — see the
   reduced-motion rule below. */
.t-turn-writing::after {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 1.05em;
  margin-left: 0.15em;
  vertical-align: -0.18em;
  background: currentColor;
  opacity: 0.55;
  animation: t-turn-caret 1.1s steps(2, start) infinite;
}
@keyframes t-turn-caret {
  0%, 49% { opacity: 0.55; }
  50%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .t-turn-writing::after { animation: none; }
}

/* The composer while a reply is arriving. Dimmed rather than removed: taking
   it off the page would move everything under the reader's thumb at the exact
   moment they are reading. */
.t-session-busy .t-say { opacity: 0.5; }
.t-session-busy .t-say textarea { cursor: default; }

/* A failed turn, said beside the thing that failed. Their words were stored
   before the model was called, so this is never a "you have lost what you
   typed" message. */
.t-session-error {
  max-width: 30rem;
  margin: -1rem auto 2rem;
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--t-muted);
}

/* The end. The only place the limit is ever mentioned, and it arrives under a
   finished answer — which is what makes an unwarned ending acceptable. */
/* The app offer, under the account offer on a wall and at the end of a
   session. Quiet and inline: it is the second-best door for somebody already
   reading here, and a button would make it compete with the account. */
.t-deep-app,
.t-session-app {
  margin: 1rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--t-muted);
}
.t-deep-app-link,
.t-session-app-link {
  margin-left: 0.6rem;
  color: var(--t-candle);
  text-decoration: none;
  border-bottom: 1px solid rgba(232, 182, 97, 0.4);
  white-space: nowrap;
}

.t-session-end { margin-top: 2rem; }

/* ---- The session protocol (Phase 15) ----

   docs/tarot_worlds_session_llm_protocol.md. Next steps under the short
   answer, typed actions inside a session, and the access layer that locks
   them. **Locked steps stay readable** (§37.5): they look unavailable because
   of access, not unimportant, so the text keeps its colour and only the
   control loses its pointer and gains a lock. */
.t-next {
  max-width: 30rem;
  margin: 0 auto 2rem;
  text-align: left;
}
.t-next-title {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--t-muted);
  margin: 0 0 0.6rem;
}
.t-next .t-chips { margin-bottom: 1rem; }
.t-next-ask { margin: 0 0 1.2rem; }

.t-locked .t-chip[disabled],
.t-locked .t-btn[disabled],
.t-locked-btn[disabled] {
  cursor: not-allowed;
  opacity: 0.78;
}
.t-locked .t-chip[disabled]::before,
.t-locked .t-btn[disabled]::before,
.t-locked-btn[disabled]::before {
  content: "\1F512";
  font-size: 0.8em;
  margin-right: 0.45em;
}
.t-locked textarea[disabled] { cursor: not-allowed; opacity: 0.7; }

/* Why the steps are locked, and one door. */
.t-access {
  max-width: 30rem;
  margin: 1.2rem auto 2rem;
  padding: 1rem 1rem 1.1rem;
  border: 1px solid rgba(232, 182, 97, 0.35);
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.28);
  text-align: left;
}
.t-access-msg {
  font-family: var(--t-body);
  font-size: 1.0625rem;
  line-height: 1.5;
  color: var(--t-parchment);
  margin: 0 0 0.4rem;
}
.t-access-note {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--t-muted);
  margin: 0 0 0.9rem;
}
.t-access form { margin: 0; }

/* The actions the cards proposed. An action is not a chip: a chip is a
   question, an action does something — draws, interprets, summarises — so the
   two must not look alike. Only the recommended one is emphasised (§32). */
.t-actions {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  max-width: 30rem;
  margin: 0 auto 1.4rem;
}
.t-action { margin: 0; }
.t-action-hint {
  display: block;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--t-muted);
  margin-top: 0.35rem;
}
.t-btn.t-btn-quiet {
  background: transparent;
  color: var(--t-parchment);
  border: 1px solid rgba(168, 130, 60, 0.4);
}

.t-session-focus {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-muted);
  margin: -0.4rem 0 1.2rem;
}
.t-session-focus span { color: var(--t-candle); }

.t-session-draw { margin: 0 0 1.4rem; }
.t-session-draw-head {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--t-candle);
  margin: 1.6rem 0 0.6rem;
}
.t-spread-answer { max-width: 30rem; margin-left: auto; margin-right: auto; }

.t-turn-interpretation,
.t-turn-summary {
  border-left: 2px solid rgba(168, 130, 60, 0.35);
  padding-left: 0.9rem;
}
.t-session-finished { max-width: 30rem; margin: 0 auto 1.6rem; }
.t-session-door { margin: 0.9rem 0 0; }
.t-session-door form { margin: 0; }
/* A door that is a link takes the button's look, and a link wraps like
   text: inline, its box broke apart line by line on a phone. A block of its
   own, centred, the way a <button> is. */
.t-session-door a.t-ask-submit,
.t-session-door button.t-ask-submit {
  display: inline-block;
  max-width: 100%;
  box-sizing: border-box;
  text-align: center;
  text-decoration: none;
  line-height: 1.3;
}
/* The quiet way out ("That is enough for now") — never the browser's
   visited purple. */
.t-session-door > a:not(.t-ask-submit),
.t-session-door > a:not(.t-ask-submit):visited { color: var(--t-candle); }

.t-explore { margin: 0 0 2rem; }
.t-explore a {
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  letter-spacing: 0.04em;
  color: var(--t-candle);
  border-bottom: 1px solid rgba(232, 182, 97, 0.4);
  text-decoration: none;
  padding-bottom: 2px;
}

.t-not-saved {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--t-muted);
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid rgba(168, 130, 60, 0.22);
  border-radius: 3px;
  padding: 0.7rem 0.9rem;
  margin: 0 0 1.75rem;
}
.t-not-saved a { color: var(--t-candle); }

.t-reveal-again { margin: 0 0 1rem; }
.t-reveal-again a { display: inline-block; text-decoration: none; }

@media (prefers-reduced-motion: reduce) {
  .t-card {
    box-shadow:
      0 0 0 1px rgba(168, 130, 60, 0.4),
      0 0.2rem 0.5rem rgba(0, 0, 0, 0.55),
      0 1rem 2rem -0.4rem rgba(0, 0, 0, 0.5);
  }
}

/* The reversed badge.
 *
 * On the card rather than beside the title, because the thing it explains is
 * the picture. Rotating a reversed card is correct — that is what reversed
 * means — but to anyone who does not already know the convention it reads as a
 * broken image, and a caption elsewhere on the page arrives too late to change
 * that first impression. */
.t-card { position: relative; }

.t-card-flag {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  font-family: var(--t-ui);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--t-table);
  background: var(--t-candle);
  padding: 0.4rem 0.5rem;
  text-align: center;
  line-height: 1.25;
}
.t-card-flag-mark { font-size: 0.9rem; line-height: 1; }

/* The badge is not rotated with the picture — it is the one thing on the card
   that has to stay the right way up. */
.t-card-reversed .t-card-flag { transform: none; }

.t-card-rev-suffix {
  font-size: 0.5em;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--t-brass);
  vertical-align: middle;
  white-space: nowrap;
}

/* =========================================================================
   Forms, the flash message and the account pages.

   These screens came over from cairnquest.co.uk as Bootstrap 5 markup driven
   by jQuery, and this site loads neither — see _outer/sections/assets_head.htm,
   where the whole point is that a page of text costs no framework. So the
   markup was rewritten against this block instead, and nothing below depends
   on a class it does not define itself.

   The rule that follows from that: **a form here must work with scripts off.**
   Every one of them is a plain POST the server answers with a redirect and a
   flash message. Script adds the strength meter, the anti-spam answer and the
   inline field errors — it is never what makes a form submit.
   ========================================================================= */

/* ---- the flash message ---- */
/* Set by beforePresenting() and read once. `is-error` is the only variant:
   a message is either something that went wrong or something that went right,
   and four severities of parchment box is three more than this site needs. */

.t-message {
  max-width: var(--t-measure);
  margin: 0 0 2rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--t-brass);
  border-left-width: 3px;
  background: rgba(168, 130, 60, 0.12);
  font-family: var(--t-ui);
  font-size: 0.9375rem;
}
.t-message.is-error {
  border-color: #b4584f;
  background: rgba(180, 88, 79, 0.14);
}
.t-message a { color: var(--t-candle); }

/* The same message on the reading shell, which has no content column to put
   it at the top of. Fixed to the top of the window: the ask screen and the
   reveal are full-height and vertically centred, so a message stacked above
   them sits off the fold — and a thank-you nobody scrolls to is the same as
   no thank-you at all. Opaque, because it is read over a photograph. */
.t-flash {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  justify-content: center;
  padding: max(1rem, env(safe-area-inset-top, 0px)) 1.25rem 1rem;
  /* The room and the ask screen keep working under it — only the message
     itself takes the pointer. */
  pointer-events: none;
}
.t-flash { transition: opacity 0.35s ease, transform 0.35s ease; }
.t-flash.is-gone { opacity: 0; transform: translateY(-0.5rem); }
.t-flash .t-message {
  margin: 0;
  max-width: 32rem;
  pointer-events: auto;
  background:
    linear-gradient(rgba(168, 130, 60, 0.12), rgba(168, 130, 60, 0.12)),
    var(--t-table);
  box-shadow: 0 0.6rem 1.6rem rgba(0, 0, 0, 0.45);
}
.t-flash .t-message.is-error {
  background:
    linear-gradient(rgba(180, 88, 79, 0.14), rgba(180, 88, 79, 0.14)),
    var(--t-table);
}

/* ---- the auth pages: sign in, register, reset, activate ---- */

.t-auth {
  max-width: 27rem;
  margin: 1rem auto 0;
}
.t-auth-hero { text-align: center; margin-bottom: 1.75rem; }
.t-auth-hero h1 {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.7rem, 4.5vw, 2.15rem);
  line-height: 1.15;
  margin: 0 0 0.5rem;
}
.t-auth-hero p {
  margin: 0;
  color: var(--t-muted);
  font-family: var(--t-ui);
  font-size: 0.9375rem;
}

/* The panel. A hairline of brass on a slightly lifted ground — the same
   language as .t-journal-entry, so the account pages and the journal read as
   one site rather than two. */
.t-panel {
  border: 1px solid rgba(168, 130, 60, 0.3);
  background: rgba(0, 0, 0, 0.22);
  padding: 1.75rem;
}
.t-panel + .t-panel { margin-top: 1.5rem; }
.t-panel-title {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: 1.15rem;
  color: var(--t-candle);
  margin: 0 0 1rem;
}
.t-panel-danger { border-color: rgba(180, 88, 79, 0.55); }
.t-panel-danger .t-panel-title { color: #d98a80; }

/* ---- fields ---- */
/* Label above input, not Bootstrap's floating label. A floating label is a
   placeholder until it is focused, and this site's inputs sit on a dark ground
   where placeholder-grey is already close to unreadable. */

.t-field { margin-bottom: 1.1rem; }
.t-field label,
.t-label {
  display: block;
  margin-bottom: 0.35rem;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--t-brass);
}
.t-input {
  display: block;
  width: 100%;
  padding: 0.7rem 0.85rem;
  color: var(--t-parchment);
  background: rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(168, 130, 60, 0.35);
  border-radius: 2px;
  font-family: var(--t-body);
  font-size: 1rem;
  line-height: 1.4;
}
.t-input::placeholder { color: #6f6555; }
.t-input:focus {
  outline: none;
  border-color: var(--t-candle);
  box-shadow: 0 0 0 3px rgba(232, 182, 97, 0.18);
}
.t-input:disabled { opacity: 0.6; }
/* The browser's own autofill paints its own background, which on a dark input
   is a white slab with white text in it. An inset shadow the size of the field
   is the only way to override it that Chrome and Safari both honour. */
.t-input:-webkit-autofill,
.t-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--t-parchment);
  -webkit-box-shadow: 0 0 0 100px #241d16 inset;
  caret-color: var(--t-parchment);
}
.t-input[type="file"] { padding: 0.55rem 0.6rem; font-size: 0.9375rem; }

/* Field-level errors. Hidden until the field is marked, so the page does not
   open with a column of red text under empty inputs. */
.t-field-error {
  display: none;
  margin-top: 0.4rem;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: #e39a91;
}
.t-input.is-invalid { border-color: #b4584f; }
.t-input.is-invalid ~ .t-field-error { display: block; }

.t-field-note,
.t-note {
  margin: 0.45rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-muted);
}
.t-note a { color: var(--t-candle); }

.t-field-row { display: flex; gap: 1rem; }
.t-field-row > * { flex: 1 1 0; min-width: 0; }
@media (max-width: 30rem) {
  .t-field-row { display: block; }
}

/* ---- checkboxes ---- */

.t-check {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: #d4c9b4;
}
.t-check input[type="checkbox"] {
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  margin: 0.15rem 0 0;
  accent-color: var(--t-candle);
}
/* A checkbox's label is a sentence, not a field name — so it undoes the
   uppercase brass caption that .t-field label sets for the inputs above it. */
.t-check label {
  margin: 0;
  font-size: inherit;
  letter-spacing: normal;
  text-transform: none;
  color: inherit;
}
.t-check a { color: var(--t-candle); }
.t-check .t-field-error { margin-top: 0.2rem; }
.t-check input.is-invalid ~ label { color: #e39a91; }

/* ---- buttons ---- */

.t-submit {
  display: block;
  width: 100%;
  padding: 0.8rem 1rem;
  color: var(--t-table);
  background: var(--t-candle);
  border: 1px solid var(--t-candle);
  border-radius: 2px;
  font-family: var(--t-ui);
  font-size: 1rem;
  letter-spacing: 0.02em;
  cursor: pointer;
}
.t-submit:hover { background: #f0c477; border-color: #f0c477; }
.t-submit[disabled] { opacity: 0.55; cursor: default; }

.t-submit-quiet {
  color: var(--t-parchment);
  background: transparent;
  border-color: rgba(168, 130, 60, 0.5);
}
.t-submit-quiet:hover { background: rgba(168, 130, 60, 0.15); border-color: var(--t-candle); }

.t-submit-danger {
  color: #e39a91;
  background: transparent;
  border-color: rgba(180, 88, 79, 0.6);
}
.t-submit-danger:hover { color: #fff; background: #8c3b34; border-color: #8c3b34; }

.t-submit-inline { display: inline-block; width: auto; }

/* A button that is really a link: "remove my picture", "revoke". */
.t-linkbutton {
  padding: 0;
  color: var(--t-muted);
  background: none;
  border: 0;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  text-decoration: underline;
  cursor: pointer;
}
.t-linkbutton:hover { color: var(--t-parchment); }

/* ---- the row between the form and the social buttons ---- */

.t-auth-or {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  margin: 1.5rem 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--t-muted);
}
.t-auth-or::before,
.t-auth-or::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: rgba(168, 130, 60, 0.3);
}

.t-social { display: grid; gap: 0.6rem; }
.t-social-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.7rem 1rem;
  border: 1px solid rgba(168, 130, 60, 0.4);
  border-radius: 2px;
  color: var(--t-parchment);
  background: rgba(0, 0, 0, 0.25);
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  text-decoration: none;
}
.t-social-btn:hover { border-color: var(--t-candle); background: rgba(232, 182, 97, 0.1); }
.t-social-btn svg { width: 1.15rem; height: 1.15rem; flex: 0 0 auto; }

.t-auth-foot {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(168, 130, 60, 0.2);
  text-align: center;
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  color: var(--t-muted);
}
.t-auth-foot a { color: var(--t-candle); }

/* The forgot-password link, level with the "remember me" box. */
.t-auth-aside {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}
.t-auth-aside a {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-candle);
}

/* ---- the password strength meter ---- */
/* Its "too weak" band is exactly what App\Handlers\User\Login::isPasswordTooWeak
   refuses, so the meter never says fair about a password the server will send
   back. Hidden until something is typed. */

.t-pw {
  margin: -0.4rem 0 1.1rem;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
}
.t-pw[hidden] { display: none; }
.t-pw-track { display: flex; gap: 0.3rem; margin-bottom: 0.3rem; }
.t-pw-track span {
  flex: 1 1 0;
  height: 3px;
  background: rgba(168, 130, 60, 0.22);
}
.t-pw-label { color: var(--t-muted); }

.t-pw[data-score="0"] .t-pw-track span:nth-child(1) { background: #b4584f; }
.t-pw[data-score="0"] .t-pw-label { color: #e39a91; }
.t-pw[data-score="1"] .t-pw-track span:nth-child(-n+1) { background: #c98a3c; }
.t-pw[data-score="2"] .t-pw-track span:nth-child(-n+2) { background: var(--t-candle); }
.t-pw[data-score="3"] .t-pw-track span { background: #7fa86a; }
.t-pw[data-score="2"] .t-pw-label,
.t-pw[data-score="3"] .t-pw-label { color: var(--t-parchment); }

/* ---- the account pages ---- */

.t-account { max-width: 48rem; }
.t-account h1 {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.8rem, 5vw, 2.4rem);
  margin: 0 0 1.75rem;
}
.t-account-hero {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
}
.t-account-who { min-width: 0; }
.t-account-name {
  font-family: var(--t-display);
  font-size: 1.4rem;
  margin: 0;
  word-break: break-word;
}
.t-account-email {
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  color: var(--t-muted);
  word-break: break-word;
}
.t-badges { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
.t-badge {
  padding: 0.15rem 0.55rem;
  border: 1px solid rgba(168, 130, 60, 0.45);
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--t-brass);
}

.t-rows { margin: 0; }
.t-row {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.7rem 0;
  border-bottom: 1px solid rgba(168, 130, 60, 0.18);
  font-family: var(--t-ui);
  font-size: 0.9375rem;
}
.t-row:last-child { border-bottom: 0; }
.t-row-label {
  flex: 0 0 7rem;
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--t-muted);
}
.t-row-value { flex: 1 1 12rem; min-width: 0; word-break: break-word; }

/* A change form is a <details>, not a modal. A modal needs a framework to open
   it; a disclosure opens itself, and one that fails to open still shows the
   form underneath instead of nothing at all. */
.t-disclose { margin-top: 0.9rem; }
.t-disclose > summary {
  display: inline-block;
  cursor: pointer;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-candle);
  list-style: none;
}
.t-disclose > summary::-webkit-details-marker { display: none; }
.t-disclose > summary::before { content: "+ "; }
.t-disclose[open] > summary::before { content: "\2212 "; }
.t-disclose-body {
  margin-top: 0.9rem;
  padding: 1.1rem;
  border: 1px solid rgba(168, 130, 60, 0.25);
  background: rgba(0, 0, 0, 0.2);
}

/* ---- the API token table ---- */

.t-table-scroll { overflow-x: auto; }
.t-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--t-ui);
  font-size: 0.875rem;
}
.t-table th,
.t-table td {
  padding: 0.6rem 0.75rem 0.6rem 0;
  text-align: left;
  border-bottom: 1px solid rgba(168, 130, 60, 0.18);
  white-space: nowrap;
}
.t-table th {
  font-weight: 400;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--t-muted);
}
.t-table code {
  font-size: 0.8125rem;
  color: var(--t-candle);
}
.t-panel-center { text-align: center; }

/* ---- contact ---- */

.t-contact {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: 2.5rem;
  align-items: start;
  max-width: 58rem;
}
.t-contact-aside {
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  color: var(--t-muted);
}
.t-contact-aside strong { color: var(--t-parchment); }
@media (max-width: 44rem) {
  .t-contact { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; }
}

/* The message box. Same input, only taller and free to be resized upwards. */
textarea.t-input { resize: vertical; min-height: 9rem; line-height: 1.6; }

/* The wall on the deep reading — an offer, not an error.
   Drawn in the reveal by DeepReading.jsx when this visitor has no free reading
   left and nothing in the balance. It sits where the "Explore this reading"
   button was, so the eye does not have to go looking for what changed. */
.t-deep-wall {
  max-width: 30rem;
  margin: 0 auto;
  text-align: center;
}
.t-deep-wall p {
  margin: 0 0 1.1rem;
  font-family: var(--t-ui);
  font-size: 0.9375rem;
  color: #d4c9b4;
}
.t-deep-wall .t-deep-button { text-decoration: none; display: inline-block; }

/* ---- the packs ---- */

.t-packs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1rem;
  margin: 1.75rem 0;
}
.t-pack {
  border: 1px solid rgba(168, 130, 60, 0.3);
  background: rgba(0, 0, 0, 0.22);
  padding: 1.5rem 1.25rem;
  text-align: center;
}
.t-pack-count {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin: 0 0 0.5rem;
}
.t-pack-price {
  font-family: var(--t-display);
  font-size: 2rem;
  line-height: 1;
  margin: 0 0 0.35rem;
}
.t-pack-each {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-muted);
  margin: 0 0 1.25rem;
}
.t-submit-link {
  display: block;
  text-align: center;
  text-decoration: none;
}
/* The "you can add more" aside on the allowance line. Quieter than the number
   it follows: it is reassurance, not an offer, and the ask screen is not a
   place to sell to somebody who has not yet had a reading. */
.t-ask-free-more { opacity: 0.75; }

/* =========================================================================
   Spreads: more than one card for one question.

   Three things are drawn from ONE description of a layout — the little
   diagram in the picker, the table on the reveal, and (through
   SpreadDiagram.jsx and SpreadReveal.jsx) the same two inside the React
   flow. The description is App\Constants::SPREADS and it arrives here as
   four custom properties and nothing else:

     --cols / --rows   on the container, the grid in card-widths and heights
     --c    / --r      on each cell, zero-based, where that card sits

   So there is no SVG, no image and no per-layout rule anywhere below. A new
   layout is an entry in a PHP array; a seven-card horseshoe and a ten-card
   Celtic Cross come out of the same eight declarations. `--t-card-aspect` is
   the deck's own proportion, and it is what keeps a cell card-shaped rather
   than square at every size.
   ========================================================================= */

:root {
  /* 2.75in x 4.75in, the physical proportions of a tarot card — the same
     number as `deck.aspect` in the Zygote. Here as well because CSS cannot
     read the Zygote, and a grid of squares does not read as cards. */
  --t-card-aspect: 0.5789;
}

/* ---- the second action on the ask screen ----

   A text button, not a second slab of candlelight. The free single card is
   still what the product is, and the README's rule for this screen is that it
   holds almost nothing except the question — so this reads as the second thing
   on the page because it *is* the second thing. */
.t-ask-more {
  margin: 1rem 0 0;
  text-align: center;
}
.t-ask-more-button {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  letter-spacing: 0.04em;
  color: var(--t-candle);
  background: transparent;
  border: 0;
  border-bottom: 1px solid rgba(168, 130, 60, 0.45);
  border-radius: 0;
  padding: 0.35rem 0.15rem;
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}
.t-ask-more-button:hover {
  color: #f2c87d;
  border-bottom-color: var(--t-candle);
}

/* ---- the picker ---- */

.t-spreads-inner {
  /* Wider than the ask screen's column: this page is a list of things to
     compare, and comparing is the whole task. */
  max-width: 44rem;
}
.t-spreads-title {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.8rem, 6vw, 2.6rem);
  line-height: 1.1;
  margin: 0 0 0.5rem;
  color: var(--t-parchment);
}
.t-spreads-form { margin: 0; }
.t-spreads-wall { margin-bottom: 1.75rem; }

.t-spread-list {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  gap: 1rem;
}

.t-spread {
  display: grid;
  /* The diagram, then everything else, then the button — which drops under
     both on a narrow screen rather than squeezing the text into a column. */
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 1rem 1.25rem;
  text-align: left;
  padding: 1.1rem 1.25rem;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(168, 130, 60, 0.28);
  border-radius: 3px;
}
.t-spread:focus-within,
.t-spread:hover { border-color: rgba(168, 130, 60, 0.55); }

/* Not finished yet. Dimmed rather than hidden — it says where this is going —
   and with no hover response at all, so nothing about it invites a press. */
.t-spread-soon { opacity: 0.55; }
.t-spread-soon:hover { border-color: rgba(168, 130, 60, 0.28); }

/* A grid item defaults to min-width:auto, which refuses to shrink below its
   content's intrinsic width — and the position chips inside bring a long one.
   Without this the whole picker was wider than a 390px phone and the page
   scrolled sideways. Same fix, and the same reason, as `.t-ask-inner`. */
.t-spread-body { display: block; min-width: 0; }

.t-spread-name {
  display: block;
  font-family: var(--t-display);
  font-size: 1.25rem;
  color: var(--t-parchment);
}
.t-spread-count {
  display: block;
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin-top: 0.15rem;
}
.t-spread-text {
  display: block;
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--t-muted);
  margin-top: 0.5rem;
}

/* The positions, named. This is the difference between one three-card layout
   and another, so it is on the card and not behind it. */
.t-spread-positions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.45rem;
  margin-top: 0.7rem;
}
.t-spread-positions span {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--t-muted);
  border: 1px solid rgba(168, 130, 60, 0.22);
  border-radius: 2px;
  padding: 0.15rem 0.45rem;
}

.t-spread-choose {
  grid-column: 1 / -1;
  justify-self: start;
}
.t-spread-soon-flag {
  grid-column: 1 / -1;
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--t-brass);
}

.t-spreads-back {
  margin: 1.75rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
}
.t-spreads-back a { color: var(--t-candle); }
.t-spreads-back a:visited { color: var(--t-candle); }

/* ---- the diagram, and the table ----

   Both are CSS Grid, sized `repeat(var(--cols), ...)`, with every card placed
   by `grid-column: calc(var(--c) + 1)`. Grid rather than absolute positioning,
   and that is not a style preference — it is a bug that was here:

   the table used to compute its own height from `--unit`, and `--unit` held a
   `min(..., 100%)`. A custom property is substituted as *tokens*, so that
   `100%` resolved against the containing block's **height** the moment it
   appeared inside `height: calc(...)` — the table collapsed to nothing and the
   card list underneath was drawn straight through it. A grid row sizes itself
   from what is in it and there is no height to compute, which is why nothing
   below states one.

   The crossing card is the one place two cards share a cell. In grid they
   simply stack, which is exactly what is wanted, so it needs a z-index and a
   quarter turn and no geometry of its own. */
.t-spread-figure {
  --unit: 0.85rem;
  --gap: 0.18rem;
  display: grid;
  grid-template-columns: repeat(var(--cols), var(--unit));
  grid-template-rows: repeat(var(--rows), calc(var(--unit) / var(--t-card-aspect)));
  gap: var(--gap);
  flex: none;
}
.t-spread-cell {
  grid-column: calc(var(--c) + 1);
  grid-row: calc(var(--r) + 1);
  border: 1px solid rgba(168, 130, 60, 0.6);
  border-radius: 1px;
  background: rgba(232, 182, 97, 0.10);
}
/* The one card in tarot that lies across another. It shares its cell with the
   card underneath, so it is turned a quarter rather than given a cell of its
   own. */
.t-spread-cell-cross {
  transform: rotate(90deg);
  background: rgba(232, 182, 97, 0.22);
}

/* The reveal's table: the layout as it was dealt.

   `max-width` is what keeps a three-card line from growing to the size of a
   poster on a desktop, and `1fr` columns are what let ten of them shrink onto
   a phone. The rows take their height from the cards and their captions. */
.t-spread-table {
  --gap: clamp(0.25rem, 1.5vw, 0.6rem);
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: var(--gap);
  align-items: start;
  width: 100%;
  max-width: calc(var(--cols) * 5.5rem + (var(--cols) - 1) * 0.6rem);
  margin: 1.5rem auto 2rem;
}
.t-spread-laid {
  grid-column: calc(var(--c) + 1);
  grid-row: calc(var(--r) + 1);
  margin: 0;
}
.t-spread-laid img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 2px;
  box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.55);
}
/* Same convention as the single card: a reversed card is drawn upside down,
   because that is what reversed means. The name in the list below carries the
   word, so nothing here has to stand in for an explanation. */
.t-spread-laid.t-card-reversed img { transform: rotate(180deg); }
/* Turned across the card it shares a cell with, and above it. `align-self`
   keeps it centred on that card rather than on the row, which is taller than
   one card wherever a caption wraps to two lines. */
.t-spread-laid-cross {
  align-self: center;
  /* Scaled so the *rotated* card fits the column it is crossing. A card is
     about 1.7 times as tall as it is wide, so turned on its side at full size
     it reaches a third of the way into the cells either side — which on a
     phone means it sits on top of "what is passing" and "what is
     approaching". Lying across the card beneath it is the whole point of this
     position; burying its neighbours is not.

     1 / 1.7 is 0.59; 0.6 leaves it a shade proud of the column, which reads
     as deliberate rather than as a card that does not fit. */
  transform: rotate(90deg) scale(0.6);
  z-index: 2;
}

.t-spread-laid-label {
  font-family: var(--t-ui);
  font-size: 0.625rem;
  line-height: 1.3;
  letter-spacing: 0.04em;
  text-align: center;
  color: var(--t-muted);
  margin-top: 0.3rem;
}
/* The crossing card's caption would be turned on its side with it, and a
   sideways word over another card is unreadable. It is named in the list
   underneath like every other. */
.t-spread-laid-cross .t-spread-laid-label { display: none; }

.t-spread-heading {
  font-family: var(--t-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 5vw, 1.9rem);
  color: var(--t-parchment);
  margin: 2.5rem 0 0;
}

.t-spread-cards {
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  display: grid;
  gap: 1.25rem;
  text-align: left;
}
.t-spread-card {
  padding: 1rem 0 0;
  border-top: 1px solid rgba(168, 130, 60, 0.22);
}
.t-spread-card-place {
  font-family: var(--t-ui);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin: 0;
}
.t-spread-card-name {
  font-family: var(--t-display);
  font-size: 1.1875rem;
  color: var(--t-parchment);
  margin: 0.2rem 0 0.4rem;
}
.t-spread-card .t-card-meaning { margin: 0; text-align: left; }
.t-spread-card .t-card-keywords { justify-content: flex-start; }

/* The sheet, when React draws the picker over the ask screen rather than
   sending the reader to /spreads. Same list, same cards, same diagram — this
   is only the container. */
.t-sheet {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(0, 0, 0, 0.72);
  padding: 0;
}
.t-sheet-panel {
  width: 100%;
  max-width: 44rem;
  max-height: 92vh;
  max-height: 92dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--t-table);
  border-top: 1px solid rgba(168, 130, 60, 0.35);
  border-radius: 6px 6px 0 0;
  padding: 1.5rem 1.25rem 2rem;
  text-align: center;
}
.t-sheet-close {
  position: sticky;
  top: 0;
  float: right;
  font-family: var(--t-ui);
  font-size: 1.25rem;
  line-height: 1;
  color: var(--t-muted);
  background: transparent;
  border: 0;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}
.t-sheet-close:hover { color: var(--t-parchment); }

@media (min-width: 40rem) {
  .t-sheet { align-items: center; padding: 2rem 1.25rem; }
  .t-sheet-panel { border-radius: 4px; border: 1px solid rgba(168, 130, 60, 0.35); }
}

/* ---- a layout whose positions are chosen from the question ----

   The picker draws these where a fixed layout draws its position names. They
   are sentences rather than labels, so they stack rather than sitting in a row
   of chips — five of them wrapped together read as one long list of places
   rather than as five alternatives. */
.t-spread-chosen {
  display: block;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--t-muted);
  margin-top: 0.7rem;
}
.t-spread-framings {
  display: grid;
  gap: 0.3rem;
  margin-top: 0.5rem;
}
.t-spread-framings span {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  line-height: 1.45;
  color: var(--t-muted);
  border-left: 1px solid rgba(168, 130, 60, 0.35);
  padding: 0.05rem 0 0.05rem 0.6rem;
}

/* On the reveal: how these cards were read, under the layout's name. */
.t-spread-framing {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--t-brass);
  margin: 0.35rem 0 0;
}

/* ---- one card of a spread, opened ----

   Built by front_dev/react/src/spreadcard.js, which clones the card's entry
   from the list below the layout — so everything inside `.t-cardmodal-text`
   is `.t-spread-card` markup and is styled by the rules above it. Only the
   frame is new.

   The card grows out of the thumbnail that was pressed and shrinks back into
   it (FLIP). The transform for that is written by the script onto
   `.t-cardmodal-card`, so **nothing here may set a transform on that element**
   — a transition, yes; a value, no, or the animation is overwritten mid-flight
   on the first frame. */
.t-cardmodal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem 1.25rem;
  background: rgba(0, 0, 0, 0);
  /* The scrim fades on its own while the card flies; both land together. */
  transition: background 260ms ease;
}
.t-cardmodal.is-open { background: rgba(0, 0, 0, 0.82); }

.t-cardmodal-panel {
  position: relative;
  width: 100%;
  max-width: 26rem;
  max-height: 92vh;
  max-height: 92dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--t-table);
  border: 1px solid rgba(168, 130, 60, 0.35);
  border-radius: 4px;
  padding: 1.25rem 1.25rem 1.75rem;
  text-align: center;
  /* The panel itself only fades. The card inside it is the thing that travels,
     and a panel scaling behind a scaling card reads as two animations. */
  opacity: 0;
  transition: opacity 200ms ease 60ms;
}
.t-cardmodal.is-open .t-cardmodal-panel { opacity: 1; }

.t-cardmodal-card { cursor: zoom-out; }
.t-cardmodal-card {
  width: min(60vw, 18rem);
  margin: 0.5rem auto 1.25rem;
  /* The card is drawn over the panel's own padding while it is travelling, so
     it is not clipped on its way in from a thumbnail near the edge. */
  position: relative;
  z-index: 1;
  will-change: transform;
}
.t-cardmodal-card img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 3px;
  box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.6);
}
/* Reversed stays reversed — the card is upside down because that is what was
   drawn, and the name in the text below says so in words. The FLIP transform
   is on the wrapper, so these two never fight. */
.t-cardmodal-card.t-card-reversed img { transform: rotate(180deg); }

.t-cardmodal-text { text-align: left; }
/* The clone arrives as a `.t-spread-card`, which carries a rule for the line
   above it. Inside the dialog it is the only thing there and needs no divider. */
.t-cardmodal-text .t-spread-card,
.t-cardmodal-text > * { border-top: 0; padding-top: 0; }
.t-cardmodal-text .t-card-meaning { text-align: left; }
.t-cardmodal-text .t-card-keywords { justify-content: flex-start; }

.t-cardmodal-more {
  display: inline-block;
  margin-top: 1rem;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-candle);
}
.t-cardmodal-more:visited { color: var(--t-candle); }
.t-cardmodal-more:hover { color: #f0c477; }

.t-cardmodal-close {
  position: absolute;
  top: 0.35rem;
  right: 0.5rem;
  z-index: 2;
  font-family: var(--t-ui);
  font-size: 1.5rem;
  line-height: 1;
  color: var(--t-muted);
  background: transparent;
  border: 0;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}
.t-cardmodal-close:hover { color: var(--t-parchment); }

/* The thumbnail is now a link. It must not pick up a browser's link colour or
   underline, and it must say it can be pressed. */
.t-spread-laid-link {
  display: block;
  border-radius: 2px;
  transition: transform 160ms ease, box-shadow 160ms ease;
}
.t-spread-laid-link:hover { transform: translateY(-2px); }
.t-spread-laid-link:focus-visible {
  outline: 2px solid var(--t-candle);
  outline-offset: 3px;
}
/* While one card is open its thumbnail should not also be lifting under the
   pointer behind the scrim. */
@media (prefers-reduced-motion: reduce) {
  .t-spread-laid-link:hover { transform: none; }
  .t-cardmodal, .t-cardmodal-panel, .t-cardmodal-card { transition: none; }
}

/* ---- Cards in the conversation (Phase 15) ----

   A draw made during a session sits in the conversation where it was made:
   `.t-moment`, drawn by Session/default.htm + Session/_cards.htm and, after a
   press, by session.js (shuffle, flip, reply). The offer that leads to it is
   `.t-offer`. The strip at the top jumps to each draw. */

.t-moment {
  margin: 0.4rem 0 1.8rem;
  padding: 1rem 0 0.4rem;
  border-top: 1px solid rgba(168, 130, 60, 0.28);
  border-bottom: 1px solid rgba(168, 130, 60, 0.28);
  scroll-margin-top: 4.5rem;
}
.t-session-draw { scroll-margin-top: 4.5rem; }
.t-moment-head {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin: 0 0 0.35rem;
}
.t-moment-num {
  display: inline-block;
  background: var(--t-candle);
  color: var(--t-ink);
  font-weight: 600;
  letter-spacing: 0.04em;
  border-radius: 0.6rem;
  padding: 0.18rem 0.5rem;
  margin-right: 0.55rem;
}
.t-moment-question {
  font-family: var(--t-display);
  font-style: italic;
  font-size: 1.125rem;
  line-height: 1.35;
  color: var(--t-candle);
  margin: 0 0 0.9rem;
}
.t-moment-body { display: flex; gap: 1rem; align-items: flex-start; }
.t-moment-spread .t-moment-body { display: block; }
.t-moment-spread .t-spread-table { margin: 0.5rem auto 1.2rem; }
.t-spread-laid.t-moment-card { flex: none; width: 5.5rem; margin: 0; }
.t-moment-reply { flex: 1; min-width: 0; }
.t-moment-reply:empty { display: none; }
.t-moment-reply.t-turn-writing:empty,
.t-moment-reply.t-moment-waiting { display: block; min-height: 1.5rem; }
.t-moment-entries { display: none; }
.t-moment-entry-for {
  font-family: var(--t-display);
  font-style: italic;
  color: var(--t-parchment);
  margin: 0.9rem 0 0;
}
.t-moment-entry-for span {
  display: block;
  font-family: var(--t-ui);
  font-style: normal;
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--t-brass);
  margin-bottom: 0.2rem;
}

/* The draw shuffle, over the session: the ask screen's own Shuffle component
   (front_dev/react/src/Shuffle.jsx, styled in its index.css), mounted in an
   overlay by session.js. **No `.t-shuffle*` rules here.** The first build had
   a pile of its own under those class names, and the ask screen's rules for
   the same names stretched it into nothing visible. */
.t-drawoverlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 1.5rem 1.25rem;
  /* See-through: the deck floats over the conversation rather than replacing
     it with a dark panel. */
  background: rgba(8, 5, 6, 0.45);
  opacity: 0;
  transition: opacity 200ms ease;
  cursor: pointer;
}
.t-drawoverlay.is-open { opacity: 1; }
/* The ask screen's shuffle fills the viewport on its own page and clips what
   swings outside it. Here it sits above a caption, and **nothing is clipped**:
   with `overflow: hidden` and a small box, the fanned cards were cut off. */
.t-drawoverlay .t-shuffle {
  min-height: 0;
  padding: 0;
  overflow: visible;
}
.t-drawoverlay-caption {
  max-width: 30rem;
  text-align: center;
  background: rgba(13, 11, 9, 0.78);
  border-radius: 8px;
  padding: 0.7rem 1rem;
}
.t-drawoverlay-question {
  font-family: var(--t-display);
  font-style: italic;
  font-size: 1.25rem;
  line-height: 1.35;
  color: var(--t-candle);
  margin: 0 0 0.4rem;
  text-wrap: balance;
}
.t-drawoverlay-status {
  font-family: var(--t-ui);
  font-size: 0.875rem;
  color: var(--t-muted);
  margin: 0;
}
/* Tapped before the cards came (9 Nov 2026): the line keeps breathing and
   the gathered stack too (index.css .t-shuffle-waiting), never a still screen. */
.t-drawoverlay.is-waiting .t-drawoverlay-status { animation: t-hint-breathe 2.4s ease-in-out infinite; }
.t-drawoverlay.is-waiting .t-shuffle-fan { animation: t-stack-breathe 1.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .t-drawoverlay.is-waiting .t-drawoverlay-status,
  .t-drawoverlay.is-waiting .t-shuffle-fan { animation: none; }
}

/* Laying and turning over. The transform is on `.t-flip-in`, never on the
   figure: a crossing card is already rotated there, and a reversed card's
   image is rotated inside its face. */
.t-laying .t-spread-laid-link {
  opacity: 0;
  translate: 0 -0.6rem;
  transition: opacity 240ms ease, translate 240ms ease;
}
.t-laying.is-laid .t-spread-laid-link { opacity: 1; translate: 0 0; }
.t-flip { display: block; position: relative; aspect-ratio: 0.5789; perspective: 40rem; }
.t-flip-in {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transition: transform 650ms cubic-bezier(0.3, 0.7, 0.3, 1);
}
.t-flip-face { position: absolute; inset: 0; backface-visibility: hidden; }
.t-flip-face img { height: 100%; object-fit: cover; }
.t-flip-front { transform: rotateY(180deg); }
.t-laying.is-up .t-flip-in { transform: rotateY(180deg); }
.t-laying.is-up .t-flip-front img { animation: t-card-land 1.1s ease-out; }
@keyframes t-card-land {
  0% { box-shadow: 0 0 0 1px rgba(232, 182, 97, 0.7), 0 0 2rem rgba(232, 182, 97, 0.45); }
  100% { box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.55); }
}

/* The draw offer. The only oxblood on the page: the cards are asking for
   something, and it must not read as one more button. */
.t-offer {
  display: grid;
  gap: 0.75rem;
  max-width: 30rem;
  margin: 0 auto 1.4rem;
  padding: 1rem;
  border: 1px solid rgba(139, 62, 62, 0.85);
  border-radius: 6px;
  background: linear-gradient(180deg, rgba(58, 29, 27, 0.9), rgba(58, 29, 27, 0.35));
}
.t-offer form { margin: 0; }
.t-offer .t-btn { width: 100%; }
.t-offer-head { display: flex; gap: 0.85rem; align-items: center; }
.t-offer-backs { position: relative; flex: none; width: 2.6rem; height: 3.4rem; }
.t-offer-backs img {
  position: absolute;
  top: 0;
  width: 2rem;
  border-radius: 2px;
  box-shadow: 0 0.3rem 0.8rem rgba(0, 0, 0, 0.6);
}
.t-offer-backs img:first-child { left: 0; transform: rotate(-8deg); }
.t-offer-backs img:last-child { left: 0.55rem; transform: rotate(6deg); }
.t-offer-why {
  font-family: var(--t-display);
  font-size: 1.125rem;
  line-height: 1.35;
  color: var(--t-parchment);
  margin: 0;
}
.t-offer-for {
  font-family: var(--t-display);
  font-style: italic;
  font-size: 1.0625rem;
  line-height: 1.4;
  color: var(--t-candle);
  margin: 0;
}

/* An earlier offer, in the history: what was proposed, and whether it was taken. */
.t-offered {
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--t-muted);
  margin: -0.9rem 0 1.4rem;
  padding-left: 0.9rem;
  border-left: 1px dashed rgba(139, 62, 62, 0.8);
}

/* What is left of an invitation once it has been answered — the association's
   own `.t-offered`, with the card in it. The thumbnail is the point: the note
   has to be recognisable as *that card* from two lines of type, because the
   section it is left over from showed the card at full width. */
.t-asked {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--t-muted);
  margin: -0.9rem 0 1.4rem;
  padding-left: 0.9rem;
  border-left: 1px dashed var(--t-brass);
}

.t-asked-card {
  flex: none;
  width: 34px;
  height: auto;
  border-radius: 2px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
}

/* Written out rather than left to `.t-card-reversed img`, which wants the class
   on a wrapper: here it is on the picture itself. */
img.t-asked-card.t-card-reversed { transform: rotate(180deg); }

.t-asked em {
  display: block;
  font-style: italic;
}

/* The strip: every draw so far, sticking to the top of the column. */
.t-drawstrip {
  position: sticky;
  top: 0.5rem;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  max-width: 30rem;
  margin: 0 auto 1.2rem;
  padding: 0.4rem 0.65rem;
  background: rgba(13, 11, 9, 0.9);
  border: 1px solid rgba(168, 130, 60, 0.28);
  border-radius: 6px;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.t-drawstrip a { display: block; flex: none; border-radius: 2px; }
.t-drawstrip a:focus-visible { outline: 2px solid var(--t-candle); outline-offset: 2px; }
.t-drawstrip img { display: block; width: 1.5rem; border-radius: 2px; }
.t-drawstrip img.t-rev { transform: rotate(180deg); }
.t-drawstrip span {
  font-family: var(--t-ui);
  font-size: 0.75rem;
  color: var(--t-muted);
  margin-left: 0.3rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .t-drawoverlay { transition: none; }
  .t-flip-in, .t-laying .t-spread-laid-link { transition: none; }
}

/* ---- Cards in the conversation, desktop (approved concept:
   Tarot/docs/Development/concepts/session-cards-desktop.html) ----

   A card that opens says so before it is pressed. With no bundle the link
   goes to the card's wiki page, which is also "see this card larger". */
.t-spread-laid-link { cursor: zoom-in; }

@media (min-width: 56.25rem) {
  /* The card and its text side by side. Stacked, a 288px card plus the text
     is taller than a 13" laptop and the panel scrolls. `.t-cardmodal-card`
     still carries no transform of its own — spreadcard.js writes the FLIP
     there, and measures the card where the grid puts it. */
  .t-cardmodal-panel {
    max-width: 48rem;
    display: grid;
    grid-template-columns: 17rem minmax(0, 1fr);
    gap: 1.75rem;
    align-items: start;
    text-align: left;
    padding: 1.75rem 2rem 1.75rem 1.75rem;
  }
  .t-cardmodal-card { width: 17rem; margin: 0; }
  .t-cardmodal-text { padding-top: 1rem; }
}

/* ---- The session page, sized for the cards (15 Sep) ----

   A tester's call: the question was too small to anchor the page, the strip
   too small to read, and a card in the conversation too small to see. */

/* The question, twice the size it was. */
.t-session .t-reveal-question {
  font-size: clamp(2rem, 7vw, 3.25rem);
  line-height: 1.15;
  margin-bottom: 0.6rem;
  text-wrap: balance;
}

/* The opening card and every single card drawn later: three quarters of the
   column, with the reply under the card rather than beside a thumbnail.
   Three classes, so these win over the older size rules for both. */
.t-session .t-spread-laid.t-clarify,
.t-session .t-spread-laid.t-moment-card {
  width: 75%;
  margin: 0 auto 1.1rem;
}
.t-session .t-moment-body { display: block; }

/* A layout uses the whole column, so its cards can be seen. */
.t-session .t-spread-table { max-width: 100%; }

/* The strip, doubled. A layout is up to three of its cards, each smaller and
   shifted down and right from the one before, so it reads as several cards. */
.t-drawstrip { gap: 0.9rem; padding: 0.55rem 0.8rem; }
.t-drawstrip img { width: 3rem; }
.t-drawstrip-stack {
  position: relative;
  width: 3.5rem;
  height: calc(2.6rem / 0.5789 + 1rem);
}
.t-drawstrip-stack img {
  position: absolute;
  width: 2.6rem;
  box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.6);
}
.t-drawstrip-stack img:nth-child(1) { left: 0; top: 0; }
.t-drawstrip-stack img:nth-child(2) { left: 0.45rem; top: 0.5rem; }
.t-drawstrip-stack img:nth-child(3) { left: 0.9rem; top: 1rem; }

/* The site header and footer on the reading shell ($SITE_CHROME): laid over
   the room, so each gets a dark ground of its own to stay readable. */
body.t-reading .t-header {
  position: relative;
  z-index: 6;
  background: rgba(13, 11, 9, 0.78);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
body.t-reading .t-footer {
  position: relative;
  z-index: 1;
  background: rgba(13, 11, 9, 0.82);
}

/* ---- Arriving from the shuffle (15 Sep) ----

   There is no reveal page: after the shuffle on the ask screen the browser
   goes straight to the draw's session page (landing.js), and main.jsx marks
   the page `t-dealing` for that one arrival. The opening cards turn in, one
   after another, and the short answer follows them. The animation is on the
   link, never the figure: a crossing card is rotated on its figure and a
   reversed card on its image. */
@keyframes t-deal-in {
  from { opacity: 0; transform: perspective(40rem) rotateY(95deg) translateY(-0.6rem); }
  to { opacity: 1; transform: none; }
}
@keyframes t-answer-in {
  from { opacity: 0; transform: translateY(0.4rem); }
  to { opacity: 1; transform: none; }
}
.t-dealing .t-session-draw .t-spread-laid-link { animation: t-deal-in 700ms cubic-bezier(0.3, 0.7, 0.3, 1) both; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(2) .t-spread-laid-link { animation-delay: 140ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(3) .t-spread-laid-link { animation-delay: 280ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(4) .t-spread-laid-link { animation-delay: 420ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(5) .t-spread-laid-link { animation-delay: 560ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(6) .t-spread-laid-link { animation-delay: 700ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(7) .t-spread-laid-link { animation-delay: 840ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(8) .t-spread-laid-link { animation-delay: 980ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(9) .t-spread-laid-link { animation-delay: 1120ms; }
.t-dealing .t-session-draw .t-spread-laid:nth-child(10) .t-spread-laid-link { animation-delay: 1260ms; }
.t-dealing .t-session-draw .t-verdict,
.t-dealing .t-session-draw .t-spread-answer,
.t-dealing .t-session-live { animation: t-answer-in 500ms ease-out 750ms both; }

/* The next steps before a session is started — the draw's suggested questions. */
.t-session-live .t-next-title { max-width: 30rem; margin: 0 auto 0.6rem; }

/* ---- The reader's words, easier to find (15 Sep) ---- */

/* The original question at the top: centred and white, so it reads as the
   page's title rather than a note. */
.t-session .t-reveal-question {
  text-align: center;
  color: #fbf7ef;
}

/* Every question the reader asked — the first one too — is larger and sits
   on a faint candle ground, so the conversation's two voices are told apart at
   a glance. */
.t-session .t-turn-reader .t-assoc-said {
  font-size: 1.25rem;
  line-height: 1.5;
  color: #f6f0e4;
  background: rgba(232, 182, 97, 0.08);
  border-left: 3px solid rgba(232, 182, 97, 0.6);
  border-radius: 0 6px 6px 0;
  padding: 0.75rem 1rem;
  margin: 0 0 1.2rem;
}
.t-session .t-turn-reader .t-assoc-h { margin-bottom: 0.4rem; }
.t-session .t-turn-opening { max-width: 30rem; margin: 0.6rem auto 1.2rem; text-align: left; }
.t-moment .t-turn-draw { margin: 0 0 0.9rem; }
.t-moment .t-moment-head { margin-bottom: 0.8rem; }

/* The one line above locked steps (Phase 17): why they are locked, and a
   way down to the box that explains it. */
.t-locked-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem 0.9rem;
  max-width: 30rem;
  margin: 0 auto 1rem;
  padding: 0.65rem 0.9rem;
  font-family: var(--t-ui);
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--t-parchment);
  background: rgba(232, 182, 97, 0.08);
  border: 1px solid rgba(232, 182, 97, 0.4);
  border-radius: 4px;
}
.t-locked-note[hidden] { display: none; }
.t-locked-note-text::before { content: "\1F512\FE0E"; margin-right: 0.45rem; }
.t-locked-note a { color: var(--t-candle); font-weight: 600; white-space: nowrap; }
.t-access { scroll-margin-top: 1rem; }

/* The access box's doors (Phase 17, concept "Tarot Worlds access"). */
.t-access-doors { display: grid; gap: 0.55rem; margin-top: 0.9rem; }
.t-access-doors[hidden] { display: none; }
.t-access-doors .t-ask-submit {
  display: block;
  box-sizing: border-box;
  width: 100%;
  text-align: center;
  text-decoration: none;
  line-height: 1.3;
}
.t-ask-submit.t-ask-submit-quiet {
  color: var(--t-parchment);
  background: transparent;
  border: 1px solid rgba(168, 130, 60, 0.5);
}
.t-access-packs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
.t-access-packs form { margin: 0; }
.t-access-pack {
  display: grid;
  gap: 0.15rem;
  width: 100%;
  padding: 0.65rem 0.3rem;
  font-family: var(--t-ui);
  color: var(--t-parchment);
  background: rgba(13, 11, 9, 0.5);
  border: 1px solid rgba(232, 182, 97, 0.45);
  border-radius: 4px;
  cursor: pointer;
}
.t-access-pack b { font-size: 1.25rem; color: var(--t-candle); }
.t-access-pack span { font-size: 0.8rem; color: var(--t-muted); }
.t-access-pack.is-best { border-color: var(--t-candle); background: rgba(232, 182, 97, 0.12); }
.t-access-pack:hover { border-color: var(--t-candle); }
.t-access-meta {
  margin: 0.6rem 0 0;
  font-family: var(--t-ui);
  font-size: 0.8125rem;
  color: var(--t-muted);
  text-align: center;
}
.t-access-quiet { text-align: center; }
.t-locked-note.is-free { border-color: rgba(143, 176, 122, 0.55); background: rgba(143, 176, 122, 0.08); }
.t-locked-note.is-free .t-locked-note-text::before { content: none; }
.t-locked-note.is-free a { color: #a9c896; }

/* ---- The seer: waiting for a reply (30 Oct 2026) ----

   session.js seer(): until the first word of a reply arrives, its place holds
   the ask screen's fan of cards with a wave running through it, the middle
   card turning over now and then to show the session's own card, and a
   changing line under it. The caret takes over once words are coming. */
.t-turn-writing.t-seer-on::after { display: none; }
.t-seer {
  display: grid;
  justify-items: center;
  gap: 0.9rem;
  padding: 1.4rem 0 0.6rem;
}
.t-seer-fan {
  position: relative;
  width: 15rem;
  max-width: 100%;
  height: 6.6rem;
}
.t-seer-card {
  position: absolute;
  top: 1.4rem;
  left: calc(50% - 1.3rem + (var(--i) - 3) * 2.1rem);
  width: 2.6rem;
  height: 4.5rem;
  border-radius: 3px;
  background: #2a2016 center / cover no-repeat;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(232, 182, 97, 0.28);
  transform-origin: 50% 160%;
  transform: rotate(calc((var(--i) - 3) * 9deg));
  animation: t-seer-wave 1.8s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.15s);
}
@keyframes t-seer-wave {
  0%, 60%, 100% { translate: 0 0; filter: brightness(0.85); }
  30% { translate: 0 -0.9rem; filter: brightness(1.25) drop-shadow(0 0 6px rgba(232, 182, 97, 0.7)); }
}
.t-seer-turner { background: none; perspective: 300px; }
.t-seer-side {
  position: absolute;
  inset: 0;
  border-radius: 3px;
  background: #2a2016 center / cover no-repeat;
  backface-visibility: hidden;
  animation: t-seer-flip 5.4s ease-in-out infinite;
}
.t-seer-face { transform: rotateY(180deg); animation-name: t-seer-flip-face; }
@keyframes t-seer-flip { 0%, 40%, 100% { transform: rotateY(0); } 50%, 85% { transform: rotateY(180deg); } }
@keyframes t-seer-flip-face { 0%, 40%, 100% { transform: rotateY(180deg); } 50%, 85% { transform: rotateY(360deg); } }
.t-seer-line {
  margin: 0;
  font-family: var(--t-display);
  font-style: italic;
  font-size: 1.05rem;
  color: var(--t-candle);
  text-align: center;
  transition: opacity 350ms ease;
}
.t-seer-line.is-changing { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .t-seer-card, .t-seer-side { animation: none; }
}

/* ---- The microphone, Phase 17 ----

   The language chooser beside the button (only when the interface and the
   browser speak different languages — voice.js), the button glowing with
   the reader's voice while it records, and the microphone in a session's
   composer. */
.t-voice-lang {
  position: absolute;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.15rem 0.45rem;
  font-family: var(--t-ui);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--t-candle);
  background: rgba(13, 11, 9, 0.85);
  border: 1px solid rgba(168, 130, 60, 0.45);
  border-radius: 999px;
  cursor: pointer;
}
.t-voice-lang::after { content: "▾"; font-size: 0.6rem; opacity: 0.8; }
/* The real control, invisible over the pill: a tap opens the system picker. */
.t-voice-lang select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  font-size: 16px;
  cursor: pointer;
}
.t-voice-lang:focus-within { outline: 2px solid var(--t-candle); outline-offset: 2px; }
.t-voice-lang option { color: #1a1410; }
/* Out of the way while the equalizer covers the field. */
.t-voice-covered ~ .t-voice-lang { visibility: hidden; }
/* While recording, the glow follows the voice instead of pulsing. */
.t-ask-voice.is-listening[style*="--t-voice-level"] {
  animation: none;
  box-shadow: 0 0 calc(0.3rem + var(--t-voice-level, 0) * 1.4rem) rgba(232, 182, 97, calc(0.25 + var(--t-voice-level, 0) * 0.5));
}
.t-voice-host { position: relative; }
/* The equalizer over the text field while the microphone is open (voice.js,
   equalizer()). Over the field rather than beside it, so it cannot be missed;
   the field's own text is under it and comes back when it goes. */
.t-voice-eq {
  position: absolute;
  z-index: 3;
  box-sizing: border-box;
  border: 1px solid var(--t-candle);
  border-radius: 3px;
  background: rgba(13, 11, 9, 0.82);
  box-shadow: 0 0 1.2rem rgba(232, 182, 97, 0.25), inset 0 0 1.5rem rgba(232, 182, 97, 0.08);
  cursor: pointer;
  overflow: hidden;
}
.t-voice-eq canvas { display: block; width: 100%; height: 100%; }
/* The recogniser's words, live, over the bars — which dim behind them. */
.t-voice-eq-words {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.6rem 0.9rem;
  font-family: var(--t-display);
  font-size: 1.15rem;
  line-height: 1.35;
  text-align: center;
  color: #fbf7ef;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.9);
  overflow: hidden;
}
.t-voice-eq.has-words canvas { opacity: 0.35; }
/* The field's own text and placeholder would show through the bars. */
.t-voice-covered,
.t-voice-covered::placeholder { color: transparent !important; }
.t-say-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}
.t-say .t-ask-voice-status,
.t-invite-form .t-ask-voice-status { margin-top: 0.5rem; text-align: left; }
.t-say-actions .t-ask-voice { margin-top: 0; }
.t-invite-form .t-say-actions { margin-top: 0.7rem; }
.t-invite-form .t-say-actions .t-btn { margin-top: 0; }
.t-spreads-voice { justify-content: center; margin: 0.6rem 0 0.2rem; }
.t-spreads-form .t-ask-voice-status { text-align: center; }

/* ---- The phone layout (30 Oct 2026) ----

   On a phone, and in the installed app, the site looks like the apps: no
   header, no footer, and a bar along the bottom with Ask, Journal and You
   (`_outer/sections/tabbar.htm`). One set of templates for every screen —
   the header and the footer stay in the markup for the crawler and are only
   hidden — so this block is the whole difference. Approved concept:
   docs/Development/concepts/phone-web.html.

   The breakpoint is 45rem (720px), or any touch screen when the site runs as
   the installed app: an installed tablet gets the app's layout too, a
   desktop browser window narrowed by hand still does. */
.t-tabbar { display: none; }

@media (max-width: 45rem), (display-mode: standalone) and (hover: none) {
  :root { --t-tabbar-h: calc(3.6rem + env(safe-area-inset-bottom, 0px)); }

  .t-header,
  .t-footer,
  .t-ask-foot,
  .t-reading-style-switch { display: none !important; }
  /* …except the bare header: without a world there is no tab bar either. */
  .t-header.t-header-bare { display: block !important; }

  body.t-page,
  body.t-reading { padding-bottom: var(--t-tabbar-h); }

  .t-tabbar {
    position: fixed;
    z-index: 20;
    left: 0;
    right: 0;
    bottom: 0;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    height: var(--t-tabbar-h);
    padding: 0 env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
    background: rgba(18, 14, 11, 0.97);
    border-top: 1px solid rgba(168, 130, 60, 0.28);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
  .t-tab {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: 0.2rem;
    font-family: var(--t-ui);
    font-size: 0.72rem;
    font-weight: 500;
    color: var(--t-muted);
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
  }
  /* The icons are gold line-work and are not tinted — the state is carried
     by opacity, as in the apps (MainShellScreen.SectionBar). */
  .t-tab img { width: 26px; height: 26px; opacity: 0.55; }
  .t-tab.is-here { color: var(--t-candle); }
  .t-tab.is-here img { opacity: 1; }
  .t-tab:focus-visible { outline: 2px solid var(--t-candle); outline-offset: -4px; }

  /* Things pinned to the bottom of the window sit on the bar, not under it. */
  .t-consent { bottom: var(--t-tabbar-h); }

  /* The page that used to start under the header now starts at the top of
     the phone, so it clears the notch itself. */
  body.t-page .t-main { padding-top: max(1.5rem, env(safe-area-inset-top, 0px)); }
  body.t-reading .t-session { padding-top: env(safe-area-inset-top, 0px); }

  /* The question is the session's title; the first "You" turn under it said
     it a second time. The apps show it once. Later turns keep their label. */
  .t-session .t-turn-opening { display: none; }

  /* The session's next actions span the column, as the apps' do —
     a button of its own width sat off-centre under the full-width text. */
  .t-session .t-actions { flex-direction: column; align-items: stretch; }
  .t-session .t-action .t-btn { display: block; width: 100%; text-align: center; }
  .t-session .t-action-hint { display: block; text-align: center; }
  .t-session-door a.t-ask-submit,
  .t-session-door button.t-ask-submit { display: block; width: 100%; }
  .t-session-door > a:not(.t-ask-submit) { display: block; text-align: center; padding: 0.4rem 0; }

  /* iOS zooms into any field under 16px when it gets the focus. */
  input, select, textarea { font-size: max(16px, 1em); }
}

/* The You page (`You/default.htm`), at any width — on a desktop nothing
   links to it, but it must not look broken if somebody opens it. */
.t-you { max-width: 32rem; margin: 0 auto; display: grid; gap: 0.9rem; font-family: var(--t-ui); }
.t-you-block {
  border: 1px solid rgba(168, 130, 60, 0.3);
  border-radius: 4px;
  padding: 1rem 1.1rem;
  background: rgba(26, 20, 16, 0.7);
}
.t-you-label {
  margin: 0 0 0.35rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--t-brass);
}
.t-you-big { margin: 0; font-family: var(--t-display, inherit); font-size: 2.4rem; line-height: 1; color: var(--t-candle); }
.t-you-text { margin: 0.35rem 0 0.8rem; color: var(--t-muted); font-size: 0.9rem; line-height: 1.45; }
.t-you-heading { margin: 0 0 0.2rem; font-size: 1.3rem; font-weight: 500; color: var(--t-parchment); }
.t-you-name { margin: 0 0 0.5rem; font-size: 1.15rem; color: var(--t-parchment); }
.t-you-buttons { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0; }
.t-you-list { list-style: none; margin: 0; padding: 0; }
.t-you-list li { border-bottom: 1px solid rgba(168, 130, 60, 0.18); }
.t-you-list li:last-child { border-bottom: 0; }
.t-you-list a {
  display: flex;
  justify-content: space-between;
  padding: 0.8rem 0.1rem;
  color: var(--t-parchment);
  text-decoration: none;
}
.t-you-list a::after { content: "›"; color: var(--t-muted); }
.t-you-links { border-top: 1px solid rgba(168, 130, 60, 0.18); border-bottom: 1px solid rgba(168, 130, 60, 0.18); }
.t-you-style summary { list-style: none; cursor: pointer; }
.t-you-style summary::-webkit-details-marker { display: none; }
.t-you-style-now { display: flex; align-items: center; gap: 0.7rem; color: var(--t-parchment); }
.t-you-style-now span:nth-child(2) { flex: 1; min-width: 0; }
.t-you-change { color: var(--t-candle); font-size: 0.9rem; }
.t-you-change::after { content: " ›"; }
.t-you-style[open] .t-you-change::after { content: " ⌄"; }
.t-you-styles { display: grid; gap: 0.5rem; margin-top: 0.9rem; }
.t-you-styles form { margin: 0; }
.t-you-styles button {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.65rem 0.75rem;
  text-align: left;
  font: inherit;
  color: var(--t-parchment);
  background: rgba(13, 11, 9, 0.6);
  border: 1px solid rgba(168, 130, 60, 0.25);
  border-radius: 4px;
  cursor: pointer;
}
.t-you-styles button[aria-current="true"] { border-color: var(--t-candle); }
.t-you-styles button span { display: grid; gap: 0.15rem; min-width: 0; }
.t-you-styles small { color: var(--t-muted); font-size: 0.8rem; }
.t-you-social { display: flex; justify-content: center; }
.t-you-social:empty { display: none; }
.t-you-legal { text-align: center; color: var(--t-muted); font-size: 0.8rem; }
.t-you-legal p { margin: 0.3rem 0; }

/* ---- The home-screen app (Phase 17) ----

   The strip at the very top of a phone page (`_outer/sections/install_strip.htm`)
   and the install page (`Install/default.htm`). Approved concept:
   docs/Development/concepts/install.html. Whether the strip shows at all is
   install.js's call, so it has no media query of its own. */
.t-install-strip {
  position: relative;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 2.75rem;
  padding: env(safe-area-inset-top, 0px) max(0.6rem, env(safe-area-inset-right, 0px)) 0 max(0.85rem, env(safe-area-inset-left, 0px));
  background: #241b13;
  border-bottom: 1px solid rgba(168, 130, 60, 0.3);
  font-family: var(--t-ui);
}
.t-install-strip[hidden] { display: none; }
.t-install-strip img { width: 26px; height: 26px; border-radius: 6px; flex: none; }
.t-install-strip-name { flex: 1; min-width: 0; font-size: 0.9rem; font-weight: 600; color: #fbf7ef; }
.t-install-strip-go {
  padding: 0.35rem 0.8rem;
  border-radius: 4px;
  background: var(--t-candle);
  color: #1a1208;
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
}
.t-install-strip-x {
  padding: 0.4rem 0.35rem;
  border: 0;
  background: none;
  color: var(--t-muted);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.t-install { max-width: 30rem; margin: 0 auto; font-family: var(--t-ui); }
.t-install-head { display: flex; gap: 0.9rem; align-items: center; margin-bottom: 1.3rem; }
.t-install-head img { width: 64px; height: 64px; border-radius: 14px; flex: none; }
.t-install-head h1 { margin: 0 0 0.2rem; font-size: 1.45rem; font-weight: 500; line-height: 1.15; color: var(--t-parchment); }
.t-install-head p { margin: 0; color: var(--t-muted); font-size: 0.9rem; line-height: 1.4; }
.t-install-case h2 { margin: 0 0 0.4rem; font-size: 1.25rem; font-weight: 500; color: var(--t-parchment); }
.t-install-case p { color: var(--t-parchment); line-height: 1.45; }
.t-install-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.75rem; counter-reset: tstep; }
.t-install-steps li {
  counter-increment: tstep;
  display: grid;
  grid-template-columns: 1.8rem 1fr;
  gap: 0.7rem;
  align-items: start;
  color: var(--t-parchment);
  line-height: 1.45;
}
.t-install-steps li::before {
  content: counter(tstep);
  width: 1.8rem;
  height: 1.8rem;
  border-radius: 50%;
  background: rgba(232, 182, 97, 0.15);
  color: var(--t-candle);
  font-weight: 600;
  font-size: 0.85rem;
  line-height: 1.8rem;
  text-align: center;
}
.t-install-steps li:first-child::before { background: var(--t-candle); color: #1a1208; }
.t-install-steps b { color: #fbf7ef; }
/* The iPhone's own Share glyph, drawn as it is on the bar: grey glass on
   iOS 26, plain blue on 16–18. */
.t-install-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.35rem;
  vertical-align: -0.3rem;
  border-radius: 0.4rem;
  background: rgba(120, 120, 128, 0.36);
  color: #f2f2f7;
}
.t-install-ico svg { width: 0.95rem; height: 0.95rem; }
.t-install-ico.is-blue { background: none; color: #0a84ff; }
.t-install-after { margin: 1.1rem 0 0; color: var(--t-muted) !important; font-size: 0.9rem; }
.t-install-other {
  display: inline-block;
  margin-top: 0.9rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.88rem;
  color: var(--t-candle);
  text-decoration: underline;
  cursor: pointer;
}
.t-install-wide { display: block; width: 100%; text-align: center; box-sizing: border-box; }
.t-install-url {
  padding: 0.55rem 0.7rem;
  border: 1px dashed rgba(168, 130, 60, 0.5);
  border-radius: 4px;
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.85rem;
  overflow-wrap: anywhere;
  user-select: all;
  -webkit-user-select: all;
}
.t-install-copied { color: var(--t-candle) !important; font-size: 0.9rem; }

/* "Here", pointing at the browser's own button outside the page: below the
   tab bar at the bottom, or up at the address bar. */
.t-install-arrow {
  position: fixed;
  z-index: 25;
  font-family: var(--t-ui);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--t-candle);
  text-align: center;
  pointer-events: none;
  animation: t-install-bob 1.4s ease-in-out infinite;
}
.t-install-arrow[hidden] { display: none; }
.t-install-arrow::after { content: "↓"; display: block; font-size: 1.6rem; line-height: 1; }
.t-install-arrow.is-bottom-right { right: 0.9rem; bottom: calc(var(--t-tabbar-h, 0px) + 0.4rem); }
.t-install-arrow.is-bottom-centre { left: calc(50% - 1rem); bottom: calc(var(--t-tabbar-h, 0px) + 0.4rem); }
.t-install-arrow.is-top-right { right: 0.9rem; top: calc(env(safe-area-inset-top, 0px) + 0.3rem); display: flex; flex-direction: column-reverse; }
.t-install-arrow.is-top-right::after { content: "↑"; }
@keyframes t-install-bob { 50% { translate: 0 0.35rem; } }
.t-install-arrow.is-top-right { animation-name: t-install-bob-up; }
@keyframes t-install-bob-up { 50% { translate: 0 -0.35rem; } }
@media (prefers-reduced-motion: reduce) { .t-install-arrow { animation: none; } }
