/* ═══════════════════════════════════════════════════════
   Page 100 — the service.

   Every rule here answers to one of the five disciplines in
   base.css. The two that get broken by reflex, and so get
   guarded hardest: NO ENCLOSURE (there is not a single
   border-radius or box-drawing border in this file) and ONE
   RULER (every bar is --bar-cells wide, no exceptions).
   ═══════════════════════════════════════════════════════ */

/* ── Skip-link ──────────────────────────────────────────
   Off-screen until it receives focus, then it's a plain field row
   like any other — a solid colour ground, no border — sitting above
   everything else so tabbing past it reaches the header next. */
.skip-link {
  position: absolute;
  top: -999px;
  left: 0;
  z-index: 1;
  padding: 0.5em 1ch;
  background: var(--field);
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}
.skip-link:focus {
  top: 0;
  outline: none;
}

/* ── Header row ─────────────────────────────────────────
   Teletext's top line, unchanged for forty years: the page
   number you are on, who is transmitting, and the clock. It
   is a row of the page, not furniture bolted above it, so it
   scrolls away like everything else. */
.tt-head {
  max-width: min(var(--page-cols), 100%);
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 1ch;
  padding: var(--row) var(--gutter) 0;
  margin: 0 auto;
}
.tt-head-num {
  font-family: var(--font-dh);
  color: var(--voice);
  letter-spacing: 0.08em;
}
.tt-head-name {
  color: var(--ink);
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.tt-head-clock {
  color: var(--live);
  white-space: nowrap;
  min-width: 0;
}
/* The page-number entry indicator. Hidden until the visitor starts
   typing digits, then fills in place — the signature interaction. */
.tt-entry {
  color: var(--voice);
  letter-spacing: 0.3em;
}
.tt-entry[hidden] { display: none; }
/* SABnzbd's queue, live on every page — filled in by service.js, empty
   (and so invisible) until its first poll returns. Quiet ink when
   idle; the live colour only while something is actually moving. */
.tt-head-dl {
  margin-left: auto;
  color: var(--ink-dim);
  white-space: nowrap;
  min-width: 0;
}
.tt-head-dl.live { color: var(--live); }

/* ── Page ───────────────────────────────────────────────
   One measure for every page in the service. Nothing is ever
   centred except the page itself. */
.tt-page {
  max-width: min(var(--page-cols), 100%);
  margin: 0 auto;
  padding: 0 var(--gutter) calc(var(--row) * 2);
}

/* The page masthead: double height in the service's own colour. */
.tt-masthead { color: var(--voice); margin-bottom: calc(var(--row) * 0.75); }

/* A rows block that follows another block rather than opening one. */
.tt-rows-after { margin-top: var(--row); }

/* Double-height. The only size other than body text, and the
   only place bloom is allowed. */
.tt-dh {
  font-family: var(--font-dh);
  font-size: var(--fs2);
  line-height: 1.15;
  letter-spacing: 0.02em;
  margin: 0;
}

/* ── Field row (discipline 2: grouping without enclosure) ──
   A section is announced by a full-bleed band of colour with
   the title reversed out of it. This is what replaces every
   card header and every 1px border in the old design. */
.tt-field {
  display: flex;
  align-items: baseline;
  gap: 1ch;
  margin: calc(var(--row) * 1.5) 0 var(--row);
  padding: 0 1ch;
  background: var(--field);
  color: var(--voice);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  min-height: calc(var(--row) * 1.6);
}
.tt-field-meta {
  margin-left: auto;
  color: var(--tt-white);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* A page carrying critical state gets a heavier field than a quiet
   one (discipline 5) — the band goes red and doubles in height. */
.tt-field.crit {
  background: var(--alert);
  /* Black, not white: white-on-red measures 4.0:1 here, under the
     4.5:1 floor for 15px uppercase text. Black-on-red clears 5.25:1
     and is the correct teletext reversal — every other coloured band
     (.warn, a hover row) already reverses to black. */
  color: var(--tt-black);
}
.tt-field.crit .tt-field-meta { color: var(--tt-black); }
.tt-field.warn { background: var(--voice); color: var(--tt-black); }
.tt-field.warn .tt-field-meta { color: var(--tt-black); }

/* ── Mosaic separator ───────────────────────────────────
   Teletext's block graphics doing the job a rule would do in a
   world that allowed rules. It is content in the grid, not a
   border on a box. */
.tt-sep {
  height: 0.4em;
  margin: var(--row) 0;
  background-image: repeating-linear-gradient(
    90deg,
    var(--ink-dimmer) 0 0.6em,
    transparent 0.6em 0.9em
  );
}

/* ── Index / list rows ──────────────────────────────────
   The index page IS the status page: every destination row
   carries its own live figure in its own colour, so reading the
   menu and reading the state are the same act. */
.tt-rows { display: flex; flex-direction: column; }
.tt-row {
  display: flex;
  align-items: center;
  gap: 1.5ch;
  min-height: var(--tap);
  padding: 0 1ch;
  color: var(--ink);
  text-decoration: none;
}
a.tt-row:hover, a.tt-row:focus-visible {
  background: var(--voice);
  color: var(--tt-black);
  outline: none;
}
a.tt-row:hover .tt-row-num,
a.tt-row:hover .tt-row-val,
a.tt-row:hover .tt-row-sub,
a.tt-row:focus-visible .tt-row-num,
a.tt-row:focus-visible .tt-row-val,
a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }

.tt-row-num {
  color: var(--voice);
  flex: 0 0 auto;
  letter-spacing: 0.08em;
}
.tt-row-name {
  flex: 0 0 auto;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  min-width: 11ch;
}
/* Marks a row that leaves the app (System's Transmission links to each
   service's own UI) — everywhere else a .tt-row stays inside Media
   Centre, so leaving needs its own quiet signal. */
.tt-row-ext { color: var(--ink-dim); }
.tt-row-body { flex: 1; min-width: 0; }
.tt-row-title {
  overflow-wrap: anywhere;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tt-row-sub { color: var(--ink-dim); }
.tt-row-val {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--live);
  white-space: nowrap;
}
.tt-row-val.ok      { color: var(--ok); }
.tt-row-val.alert   { color: var(--alert); }
.tt-row-val.warn    { color: var(--voice); }
.tt-row-val.pending { color: var(--pending); }
.tt-row-val.off     { color: var(--ink-dim); }

/* ── The one ruler (discipline 3) ───────────────────────
   Every bar in the service is exactly --bar-cells cells wide and
   fills in whole cells. Two percentages anywhere in the app are
   therefore directly comparable by eye. Never restyle this per
   card; that is precisely what it exists to prevent. */
.tt-bar {
  display: inline-block;
  width: min(calc(var(--bar-cells) * var(--bar-cell-w)), 100%);
  height: 0.8em;
  flex: 0 0 auto;
  vertical-align: -0.05em;
  background-image: repeating-linear-gradient(
    90deg,
    var(--ink-dimmer) 0 calc(var(--bar-cell-w) - 2px),
    transparent calc(var(--bar-cell-w) - 2px) var(--bar-cell-w)
  );
}
.tt-bar > i {
  display: block;
  height: 100%;
  background-image: repeating-linear-gradient(
    90deg,
    currentColor 0 calc(var(--bar-cell-w) - 2px),
    transparent calc(var(--bar-cell-w) - 2px) var(--bar-cell-w)
  );
}
.tt-bar.ok    { color: var(--ok); }
.tt-bar.warn  { color: var(--voice); }
.tt-bar.alert { color: var(--alert); }

/* ── Trend (discipline 4) ───────────────────────────────
   Only rendered where the backend actually carries a comparison
   (current speed against the 7-day average, a temperature
   against its threshold). Where there is no history there is no
   arrow — an invented trend is worse than no trend. */
.tt-trend { flex: 0 0 auto; letter-spacing: 0.05em; }
.tt-trend.up   { color: var(--ok); }
.tt-trend.down { color: var(--voice); }
.tt-trend.flat { color: var(--ink-dim); }

/* ── Figures ────────────────────────────────────────────
   A labelled number, set at double height because on this page
   the number is the content. */
.tt-figs {
  display: flex;
  flex-wrap: wrap;
  gap: 0 4ch;
  margin: var(--row) 0;
  padding: 0 1ch;
}
.tt-fig { min-width: 12ch; }
.tt-fig-v { color: var(--live); }
.tt-fig-v.ok      { color: var(--ok); }
.tt-fig-v.alert   { color: var(--alert); }
.tt-fig-v.warn    { color: var(--voice); }
.tt-fig-v.pending { color: var(--pending); }
.tt-fig-l {
  color: var(--ink-dim);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

/* ── Now showing ────────────────────────────────────────
   The lower two-thirds of the index. Double height, cyan, with
   the progress as a run of mosaic cells. */
.tt-now { margin: var(--row) 0 0; padding: 0 1ch; }
.tt-now-title { color: var(--live); margin-bottom: calc(var(--row) * 0.4); }
.tt-now-meta { color: var(--ink); }
.tt-now-line {
  display: flex;
  align-items: center;
  gap: 1.5ch;
  margin-top: calc(var(--row) * 0.5);
  color: var(--live);
}
.tt-now-pct { color: var(--live); flex: 0 0 auto; }

/* ── Table ──────────────────────────────────────────────
   No rules, no zebra, no borders. Columns are held by alignment
   and the monospace grid; the head row is a colour field. */
.tt-table-scroll { overflow-x: auto; }
.tt-table {
  width: 100%;
  border-collapse: collapse;
  white-space: nowrap;
}
.tt-table th, .tt-table td {
  padding: 0.35em 1ch 0.35em 0;
  text-align: left;
  font-weight: 400;
}
.tt-table thead th {
  color: var(--voice);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.tt-table tbody th { color: var(--ink); }
.tt-table td { color: var(--ink-dim); }
.tt-table .num { text-align: right; padding-right: 2ch; }
.tt-table .val { color: var(--live); }
.tt-table tbody tr:hover th,
.tt-table tbody tr:hover td { background: var(--field); color: var(--tt-white); }

/* ── Disk rows (System, phone reflow of the table above) ──
   Below 560px a six-column table stops fitting; scrolling it
   horizontally hides the bar and the temperature, the two
   readings that matter most here. This is the other rendering
   of the same disk data, shown only below that width — see the
   media query at the bottom of this file for the swap. */
.tt-disk-rows { display: none; flex-direction: column; }
.tt-disk-row { padding: 0.4em 1ch; }
.tt-disk-row-top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4em 1ch;
}
.tt-disk-name {
  flex: 0 0 auto;
  min-width: 5ch;
  color: var(--ink);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.tt-disk-pct {
  flex: 0 0 auto;
  color: var(--live);
  font-variant-numeric: tabular-nums;
}
.tt-disk-row-sub {
  margin-top: 0.15em;
  color: var(--ink-dim);
}
.tt-disk-row:hover { background: var(--field); }
.tt-disk-row:hover .tt-disk-name,
.tt-disk-row:hover .tt-disk-pct,
.tt-disk-row:hover .tt-disk-row-sub { color: var(--tt-white); }

/* ── Tiles (library / discover) ─────────────────────────
   Artwork is the one thing in this world that is not made of
   characters, so it is treated as an inserted transmission: a
   flat rectangle on the grid with its label in a colour field
   beneath. No rounding, no shadow, no border. */
.tt-tiles {
  display: grid;
  gap: var(--row) 2ch;
  grid-template-columns: repeat(auto-fill, minmax(min(12.5ch, 100%), 1fr));
  padding: 0 1ch;
}
/* P100's Just Added: a fixed row, not a viewport-dependent auto-fill.
   Paired with a fixed item count (see home()), this guarantees a full
   row every time rather than leaving a lone tile beside empty tracks —
   and it keeps this block compact, so NOW SHOWING above it can hold the
   lower two-thirds of the first viewport the direction contract names. */
.tt-tiles-row {
  grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 560px) {
  .tt-tiles-row { grid-template-columns: repeat(1, 1fr); }
}
.tt-tile { display: flex; flex-direction: column; min-width: 0; }
.tt-tile-art {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  /* Covers the box while a real poster is still loading, and IS the
     fallback for a poster that never arrives — dithered, per
     discipline 1, never a flat mid-grey panel. */
  background-color: var(--tt-black);
  background-image: var(--dither-mid);
}
.tt-tile-t {
  margin-top: 0.4em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tt-tile-s { color: var(--ink-dim); }
.tt-tile-badge { color: var(--voice); }
.tt-tile-p { margin-top: 0.35em; }

/* ── Controls ───────────────────────────────────────────
   A teletext control is a reversed row of cells. That is the
   entire button vocabulary: no fill, no radius, no border. */
.tt-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 0 1.5ch;
  background: var(--voice);
  color: var(--tt-black);
  border: none;
  font-family: inherit;
  font-size: inherit;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
}
.tt-btn:hover:not(:disabled) { background: var(--live); }
.tt-btn:disabled {
  background-color: var(--tt-black);
  background-image: var(--dither-mid);
  color: var(--ink-dim);
  cursor: default;
}
.tt-btn.armed { background: var(--alert); color: var(--tt-black); }
.tt-btn.done  { background: transparent; color: var(--ok); cursor: default; }
.tt-btn-quiet {
  background: transparent;
  color: var(--live);
  padding: 0 1ch;
}
.tt-btn-quiet:hover:not(:disabled) { background: var(--live); color: var(--tt-black); }
.tt-btn-sm { min-height: calc(var(--row) * 1.6); width: 100%; }

.tt-input {
  min-height: var(--tap);
  padding: 0 1ch;
  background: var(--tt-black);
  color: var(--voice);
  border: none;
  /* The one place a line is allowed: an underscore run, which is
     how a teletext form showed a field. It is a character row, not
     an enclosure. */
  background-image: repeating-linear-gradient(
    90deg,
    var(--ink-dimmer) 0 0.6em,
    transparent 0.6em 0.9em
  );
  background-size: 100% 2px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  font-family: inherit;
  font-size: 16px; /* stops iOS zooming the page on focus */
  outline: none;
}
.tt-input::placeholder { color: var(--ink-dim); }
.tt-input:focus {
  background-image: repeating-linear-gradient(
    90deg,
    var(--voice) 0 0.6em,
    transparent 0.6em 0.9em
  );
  background-size: 100% 2px;
  background-repeat: no-repeat;
  background-position: 0 100%;
}

/* ── Degraded service ───────────────────────────────────
   Teletext's own failure state was a page that simply did not
   arrive, announced in red on the row where it should have been.
   No panel, no left-border accent, no icon. */
.tt-err { padding: 0 1ch; color: var(--alert); }
.tt-err-cause { color: var(--alert); }
.tt-err details { margin-top: 0.3em; }
.tt-err summary {
  color: var(--ink-dim);
  cursor: pointer;
  list-style: none;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  min-height: calc(var(--row) * 1.6);
  display: flex;
  align-items: center;
}
.tt-err summary::-webkit-details-marker { display: none; }
.tt-err summary::before { content: "\25B8\00a0"; }
.tt-err details[open] summary::before { content: "\25BE\00a0"; }
.tt-err summary:hover { color: var(--ink); }
.tt-err pre {
  margin: 0.3em 0 0;
  padding: 0.5em 1ch;
  background-color: var(--tt-black);
  background-image: var(--dither-mid);
  color: var(--ink);
  font-family: inherit;
  font-size: inherit;
  white-space: pre-wrap;
  word-break: break-word;
}

.tt-empty { padding: 0 1ch; color: var(--ink-dim); }

/* ── Fastext ────────────────────────────────────────────
   The four coloured keys. Real navigation, not decoration:
   click one, or press its initial. Deliberately NOT fixed to
   the viewport — it is a row of the page, where it always
   was, not furniture bolted over it. On the index it closes
   the page, under Now Showing/Just Added, because the index's
   own body is already the full nav. Everywhere else — where
   these four keys are the only nav on the page — it opens the
   page, right under the header, so it doesn't need a scroll
   to reach. */
.fastext {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  max-width: min(var(--page-cols), 100%);
  margin: calc(var(--row) * 2) auto 0;
  padding: 0 var(--gutter);
  gap: 1ch;
}
.fastext-top {
  margin: var(--row) auto calc(var(--row) * 1.5);
}
.fastext a, .fastext-here {
  display: flex;
  align-items: center;
  min-width: 0;
  gap: 1ch;
  min-height: var(--tap);
  padding: 0 1ch;
  color: var(--ink);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The key's identity is carried by a cell at its head — the physical
   coloured button on the remote. */
.fastext a > span, .fastext-here > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fastext a::before, .fastext-here::before {
  content: "";
  flex: 0 0 auto;
  width: 1.2ch;
  height: 1em;
  background: currentColor;
}
/* The page you're already on: present for orientation, same shape as
   its neighbours, but not a link — clicking your own destination did
   nothing useful, so now it isn't offered as a click at all. */
.fastext-here {
  color: var(--ink-dim);
  cursor: default;
}
.fastext .k-red     { color: var(--alert); }
.fastext .k-green   { color: var(--ok); }
.fastext .k-yellow  { color: var(--voice); }
.fastext .k-cyan    { color: var(--live); }
.fastext .k-magenta { color: var(--pending); }
/* Pure blue on black is ~2.4:1 contrast — well under the 4.5:1 every
   other body text in this app clears — but the word carries the
   colour like every other key here, at the visitor's request. */
.fastext .k-blue { color: var(--field); }
/* --tt-orange is a genuine ninth colour, not one of the locked eight —
   see its definition in base.css for why the palette ran out here. */
.fastext .k-orange { color: var(--tt-orange); }
.fastext a:hover, .fastext a:focus-visible {
  background: currentColor;
  outline: none;
}
.fastext a:hover span, .fastext a:focus-visible span { color: var(--tt-black); }

/* ── Footer ─────────────────────────────────────────────  */
.tt-foot {
  max-width: min(var(--page-cols), 100%);
  margin: var(--row) auto 0;
  padding: 0 var(--gutter) var(--row);
  display: flex;
  gap: 2ch;
  color: var(--ink-dim);
}
.tt-foot .v { margin-left: auto; }

/* ── Motion ─────────────────────────────────────────────
   The one authored moment: a teletext page did not appear, it
   was transmitted, painting in a few rows at a time. Sections
   arrive in sequence on load. Nothing else in this world moves.
   Content is visible by default; the animation only delays an
   already-rendered row's opacity. */
@keyframes tt-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.tt-page > * {
  animation: tt-in 0.18s steps(2, end) both;
}
.tt-page > :nth-child(1) { animation-delay: 0.00s; }
.tt-page > :nth-child(2) { animation-delay: 0.05s; }
.tt-page > :nth-child(3) { animation-delay: 0.10s; }
.tt-page > :nth-child(4) { animation-delay: 0.15s; }
.tt-page > :nth-child(5) { animation-delay: 0.20s; }
.tt-page > :nth-child(6) { animation-delay: 0.25s; }
.tt-page > :nth-child(7) { animation-delay: 0.30s; }
.tt-page > :nth-child(n+8) { animation-delay: 0.35s; }

@media (prefers-reduced-motion: reduce) {
  .tt-page > * { animation: none; }
  * { transition-duration: 0.01ms !important; }
}

/* ── Responsive ─────────────────────────────────────────
   The grid is character-based, so it survives narrowing on its
   own. Two real moves: the double-height row stops being double
   on a phone (it would eat the viewport), and fastext stacks to
   two columns rather than shrinking below a tap target. */
@media (max-width: 760px) {
  /* --bar-cells stays 20 at every width, on purpose: the bar() macro in
     _macros.html hardcodes the same 20 so it can quantise a percentage to
     whole cells server-side. Two constants that have to agree is exactly
     the kind of drift discipline 3 (One Ruler) exists to prevent — an
     earlier version of this file set 14 here, which desynced the CSS
     width from the markup's own cell math and could land a fill mid-cell
     on a phone. --bar-cell-w is in ch, so the ruler still shrinks with
     the font; it just does it as 20 smaller cells, never a different
     cell count. */
  :root {
    --gutter: 1rem;
  }
  .tt-dh { font-size: 1.375rem; }
  .fastext { grid-template-columns: repeat(2, 1fr); gap: 0; }
  .tt-row-name { min-width: 9ch; }
  .tt-figs { gap: 0 2.5ch; }
}
@media (max-width: 560px) {
  /* Below this a row cannot hold number, name, description and value on
     one line. The description drops to its own line under them rather
     than the row growing wider than the screen. */
  .tt-row {
    gap: 0 1ch;
    flex-wrap: wrap;
    padding-top: 0.35em;
    padding-bottom: 0.35em;
  }
  .tt-row-name { min-width: 0; }
  .tt-row-body {
    flex: 1 0 100%;
    order: 3;
  }
  .tt-row-title { white-space: normal; }
  .tt-row-val { margin-left: auto; white-space: normal; }
  .tt-head-name { letter-spacing: 0.12em; }
  .tt-head-dl { margin-left: 0; flex-basis: 100%; }
  .tt-head-clock { margin-left: 0; flex-basis: 100%; }

  /* The System disk table swaps for its row reflow here — see the
     "Disk rows" block above. */
  .tt-table-scroll { display: none; }
  .tt-disk-rows { display: flex; }
}

/* ── Search ─────────────────────────────────────────────
   A field and a reversed key. The underscore run under the
   input is the only line in this world, and it is a character
   row rather than a box around anything. */
.tt-search {
  display: flex;
  gap: 1ch;
  align-items: stretch;
  margin: var(--row) 0;
  padding: 0 1ch;
}
.tt-search .tt-input { flex: 1; min-width: 0; }

@media (max-width: 560px) {
  .tt-search { flex-direction: column; }
}

/* ── Forms ──────────────────────────────────────────────
   A label above its field, the field marked by an underscore
   run. No box, no group border — the colour field header above
   each block is what says where one section ends. */
.tt-form {
  display: flex;
  flex-direction: column;
  gap: var(--row);
  align-items: flex-start;
  margin: var(--row) 0 calc(var(--row) * 1.5);
  padding: 0 1ch;
  max-width: 52ch;
}
.tt-field-row { width: 100%; }
.tt-label {
  display: block;
  color: var(--ink-dim);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.tt-form .tt-input { width: 100%; }
.tt-select {
  appearance: none;
  -webkit-appearance: none;
  color: var(--voice);
  cursor: pointer;
}
.tt-select option { background: var(--tt-black); color: var(--voice); }
.tt-hint { margin: 0.2em 0 0; color: var(--ink-dim); }
.tt-inline-err { color: var(--alert); min-height: 1em; }

.tt-keys {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(30ch, 100%), 1fr));
  gap: 0 3ch;
}
.tt-keys .tt-form { max-width: none; }

/* ── Toast ──────────────────────────────────────────────
   Teletext announced itself in the header row, so a confirmation
   lands as a reversed strip rather than a floating card. */
.tt-toast {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.6em var(--gutter);
  background: var(--ok);
  color: var(--tt-black);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 0.15s, transform 0.15s;
  pointer-events: none;
}
.tt-toast.show { opacity: 1; transform: translateY(0); }
.tt-toast.error {
  background: var(--alert);
  color: var(--tt-black); /* same contrast fix as .tt-field.crit */
}

/* ── Sign-in ────────────────────────────────────────────
   The one page with no navigation, so it centres its single
   block rather than opening the full page measure. */
.tt-login .tt-page { max-width: 52ch; margin: 0 auto; }
.tt-login .tt-head { max-width: 52ch; }
.tt-login .tt-foot { max-width: 52ch; }
