/* tecle-ui tokens — the color and type values every property shares.
 *
 * Source of truth: these are tecle.ai's live values (site/style.css, shipped
 * 2026-09-13), not a guess. tecle-surface's inline <style> block invented a
 * second, numerically different palette for the same shapes (bg/fg/muted/
 * card/border) and tecle-idp's sign-in pages have none at all — that drift is
 * why this file exists. A consumer that wants its own accent overrides only
 * --accent (and the accent-derived values below it); it does not fork the
 * rest of the scale.
 */

:root {
  --tui-bg: #fbfaf7;
  --tui-fg: #1b1b1a;
  --tui-muted: #5f5d57;
  --tui-rule: #e4e1da;
  --tui-card: #ffffff;
  --tui-accent: #2a5bd7;
  --tui-accent-fg: var(--tui-bg);

  --tui-live-bg: #e3f4e8;  --tui-live-fg: #1d6b37;
  --tui-built-bg: #e6edfb; --tui-built-fg: #2a4f9e;
  --tui-next-bg: #fbf0dc;  --tui-next-fg: #8a5a12;
  --tui-later-bg: #eeece7; --tui-later-fg: #5f5d57;

  --tui-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --tui-radius: 10px;
  --tui-radius-pill: 999px;

  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --tui-bg: #131312;
    --tui-fg: #ecebe7;
    --tui-muted: #a4a19a;
    --tui-rule: #2c2b28;
    --tui-card: #1b1b19;
    --tui-accent: #8fb0ff;

    --tui-live-bg: #183324;  --tui-live-fg: #8fdca8;
    --tui-built-bg: #1b2744; --tui-built-fg: #a9c1f5;
    --tui-next-bg: #3a2c12;  --tui-next-fg: #f0c475;
    --tui-later-bg: #262522; --tui-later-fg: #a4a19a;
  }
}

/* An explicit override wins over the media query either direction, the way
 * every Artifact/host theme toggle in this fleet already expects. */
:root[data-theme="light"] {
  --tui-bg: #fbfaf7; --tui-fg: #1b1b1a; --tui-muted: #5f5d57;
  --tui-rule: #e4e1da; --tui-card: #ffffff; --tui-accent: #2a5bd7;
  --tui-live-bg: #e3f4e8;  --tui-live-fg: #1d6b37;
  --tui-built-bg: #e6edfb; --tui-built-fg: #2a4f9e;
  --tui-next-bg: #fbf0dc;  --tui-next-fg: #8a5a12;
  --tui-later-bg: #eeece7; --tui-later-fg: #5f5d57;
}
:root[data-theme="dark"] {
  --tui-bg: #131312; --tui-fg: #ecebe7; --tui-muted: #a4a19a;
  --tui-rule: #2c2b28; --tui-card: #1b1b19; --tui-accent: #8fb0ff;
  --tui-live-bg: #183324;  --tui-live-fg: #8fdca8;
  --tui-built-bg: #1b2744; --tui-built-fg: #a9c1f5;
  --tui-next-bg: #3a2c12;  --tui-next-fg: #f0c475;
  --tui-later-bg: #262522; --tui-later-fg: #a4a19a;
}

/* tecle-ui primitives — the handful of shapes every property redraws today:
 * a page container, a button, a status badge, a card. Nothing page-specific
 * (no hero, no nav, no footer) — those stay in each site. Every value here
 * is a var(--tui-*) from tokens.css; a primitive with a literal color is a
 * bug (test/primitives.test.js checks this).
 */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

.tui-reset {
  margin: 0;
  background: var(--tui-bg);
  color: var(--tui-fg);
  font: 16px/1.6 var(--tui-font);
}

.tui-wrap { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem; }

.tui-muted { color: var(--tui-muted); }

/* Button. tecle-ai's `.cta`; tecle-idp's sign-in buttons have no primitive
 * to draw from today — that's this. */
.tui-btn {
  display: inline-block;
  padding: .5rem 1rem;
  border: 0;
  border-radius: var(--tui-radius-pill);
  background: var(--tui-accent);
  color: var(--tui-accent-fg);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.tui-btn:hover { opacity: .9; }
.tui-btn:disabled { opacity: .5; cursor: not-allowed; }

/* Status badge. tecle-ai's `.pill`. */
.tui-pill {
  display: inline-block;
  min-width: 3.75rem;
  padding: .0625rem .5rem;
  border-radius: var(--tui-radius-pill);
  font-size: .75rem;
  font-weight: 600;
  text-align: center;
  white-space: nowrap;
}
.tui-pill-live  { background: var(--tui-live-bg);  color: var(--tui-live-fg); }
.tui-pill-built { background: var(--tui-built-bg); color: var(--tui-built-fg); }
.tui-pill-next  { background: var(--tui-next-bg);  color: var(--tui-next-fg); }
.tui-pill-later { background: var(--tui-later-bg); color: var(--tui-later-fg); }

/* Card. Generalizes tecle-ai's `.status li` and tecle-surface's `.profile` —
 * the same bordered, rounded, card-colored box, drawn twice with two
 * different palettes before this existed. */
.tui-card {
  background: var(--tui-card);
  border: 1px solid var(--tui-rule);
  border-radius: var(--tui-radius);
  padding: .875rem 1rem;
}

/* ---- form controls ------------------------------------------------------ *
 *
 * Here rather than in components.css because a form is not an application
 * shape: tecle-idp's sign-in and enrollment pages are static pages whose
 * whole content is a field and a button, and they have no styling at all
 * today (README "Why"). A property that takes tokens + primitives and stops
 * should still be able to draw a sign-in box.
 *
 * Every control sets `font: inherit`. A bare <textarea> renders in the
 * browser's monospace default and a <select> in the system UI face, so a form
 * built from unstyled controls silently uses three typefaces — which is what
 * tecle-os's Orchestration submit form does today
 * (apps/shell/src/orch/Orchestration.jsx:220-228, bare <select> and
 * <textarea>, written that way because this section did not exist).
 */

.tui-field { display: block; }
.tui-field + .tui-field { margin-top: .875rem; }

.tui-label {
  display: block;
  margin-bottom: .25rem;
  color: var(--tui-muted);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}

/* A fieldset used only for what it does — one `disabled` that reaches every
 * control inside — never for how it looks. `min-width: 0` is not a no-op: a
 * fieldset's default min-width is min-content, so one long unbroken line
 * inside it pushes the form past its panel instead of wrapping. */
.tui-fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }

/* One shape for all three controls. */
.tui-input,
.tui-select,
.tui-textarea {
  display: block;
  width: 100%;
  padding: .5rem .625rem;
  border: 1px solid var(--tui-rule);
  border-radius: var(--tui-radius);
  background: var(--tui-card);
  color: var(--tui-fg);
  font: inherit;
}

.tui-textarea {
  /* Vertical only: a horizontally resizable textarea drags itself out of the
   * panel it sits in. */
  resize: vertical;
  min-height: 6rem;
}

/* The native select arrow ignores padding and colour and differs per engine,
 * so it goes. The chevron is two gradient triangles in a token colour: it
 * follows the theme with no image and no `url()`, which is the objection that
 * kept the native arrow here until the Air's parallel 0.4.0 showed the third
 * option. */
.tui-select {
  appearance: none;
  padding-right: 2rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--tui-muted) 50%),
    linear-gradient(135deg, var(--tui-muted) 50%, transparent 50%);
  background-position: calc(100% - 1.0625rem) 55%, calc(100% - .75rem) 55%;
  background-size: .3125rem .3125rem;
  background-repeat: no-repeat;
}

.tui-input:hover,
.tui-select:hover,
.tui-textarea:hover { border-color: var(--tui-muted); }

/* Error styling keys off `aria-invalid`, never a class of its own, so the
 * announced state and the visible state cannot drift apart — a field that
 * looks wrong but reads as fine to a screen reader is the failure mode.
 *
 * The `next` pair, not a fifth status color (components.css says the same
 * about .tui-state-error) and deliberately not `later`: in dark mode
 * --tui-later-fg IS --tui-muted, which is the hover border above, so a field
 * borrowing `later` would simply look hovered. --tui-next-fg is distinct from
 * --tui-muted in both themes. */
.tui-input[aria-invalid="true"],
.tui-select[aria-invalid="true"],
.tui-textarea[aria-invalid="true"] {
  border-color: var(--tui-next-fg);
}

.tui-hint { margin-top: .25rem; color: var(--tui-muted); font-size: .8125rem; }

.tui-input:disabled,
.tui-select:disabled,
.tui-textarea:disabled { opacity: .5; cursor: not-allowed; }

/* One focus ring for everything focusable, drawn explicitly rather than left
 * to the UA: a control that sets its own border and background gets a default
 * ring whose contrast is computed against the page, not against .tui-card,
 * and it disappears against the dark theme's panel. `:focus-visible` so a
 * mouse click does not leave a ring behind. */
.tui-btn:focus-visible,
.tui-input:focus-visible,
.tui-select:focus-visible,
.tui-textarea:focus-visible {
  outline: 2px solid var(--tui-accent);
  outline-offset: 2px;
}

/* A second and third button, because a form has more than one verb. `quiet`
 * is the cancel beside a submit; `risk` is the destructive one — revoking a
 * connection, deleting a grant. `risk` borrows the `next` pair for the same
 * reason .tui-state-error borrows `later`: four status pairs exist, and a
 * fifth invented here is the start of a second palette. */
.tui-btn-quiet {
  background: transparent;
  border: 1px solid var(--tui-rule);
  color: var(--tui-fg);
}
/* Inverted — the pair's foreground as the fill — because the destructive
 * button's usual home is .tui-confirm, which is itself drawn on `next-bg`. A
 * tinted button on a tinted block is amber text on amber and the button
 * disappears; this was visible in the preview the moment both existed. */
.tui-btn-risk {
  background: var(--tui-next-fg);
  color: var(--tui-next-bg);
}

/* tecle-ui components — the shapes an application shell needs that a page does
 * not: a frame, a rail, a panel, a data table, a stat tile, and the states a
 * live view spends most of its time in.
 *
 * Distinct from primitives.css on purpose. A primitive is a shape every
 * property redraws (a button, a card, a pill) and tecle.ai or a tecle-surface
 * would use one tomorrow. A component here assumes an *application*: something
 * with chrome, regions, and data that arrives later or fails to. A static page
 * has no use for any of it, so it does not have to carry it.
 *
 * Same rule as primitives, enforced the same way (test/components.test.js): no
 * literal colors. Every color is a var(--tui-*). Literal LENGTHS are fine and
 * primitives.css already uses them — the drift this package exists to stop was
 * five palettes, not five paddings.
 *
 * Layout is grid with named areas rather than nested flex, so a region can be
 * added or moved in one place. The AI rail (Wave 3) is the reason: it has to
 * appear beside every screen without each screen knowing about it.
 */

/* ---- the frame ---------------------------------------------------------- */

/* Full-viewport application chrome: a header across the top, an optional nav
 * beside the content, an optional rail on the far side. `min-height: 100dvh`
 * rather than `vh` — on mobile browsers `vh` includes the collapsing URL bar,
 * so a `100vh` shell is always slightly taller than the screen and the page
 * scrolls by a few pixels that contain nothing. */
.tui-shell {
  display: grid;
  grid-template-areas: "head head head" "nav main rail";
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr);
  min-height: 100dvh;
  background: var(--tui-bg);
  color: var(--tui-fg);
}

.tui-shell-head {
  grid-area: head;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: .75rem 1.25rem;
  border-bottom: 1px solid var(--tui-rule);
  background: var(--tui-card);
}

/* Pushes whatever follows it to the far end of the header. */
.tui-shell-head-end { margin-left: auto; }

.tui-shell-nav {
  grid-area: nav;
  width: 14rem;
  padding: 1rem .75rem;
  border-right: 1px solid var(--tui-rule);
  overflow-y: auto;
}

.tui-shell-main {
  grid-area: main;
  /* minmax(0, 1fr) above plus this is what stops a wide table forcing the
   * whole grid wider than the viewport — a grid item's default min-width is
   * auto, which means "as wide as my content", which means the frame grows
   * and the nav slides off screen. */
  min-width: 0;
  padding: 1.5rem;
  overflow-y: auto;
}

/* The rail: persistent beside every screen, its own scroll context. Wave 3's
 * AI layer is the intended occupant; nothing here assumes that. */
.tui-shell-rail {
  grid-area: rail;
  width: 20rem;
  min-width: 0;
  border-left: 1px solid var(--tui-rule);
  background: var(--tui-card);
  overflow-y: auto;
}

/* Below a tablet there is room for one column. Nav and rail become ordinary
 * blocks in the flow rather than disappearing: a hidden nav on a phone is a
 * screen with no way out of it. */
@media (max-width: 60rem) {
  .tui-shell {
    grid-template-areas: "head" "nav" "main" "rail";
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
  }
  .tui-shell-nav { width: auto; border-right: 0; border-bottom: 1px solid var(--tui-rule); }
  .tui-shell-rail { width: auto; border-left: 0; border-top: 1px solid var(--tui-rule); }
  .tui-shell-main { padding: 1rem; }
}

/* ---- nav ---------------------------------------------------------------- */

.tui-nav { display: flex; flex-direction: column; gap: .125rem; }

.tui-nav-item {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .5rem .75rem;
  border-radius: var(--tui-radius);
  color: var(--tui-fg);
  text-decoration: none;
  font-size: .9375rem;
}
.tui-nav-item:hover { background: var(--tui-rule); }

/* aria-current, not a class: the accessible state and the visible state are
 * the same fact, and keeping them in one attribute means they cannot disagree. */
.tui-nav-item[aria-current="page"] {
  background: var(--tui-rule);
  font-weight: 600;
}

.tui-nav-label {
  padding: .75rem .75rem .25rem;
  color: var(--tui-muted);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}

/* ---- panel -------------------------------------------------------------- */

/* A titled region inside main. Where .tui-card is a box, a panel is a section
 * with a header that can carry actions and a body that can hold a table
 * flush to its edges. */
.tui-panel {
  border: 1px solid var(--tui-rule);
  border-radius: var(--tui-radius);
  background: var(--tui-card);
  overflow: hidden;
}

.tui-panel-head {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .75rem 1rem;
  border-bottom: 1px solid var(--tui-rule);
}

.tui-panel-title { margin: 0; font-size: .9375rem; font-weight: 600; }
.tui-panel-head-end { margin-left: auto; }
.tui-panel-body { padding: 1rem; }
/* For a table or list that should meet the panel's edges. */
.tui-panel-body-flush { padding: 0; }

/* ---- stat tile ---------------------------------------------------------- */

.tui-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: .75rem;
}

.tui-stat {
  padding: .875rem 1rem;
  border: 1px solid var(--tui-rule);
  border-radius: var(--tui-radius);
  background: var(--tui-card);
}

.tui-stat-label {
  color: var(--tui-muted);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.tui-stat-value {
  /* Tabular figures: without them a counter that ticks 1→2 reflows its own
   * width, and a column of numbers does not line up. */
  font-variant-numeric: tabular-nums;
  font-size: 1.75rem;
  line-height: 1.2;
  margin-top: .25rem;
}

.tui-stat-note { color: var(--tui-muted); font-size: .8125rem; margin-top: .125rem; }

/* A tile whose number is known to be out of date — a poll that failed, a
 * backend that stopped answering. Dimmed rather than hidden: a stat that
 * vanishes on error reads as zero. */
.tui-stat-stale { opacity: .55; }

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

.tui-table-scroll { overflow-x: auto; }

.tui-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .875rem;
}

.tui-table th,
.tui-table td {
  padding: .5rem .75rem;
  text-align: left;
  border-bottom: 1px solid var(--tui-rule);
}

.tui-table th {
  color: var(--tui-muted);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Survives the panel body's own scroll, so a long job list keeps its
   * headers. Needs a background or rows show through as they pass under. */
  position: sticky;
  top: 0;
  background: var(--tui-card);
}

.tui-table tbody tr:last-child td { border-bottom: 0; }
.tui-table-num { text-align: right; font-variant-numeric: tabular-nums; }
.tui-table-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8125rem; }

/* ---- the states a live view actually spends its time in ------------------ */

/* Three distinct states, three classes, on purpose. A view that renders a
 * spinner for "loading", "empty" and "the backend is down" alike teaches the
 * person to wait for something that is never coming. */
.tui-state {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .5rem;
  padding: 2rem 1rem;
  color: var(--tui-muted);
  text-align: left;
}

.tui-state-title { color: var(--tui-fg); font-weight: 600; }
.tui-state-center { align-items: center; text-align: center; }

/* Something failed. Uses the `later` pair rather than inventing a danger
 * color: this package has four status pairs and a fifth would be the start of
 * a second palette. When a real danger token is needed it belongs in
 * tokens.css beside the other four, not here. */
.tui-state-error {
  border: 1px solid var(--tui-rule);
  border-radius: var(--tui-radius);
  background: var(--tui-later-bg);
  color: var(--tui-later-fg);
}

/* A skeleton line for content that is genuinely on its way. Deliberately not
 * animated: a pulsing placeholder next to live-updating numbers reads as
 * motion that means something. */
.tui-skeleton {
  display: block;
  height: .75rem;
  border-radius: var(--tui-radius-pill);
  background: var(--tui-rule);
}

/* ---- forms, at application scale ---------------------------------------- *
 *
 * The controls themselves are primitives. What is here is what a screen full
 * of them needs: a stack, a row, an action bar, and the one interaction a
 * static page never has — confirming something destructive.
 */

/* Fields stacked with one gap. Grid rather than margins so the spacing lives
 * in one declaration; the stacking margin primitives.css gives adjacent fields
 * is for a field outside a form, and is zeroed here so it does not add to the
 * gap. */
.tui-form { display: grid; gap: .75rem; }
.tui-form > .tui-field + .tui-field { margin-top: 0; }

/* Fields side by side, wrapping to a stack when there is no room. The fields
 * flex from a basis rather than a fixed width, so a two-field row and a
 * three-field row need no different class. */
.tui-form-row {
  display: flex;
  flex-wrap: wrap;
  gap: .875rem;
}
.tui-form-row > .tui-field { flex: 1 1 12rem; min-width: 0; }
/* A row's own fields already carry the gap; the stacking margin would double
 * it on the wrap. */
.tui-form-row > .tui-field + .tui-field { margin-top: 0; }

/* A control that should take the rest of a .tui-row. min-width: 0 because an
 * input's automatic minimum is its intrinsic ~20ch, which overflows a phone. */
.tui-grow { flex: 1 1 12rem; min-width: 0; }

/* The verbs, at the end of a form. Right-aligned because the primary action
 * sits closest to the thumb and to the submit key's mental home; wrapping so
 * three buttons on a phone stack instead of overflowing the panel. */
.tui-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: .5rem;
  margin-top: 1rem;
}

/* Confirming something destructive — revoking a connection, ending a grant.
 *
 * Inline, in the flow, where the thing being acted on is. NOT a modal: a
 * modal needs a focus trap, a scroll lock, an escape handler and a portal,
 * none of which a stylesheet can provide, and a half-built one is worse than
 * none — the focus escapes behind it and the page beneath still scrolls.
 *
 * The `next` pair, like .tui-btn-risk and for the same reason: no fifth
 * status color. It reads as "needs your attention", which is exactly the
 * claim being made.
 */
.tui-confirm {
  border: 1px solid var(--tui-next-fg);
  border-radius: var(--tui-radius);
  background: var(--tui-next-bg);
  color: var(--tui-next-fg);
  padding: .875rem 1rem;
}

.tui-confirm-title { font-weight: 600; }

/* The consequence, spelled out. A confirmation whose body is "Are you sure?"
 * has told the person nothing they did not already know; this is where what
 * will actually happen goes, and it inherits the block's color rather than
 * going muted, because muted on the `next` background is unreadable. */
.tui-confirm-body { margin-top: .25rem; font-size: .875rem; }

.tui-confirm .tui-actions { margin-top: .75rem; }

/* ---- output ------------------------------------------------------------- */

/* Text a backend produced, shown verbatim: line breaks kept, nothing rendered
 * as markup. `overflow-wrap` is the half `pre-wrap` does not do — a long URL
 * or hash has no space to wrap at and would otherwise widen the whole panel.
 * Capped and scrolled, so one long answer cannot push the rest of the screen
 * out of reach. A job's result and an /ask answer are both this shape. */
.tui-output {
  margin: 0;
  max-height: 24rem;
  overflow: auto;
  padding: .75rem 1rem;
  border: 1px solid var(--tui-rule);
  border-radius: var(--tui-radius);
  background: var(--tui-bg);
  color: var(--tui-fg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8125rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---- utility ------------------------------------------------------------ */

/* Visually hidden, still announced. The same shape brand.css uses for an
 * SVG-mode wordmark's label — display:none would take it from screen readers
 * too. */
.tui-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.tui-row { display: flex; align-items: center; gap: .5rem; }
.tui-row-wrap { flex-wrap: wrap; }
.tui-spacer { flex: 1 1 auto; }

/* tecle-ui brand — the identity layer: the mark, the wordmark, and the lockup
 * that pairs them. Split from tokens.css deliberately. tokens.css is the
 * *system* (what "muted text" or "a card" means) and does not change when the
 * brand does; this file is the *brand* (which mark, which wordmark face) and
 * is the one file that changes when either is redrawn.
 *
 * Why the split earns its keep right now: the mark is decided (the flat
 * ink/mono lattice — chosen over the neon-gradient direction precisely
 * because a single flat color can be drawn with currentColor and needs no
 * light/dark variant), and the wordmark is NOT. The two candidates are not
 * the same kind of asset:
 *
 *   Orbis    circle-built monoline, uniform construction — can be live text
 *            in a real typeface. Selectable, reflows, no asset pipeline.
 *   Metatron letter-specific stencil breaks — can only ever be an SVG
 *            outline. Never live text, needs an accessible label.
 *
 * Those need different render paths, so the lockup below supports BOTH and
 * chooses with one attribute. Deciding later means editing this file and
 * nothing else — no screen re-lays-out, because the wordmark's footprint is
 * reserved from --tui-wordmark-ratio rather than measured from its content.
 *
 * A screen that reaches past .tui-lockup / .tui-mark / .tui-wordmark to an
 * SVG path or a font name of its own is the drift test/brand.test.js exists
 * to make expensive. Same rule, and same reason, as primitives.css's "a
 * literal color here is a bug".
 */

:root {
  /* Mark assets. These two lines are the only url()s in the package —
   * test/brand.test.js enforces that, because "the logo lives in exactly one
   * place" is the property that makes a redraw a one-file change.
   *
   * Loaded as CSS masks below, not as <img>. A mask uses only the asset's
   * alpha channel, so the rendered color is always the inherited text color:
   * one file serves light and dark with nothing to keep in sync. */
  --tui-mark-src: url("./mark.svg");
  --tui-mark-core-src: url("./mark-core.svg");

  /* Wordmark asset. `none` while the choice is open — the lockup falls back
   * to live text (see .tui-wordmark below). The day Metatron wins: set this
   * to a url() and add data-render="svg" at the call site. The day Orbis
   * wins: leave this at `none` and point --tui-font-display at the face.
   *
   * GOTCHA, verified in a browser 2026-09-18: a relative url() inside a
   * custom property is resolved against the stylesheet that *consumes* it,
   * not the document that declares the override. A consumer that sets
   *   style="--tui-wordmark-src: url('brand/wordmark.svg')"
   * gets it resolved against this file's directory — .../dist/brand/… — and
   * the element silently renders as nothing, because a mask that fails to
   * load masks everything out. There is no console error. An override must
   * use a root-relative or absolute URL. The two mark tokens above are
   * exempt: they are declared here, so "./" is this directory, which is
   * exactly where build.mjs puts the files. */
  --tui-wordmark-src: none;

  /* THE RESERVED BOX — the token that makes the decision genuinely
   * deferrable. The lockup sizes the wordmark from this ratio, never from
   * its content, so Orbis (wide, airy) and Metatron (tight, heavy) occupy
   * the same footprint and no header shifts when the choice lands. Measure
   * the final vector once and set it here. Value is width ÷ cap-height. */
  --tui-wordmark-ratio: 2.9;

  /* Display face, deliberately separate from --tui-font. --tui-font is the
   * UI stack — tables, labels, body copy. This is the wordmark's face, and
   * it must never leak into either (test/brand.test.js checks primitives.css
   * does not reference it). Falls back to the UI stack while undecided. */
  --tui-font-display: var(--tui-font);
  --tui-wordmark-weight: 600;
  --tui-wordmark-tracking: -0.01em;

  /* One knob scales the whole lockup: mark size, wordmark height and gap all
   * derive from it. A consumer sets --tui-lockup-size and nothing else. */
  --tui-lockup-size: 1.5rem;
  --tui-lockup-gap: 0.5em;
}

/* ---- the mark ----------------------------------------------------------
 *
 * Masked over currentColor, so the mark takes the text color of wherever it
 * sits — a rail, a button, a light card, a dark chrome bar — with no variant
 * files. This is the entire payoff of picking the flat ink direction.
 */

.tui-mark,
.tui-mark-core {
  display: inline-block;
  flex: none;
  width: var(--tui-mark-size, var(--tui-lockup-size));
  height: var(--tui-mark-size, var(--tui-lockup-size));
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: contain;
          mask-size: contain;
}

.tui-mark {
  -webkit-mask-image: var(--tui-mark-src);
          mask-image: var(--tui-mark-src);
}

/* The reduced mark: the centre joint only, no outer branches. Below roughly
 * 20px the full mark's branches fuse into the joint and read as noise — use
 * this instead. Documented rather than switched automatically because the
 * caller knows its own render size and CSS cannot branch on an element's own
 * width without a container query at every call site; a rule that needs
 * per-call-site setup is not a primitive. Favicon and app-icon builds take
 * mark-core.svg at 16/32px and mark.svg from 48px up. */
.tui-mark-core {
  -webkit-mask-image: var(--tui-mark-core-src);
          mask-image: var(--tui-mark-core-src);
}

/* Escape hatch: a consumer that would rather inline the SVG — to animate a
 * path, or to avoid a second request in a Worker-rendered page like
 * tecle-surface — gets the same color behavior from the same class name
 * family, so call sites still never name a file. */
.tui-mark-inline {
  display: inline-block;
  flex: none;
  width: var(--tui-mark-size, var(--tui-lockup-size));
  height: var(--tui-mark-size, var(--tui-lockup-size));
}
.tui-mark-inline > svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: currentColor;
  stroke: currentColor;
}

/* ---- the wordmark ------------------------------------------------------
 *
 * Base rule is TEXT mode, which is both the Orbis outcome and the honest
 * default while the choice is open: it renders today, in the UI stack, and
 * upgrades in place by repointing --tui-font-display.
 *
 * min-width holds the reserved box even in text mode, so a wordmark that is
 * narrower than the final vector does not let surrounding layout collapse
 * into space it will later have to give back.
 */

.tui-wordmark {
  display: inline-block;
  min-width: calc(var(--tui-wordmark-height, var(--tui-lockup-size)) * var(--tui-wordmark-ratio));
  font-family: var(--tui-font-display);
  font-size: var(--tui-wordmark-height, var(--tui-lockup-size));
  font-weight: var(--tui-wordmark-weight);
  letter-spacing: var(--tui-wordmark-tracking);
  line-height: 1;
  color: inherit;
  white-space: nowrap;
}

/* SVG mode — Metatron, or any outline wordmark. Same mask-over-currentColor
 * trick as the mark, so it themes identically. aspect-ratio (not min-width)
 * fixes the box exactly; the text child stays in the DOM and is hidden
 * visually rather than removed, because an outline wordmark carries no
 * accessible name of its own. */
.tui-wordmark[data-render="svg"] {
  min-width: 0;
  height: var(--tui-wordmark-height, var(--tui-lockup-size));
  aspect-ratio: var(--tui-wordmark-ratio);
  background-color: currentColor;
  -webkit-mask-image: var(--tui-wordmark-src);
          mask-image: var(--tui-wordmark-src);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: left center;
          mask-position: left center;
  -webkit-mask-size: contain;
          mask-size: contain;
}

.tui-wordmark[data-render="svg"] > .tui-wordmark-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- the lockup --------------------------------------------------------
 *
 * The only shape a screen should ask for by name. Horizontal by default,
 * stacked variant for a splash or a sign-in card (tecle-idp's login page has
 * no chrome at all today — this is what it gets).
 */

.tui-lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--tui-lockup-gap);
  font-size: var(--tui-lockup-size);
  color: inherit;
  text-decoration: none;
}

.tui-lockup-stack {
  flex-direction: column;
  align-items: flex-start;
  gap: calc(var(--tui-lockup-gap) * 0.75);
}

/* A lockup used as a link should not pick up a visited color or an underline
 * from a host page — the mark is not body copy. */
.tui-lockup:hover { opacity: .9; }
.tui-lockup:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
