/* ═════════════════════════════════════════════════════════════════════════════
   MARK · 2 · ATOMS

   The smallest things that carry meaning. An atom knows nothing about where it
   sits - no margins, no widths, no assumptions about a parent. If a rule here
   needs to know about its container, it is a molecule.

   Every atom has: a resting state, hover, focus-visible, and disabled. A
   control missing one of those is unfinished, not minimal.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ───────────────────────────── BUTTON ──────────────────────────────────────
   FILLED MEANS GO. One filled button per screen, and it is the thing the screen
   exists to do. Everything else is outline or quiet. That rule is what makes a
   page scannable in two seconds, and it is the first thing to break when a
   second "primary" is added because someone felt two things mattered. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  min-height: var(--h-control);
  padding: 0 var(--s-5);
  border-radius: var(--r-md);
  font-size: var(--t-md);
  font-weight: var(--w-medium);
  letter-spacing: var(--track-snug);
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background var(--fast) var(--ease),
              border-color var(--fast) var(--ease),
              color var(--fast) var(--ease),
              transform var(--fast) var(--ease);
}
.btn:hover { text-decoration: none; }
/* 1px, not a scale transform. A button that grows on press pushes its
   neighbours; one that sinks reads as a physical key. */
.btn:active { transform: translateY(1px); }
/* Disabled is an EDGE and pale ink, not a grey slab. A filled grey rectangle
   is the loudest thing on a page of white paper, which is the opposite of what
   "you cannot press this" should be. */
.btn[disabled], .btn[aria-disabled="true"] {
  cursor: not-allowed; transform: none; box-shadow: none;
  background: none; border-color: var(--line); color: var(--ink-ghost);
}

.btn--go {
  background: var(--action); color: var(--ink-on-fill);
  box-shadow: var(--lift-go);
}
/* THE COLOUR IS RE-ASSERTED ON HOVER, AND IT IS NOT REDUNDANT.
   base.css sets `a:hover { color: var(--c-teal-700) }` for links in prose. Most
   buttons in this panel are anchors - 8 .btn--go, 33 .btn--line, 15 .btn--quiet -
   and `a:hover` beats a variant that only declares its colour in the resting
   state. So hovering "Join Membership" turned its white label teal on crimson:
   the owner read it as the button greying out.
   `.btn--quiet` below already restates its colour on hover, which is what made
   this a bug in the other three rather than a decision about all four. */
.btn--go:hover { background: var(--action-hover); color: var(--ink-on-fill); }
.btn--go:active { background: var(--action-press); }

/* ─── WEIGHT WITHOUT CRIMSON ─────────────────────────────────────────────────
   Added 2026-08-14. THE GAP, IN THE WORDS CLAUDE.md USES FOR IT: "there is no
   way to emphasise a control without claiming money is at risk." That is the
   file's own example of a real gap rather than a preference, and three screens
   had already paid for it before this existed.

   WHAT IT COST, ALL THREE VERIFIED. `/sandbox/`'s one action was a ghost button
   under a band that outweighed it, and the agent that built it reported the
   defect as unfixable on the page. `/home/` at day zero offers two routes out -
   fund the account, or take a sandbox key and build first - and shipped the
   first in crimson and the second as an outline, so the panel shouted one and
   whispered the other; the owner has since said the quiet one is the route he
   expects serious resellers to take. `/pricing/` links to Membership with
   `btn--line` for the same reason, deliberately, with the reason recorded.

   WHY INK AND NOT A HUE. Every hue in this system is spoken for: crimson is the
   action and money at risk, teal is fine, amber is due, green is money back.
   Filling a button with any of them would spend a state on an action, which is
   rule 2 broken in the component most likely to be copied. Ink is not a state
   and never has been - it is what everything is written in - so a button filled
   with it borrows no meaning at all. It reads as weight and nothing else, which
   is exactly what was missing. --ink on white is 15.92:1 and white on --ink is
   the same figure, so it is the highest-contrast control in the system.

   IT IS NOT A SECOND PRIMARY, AND THIS IS THE WHOLE BOUNDARY:

     · ONE FILLED BUTTON PER SCREEN, unchanged. `.btn--firm` is filled. A screen
       with `.btn--go` on it does NOT also get one - if the screen's decision is
       a purchase, everything else on it is an outline, exactly as before.
       Two filled buttons is the rule this system breaks most easily.
     · NOT FOR ANYTHING THAT SPENDS MONEY. Crimson still means money, and a
       reseller who learns that from four screens and then meets a black button
       that charges them has learned it wrongly. Adding funds, renewing, joining
       a plan, buying a certificate: `.btn--go`.
     · NOT DESTRUCTIVE. That is `.btn--danger`, and it stays outlined.
     · NOT THE NEW DEFAULT FOR A BUTTON SOMEBODY WANTS NOTICED. The reason
       `.btn--line` exists is that most actions on most screens are secondary and
       should look it. If this becomes the answer to "make it stand out", the
       page has three weights again and none of them means anything.

   THE ONE TEST: could this control cost the reader money? Then it is crimson.
   Otherwise, if it is the single thing the screen exists to do, it is firm. */
.btn--firm {
  background: var(--firm); color: var(--firm-ink);
  border-color: var(--firm);
}
/* Restated on hover for the same reason `.btn--go` restates it: most buttons in
   this panel are anchors, and base.css's `a:hover` colour beats a variant that
   only declares its colour at rest - which turned a white label teal on crimson
   and read as the button greying out. */
.btn--firm:hover { background: var(--firm-hover); color: var(--firm-ink);
                   border-color: var(--firm-hover); }
.btn--firm:active { background: var(--firm-press); }
/* NO SHADOW HERE, AND NONE ON `.btn--go` EITHER ANY MORE. This comment used to
   say `--lift-go` was a crimson glow allowed on the primary "because colour that
   is already meaning costs the page nothing" - true while the primary was
   crimson, false since 2026-08-20, when it became the logo's black and the
   designer set --lift-go to `none`. The conclusion it reached still stands and
   now applies to both: a grey shadow under a near-black button is the 2012 admin
   panel this system is built against, and the fill is already the emphasis. */

.btn--line {
  background: var(--surface); color: var(--ink);
  border-color: var(--line-strong);
}
.btn--line:hover { background: var(--tint-cool); border-color: var(--c-slate-400); color: var(--ink); }

.btn--quiet { background: none; color: var(--ink-soft); }
.btn--quiet:hover { background: var(--tint-cool); color: var(--ink); }

/* The only destructive treatment. It is outlined, never filled: a filled
   crimson button that deletes something sits one pixel from a filled crimson
   button that buys something. */
.btn--danger { background: var(--surface); color: var(--action-text); border-color: var(--action-text); }
.btn--danger:hover { background: var(--action-wash); color: var(--action-text); }

.btn--sm { min-height: var(--h-control-sm); padding: 0 var(--s-3); font-size: var(--t-sm); }
.btn--lg { min-height: var(--h-control-lg); padding: 0 var(--s-6); font-size: var(--t-lg); }
.btn--block { width: 100%; }
.btn--icon { padding: 0; width: var(--h-control); }
.btn--icon.btn--sm { width: var(--h-control-sm); }

/* ───────── LINK-ACTION (PROPOSED - DS reconciliation, 2026-08-21) ────────
   A quiet, underlined text action trailing a row's own label - "details",
   "remove" - not a control with its own visible chrome, closer to a link
   than a button. Built on `.btn.btn--quiet.btn--sm` (both already
   declared above) plus an underline and a small left offset, so it reads
   as continuing the preceding text rather than sitting beside it as a
   separate control. Reconciles funds.py's Step 3 "What it covers" row,
   direct-edited 2026-08-21 with two near-identical inline styles
   (`padding:0 0 0 .35rem` for "details", `.5rem` for "remove", both
   `text-decoration:underline`) - unified to one value, `--s-2`, since two
   paddings that differ by less than 2px were never a deliberate design
   decision, just two edits of the same idea. */
.link-action { padding-left: var(--s-2); text-decoration: underline; }

/* ───────────────────────────── FIELD ───────────────────────────────────────
   Inputs are cut INTO the surface, not laid on top of it. A well reads as
   "type here" without a placeholder having to say so.

   16px is not a preference: below it, iOS Safari zooms the viewport on focus,
   and a form that zooms is a form you have to dismiss twice. */
.input, .select, .textarea {
  width: 100%;
  min-height: var(--h-control);
  padding: 0 var(--s-4);
  background: var(--well);
  color: var(--ink);
  border: 1px solid var(--well-edge);
  border-radius: var(--r-md);
  font-size: var(--t-md);
  transition: background-color var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
/* Grows with its content up to a ceiling, then scrolls. A fixed box that
   scrolls internally hides the start of what was just typed; a box that grows
   without limit pushes the submit button off the screen. */
.textarea {
  padding: var(--s-3) var(--s-4); min-height: 96px; max-height: 40vh;
  line-height: var(--lh-body); resize: vertical; field-sizing: content;
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--c-slate-400); }
/* background-COLOR, never the shorthand. The shorthand resets background-image,
   and .select's chevron IS a background-image - so the arrow vanished the
   instant a select took focus, and never existed in the error state at all,
   while padding-right kept holding 40px of empty gutter where it had been.
   In light mode the declaration is a colour no-op (--well and --surface are
   both white); it does real work only under .on-night, which is why the fix is
   the longhand rather than deleting it. */
.input:focus, .textarea:focus, .select:focus {
  background-color: var(--surface); border-color: var(--focus);
}
.input::placeholder, .textarea::placeholder { color: var(--ink-ghost); }
/* An invalid field paints. aria-invalid without a visual is an accessibility
   attribute doing the whole job alone, which means sighted users get nothing -
   the message below the field is not enough to find the field. */
.input[aria-invalid="true"], .textarea[aria-invalid="true"], .select[aria-invalid="true"] {
  border-color: var(--action-text); background-color: var(--action-wash);
}
.input[disabled], .textarea[disabled] { color: var(--ink-ghost); cursor: not-allowed; }
.input--lg { min-height: var(--h-control-lg); font-size: var(--t-lg); padding-inline: var(--s-5); }

/* The select keeps its own chevron rather than the UA's, so it matches the rest
   of the system on every platform instead of on one. */
.select {
  appearance: none;
  padding-right: var(--s-10);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23606A80' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-4) center;
}

.label {
  display: block;
  margin-bottom: var(--s-2);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  color: var(--ink);
}
/* BLOCK, always. As an inline span its margin-top collapsed to nothing and it
   ran straight on from whatever preceded it - "WHMCSRegistrar module, v8.9 and
   up". A help line is a line; it is never mid-sentence. */
.help { display: block; margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--ink-mute); }
.help--bad { color: var(--action-text); }

/* Owner, 2026-08-20, on the board's source citation: "make this more small and
   dull and add a star. just for legal purposes." `.help` is body-adjacent
   supporting text - too loud for boilerplate. `.fine` is the line under it:
   `--t-xs`, the smallest size this scale has, and `--ink-mute` for colour,
   NOT `--ink-ghost` - that token's own comment in tokens.css reserves it for
   placeholders and disabled controls and measures it at 3.07:1, under the
   4.5:1 AA floor for text. A legal line nobody can read is not doing its
   legal job either, so this goes as quiet as the type scale allows and no
   quieter. Same block/margin behaviour as `.help` so it drops in wherever
   `.help` was. The asterisk itself is content the caller writes into the
   string - never a pseudo-element - because a screen reader reads a
   `content:` glyph as decoration and this one is not decorative.

   `.on-night` needs no rule of its own: `--ink-mute` is one of the roles
   `.on-night` re-points (tokens.css), so this already reads 5.31:1 on the
   dark ground without .fine knowing night exists - the whole reason the
   two-layer palette exists.

   A SCREEN NEEDING A "FINE PRINT" TREATMENT NEVER GETS A SECOND `.fine`. A
   panel build (the create-account door, same day) independently needed an
   inline fine-print variant for a footer row and DROPPED it in favour of this
   one instead of forking a second class - the block/margin version above
   already covers a footer row, verified by screenshot rather than diverging.
   That is the standing rule this class enforces, not a one-off: one `.fine`,
   used wherever the treatment is needed, never a page-local twin. */
.fine { display: block; margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--ink-mute); }

/* ───────────────────────────── CHECK & SWITCH ─────────────────────────────
   Both are real inputs, styled. A div pretending to be a checkbox loses the
   label association, the keyboard, and the form. */
/* CHECKBOX *AND* RADIO. The first version scoped nothing to [type], so a radio
   inside .check rendered as a square with a tick - which tells someone they may
   pick several when they may pick exactly one. That is not a styling slip; it
   is the control lying about its own arity.

   Radio is round and its mark is a filled dot, because those two differences
   are the only thing carrying "one of these" versus "any of these", and they
   have to survive being seen at a glance in a column of twelve. */
.check { display: inline-flex; align-items: flex-start; gap: var(--s-3); cursor: pointer; }
.check input {
  appearance: none;
  flex: none;
  width: 20px; height: 20px; margin-top: 2px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
/* A CHECKBOX HAS TO LOOK SQUARE, and at 20px it did not. Corrected
   2026-08-23. `--r-sm` is 8px, which is 40% of a 20-pixel box - enough that
   with anti-aliasing every `.check` in the panel read as a circle, and a
   critic looking at a 52-row multi-select screen reported it as a RADIO, i.e.
   "you may only pick one". The comment three lines above this is the argument
   for treating that as a defect rather than a preference: it says roundness
   and a dot are "the only thing carrying 'one of these' versus 'any of
   these'". That distinction cannot be load-bearing and also be invisible.
   5px keeps the corner soft and unmistakably square. `--r-sm` itself is
   unchanged, because chips and dots want the rounder value; only the box that
   has to be told apart from a circle gets the tighter one. */
.check input:not([type="radio"]) { border-radius: 5px; }
.check input[type="radio"] { border-radius: var(--r-full); }
.check input:checked {
  /* Teal, not crimson. Crimson is the action and money at risk; a row you
     ticked in order to renew it is neither, and a full-page selection drawn in
     crimson turns the table into a wall of alarm. */
  background: var(--calm) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E") center / 14px no-repeat;
  border-color: var(--calm);
}

.switch { display: inline-flex; align-items: center; cursor: pointer; }
.switch input {
  appearance: none;
  position: relative;
  width: 40px; height: 24px;
  background: var(--c-slate-300);
  border-radius: var(--r-full);
  cursor: pointer;
  transition: background var(--fast) var(--ease);
}
.switch input::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px;
  background: var(--surface); border-radius: var(--r-full);
  box-shadow: 0 1px 2px rgba(60, 40, 24, .18);
  transition: transform var(--fast) var(--ease);
}
/* Teal, not crimson. "On" is a state, and crimson in this system means money at
   risk - a portfolio of switches glowing crimson for being correctly configured
   would spend the one alarming colour on good news. */
.switch input:checked { background: var(--calm); }
.switch input:checked::after { transform: translateX(16px); }

/* ───────────────────────────── STATE ───────────────────────────────────────
   FIVE, and a row may only be one. Four carry meaning - calm, good, soon,
   risk - and the fifth is the absence of one, which is a real answer and not a
   missing value: "not configured" is different from "we do not know".

   The dot carries the colour so the label stays readable ink; a whole column of
   coloured words is a column you stop reading. Only RISK colours its label,
   because only risk is asking to be acted on. */
.state {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-sm); color: var(--ink-body); white-space: nowrap;
}
.state::before {
  content: ""; flex: none; width: 8px; height: 8px; border-radius: var(--r-full);
  background: var(--c-slate-300);
}
.state--calm::before { background: var(--calm-fill); }
.state--good::before { background: var(--good-fill); }
.state--soon::before { background: var(--soon-fill); }
.state--risk::before { background: var(--risk-fill); }
/* Only risk colours its label, because only risk is asking to be acted on. */
.state--risk { color: var(--action-text); font-weight: var(--w-medium); }

/* ───────────────────────────── TAG ─────────────────────────────────────────
   A tag is a fact about a thing. It is never a button - if it can be pressed it
   is a filter, and filters live in molecules. */
.tag {
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: 2px var(--s-2);
  border-radius: var(--r-sm);
  font-size: var(--t-xs); font-weight: var(--w-medium);
  background: var(--tint-cool); color: var(--ink-soft);
  border: 1px solid var(--well-edge);
  white-space: nowrap;
}
.tag--good { background: var(--good-wash); color: var(--good-ink); }
.tag--soon { background: var(--soon-wash); color: var(--soon-ink); }
.tag--risk { background: var(--risk-wash); color: var(--risk-ink); }
.tag--calm { background: var(--calm-wash); color: var(--calm-ink); }
/* The one filled tag in the system, for the one offer that is unambiguously
   good news and happens once per account. */
/* THEME-SAFE, and the one-line "use --good-fill" fix does NOT work: --good on
   .on-night is an INK tone (green-300), white on it 1.89:1; --good-fill is
   green-400 at 2.55:1 - both under the 3:1 floor. wash+ink is the system's own
   state-on-a-surface idiom and computes 14.5:1 light / 12.6:1 night. It reads
   quieter than white-on-saturated-green; a bold badge in both themes would need a
   new --good-solid token, which is a bigger change than this bug warrants. */
.tag--offer { background: var(--good-wash); color: var(--good-ink); }

/* ───────────────────────────── MARK ────────────────────────────────────────
   An extension, drawn as the thing it is: a name a business can own. The system
   is named for it.

   Two emphases and not three. A wall of identical marks is a texture; a wall
   with a few lit is a picture; a wall with three levels is a wall nobody can
   read an order out of. */
.mark {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  /* BORDERLESS, WHICH IS WHAT THE RULE BELOW ALREADY SAID.
     Every mark carried `border: 1px solid var(--well-edge)` while the comment
     under it claimed marks are borderless by default and only the exception is
     drawn. So a wall of them read as a keypad of buttons - which is exactly the
     failure the comment was written to prevent, and on the verify screen it put
     eighteen button-shaped non-buttons directly above a code field. The comment
     was right and the code was wrong for as long as both have existed.
     Found by an interaction audit that read the CSS against its own prose. */
  border: 1px solid transparent;
  background: var(--surface);
  color: var(--ink-soft);
  font-family: var(--font-mono);
  font-size: var(--t-sm); font-weight: var(--w-medium);
  letter-spacing: -0.02em;
  white-space: nowrap; text-decoration: none;
}
/* Borderless by default, so a wall of them reads as a field of names rather
   than a keypad of buttons. Only the exception is drawn. */
.mark--free {
  background: var(--good-wash); color: var(--good);
  border-color: color-mix(in srgb, var(--good) 34%, transparent);
  font-weight: var(--w-semi);
}
.mark--more { background: none; color: var(--ink-mute); font-weight: var(--w-regular); }

/* ───────────────────────────── MONEY ───────────────────────────────────────
   ONE RECIPE. The currency mark steps back so the figure reads first, the
   digits are tabular so a column aligns, and there is no second way to render
   an amount anywhere in this system. */
.money { font-variant-numeric: var(--nums); color: var(--ink); white-space: nowrap; }
.money__cur { color: var(--ink-mute); font-weight: var(--w-regular); margin-right: 1px; }
.money--lg { font-size: var(--t-lg); font-weight: var(--w-medium); }
.money--xl { font-size: var(--t-2xl); font-weight: var(--w-semi); letter-spacing: var(--track-tight); }
.money--display { font-size: var(--t-3xl); font-weight: var(--w-semi); letter-spacing: var(--track-tight); }
.money--soft { color: var(--ink-mute); }
.money--good { color: var(--good); }
/* The struck "previous price" state - list price shown, not implied, beside
   the price that replaces it. Same recipe as .hit__was (organisms.css),
   promoted to a .money modifier because a browse-grid card carries two
   prices inline rather than one row's worth of context either side. Not for
   a price that is merely lower (.money--good already reads as "this is the
   good outcome"); this is specifically the number being superseded. */
.money--was { color: var(--ink-mute); text-decoration: line-through;
  text-decoration-color: var(--line-strong); font-weight: var(--w-regular); }
/* THE ONE FILLED MONEY FIGURE. .money--good already says "this is the good
   number" in ink alone - text on the page's own surface. This is for the row
   whose whole job is to report a payoff (a margin, a gap that came back),
   raised off the surface so it reads as the row's answer rather than one more
   line of it. Wash+ink is the system's existing state-on-a-surface recipe
   (.tag--good, .notice--good) - NOT `--good` text on the wash, which is the
   contrast failure .tag--offer's own comment already warns against. Compose
   with `.money--lg` for size; this only adds the fill, and its own
   font-weight (declared after --lg here, so it wins) takes it to semibold -
   a shade past .money--lg's medium, because a filled figure carries more
   visual weight than an inked one at the same size and needs to earn it.
   Reserved for the one payoff a row makes - rule 2, colour always means
   something - so this fires once per row, never on every green figure. */
.money--pill {
  display: inline-block;
  background: var(--good-wash); color: var(--good-ink);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-md);
  font-weight: var(--w-semi);
}

/* ───────────────────────────── ICON ────────────────────────────────────────
   Stroked, currentColor, 1.6 - heavy enough to survive a 14px row, light
   enough not to compete with the text it sits beside. */
.icon { width: 20px; height: 20px; flex: none; stroke: currentColor; stroke-width: 1.6;
        stroke-linecap: round; stroke-linejoin: round; }
.icon--sm { width: 16px; height: 16px; stroke-width: 1.8; }
.icon--lg { width: 24px; height: 24px; }
/* PROPOSED, DS reconciliation pending - the rail's quiet unlock-preview chips
   (organisms.css `.rail__preview__item`) sit at `--t-xs` text; `--icon--sm`
   at 16px overpowered that size once each of the nine names carried its own
   icon, so a step down matches the smallest text token this system already
   has rather than inventing a size with no text to pair with. */
.icon--xs { width: 12px; height: 12px; stroke-width: 2; }

/* ───────────────────────────── EYEBROW ─────────────────────────────────────
   The only place uppercase is allowed, and it is always tracked - uppercase
   without tracking is a word with its letters jammed together. */
.eyebrow {
  font-size: var(--t-xs); font-weight: var(--w-semi);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--ink-mute);
}
.eyebrow--action { color: var(--action-text); }

/* ───────────────────────────── AVATAR / KBD / CODE ─────────────────────── */
/* Five tones, assigned from the name rather than chosen. A customer list where
   every avatar is the same teal is a list you scan by reading; five tones make
   the same list scannable by shape, and a person you deal with twice keeps the
   same colour both times. This is the one place hue is identity, not state -
   which is why the tones are the palette's 100/800 pairs and never the four
   state colours. */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; flex: none;
  border-radius: var(--r-full);
  background: var(--c-teal-100); color: var(--c-teal-800);
  font-size: var(--t-sm); font-weight: var(--w-semi); letter-spacing: 0;
}
.avatar[data-tone="1"] { background: var(--c-teal-100);    color: var(--c-teal-800); }
.avatar[data-tone="2"] { background: var(--c-crimson-100); color: var(--c-crimson-800); }
.avatar[data-tone="3"] { background: var(--c-green-100);   color: var(--c-green-800); }
.avatar[data-tone="4"] { background: var(--c-amber-100);   color: var(--c-amber-700); }
.avatar[data-tone="5"] { background: var(--c-sand-100);   color: var(--c-sand-700); }
.avatar--lg { width: 48px; height: 48px; font-size: var(--t-md); }
.kbd {
  padding: 2px var(--s-2);
  font-family: var(--font-mono); font-size: var(--t-xs);
  color: var(--ink-mute); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-sm);
}
.code {
  font-family: var(--font-mono); font-size: .9em;
  background: var(--tint-cool); color: var(--ink);
  padding: 1px 5px; border-radius: var(--r-sm);
}

/* ───────────────────────────── SPINNER ─────────────────────────────────────
   Only ever shown for something that genuinely takes time. Anything under
   Doherty's 400ms should just happen. */
.spinner {
  width: 16px; height: 16px; flex: none;
  border: 2px solid var(--well-edge);
  border-top-color: var(--calm);
  border-radius: var(--r-full);
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2s; } }

/* ───────────────────────────── CHIP ────────────────────────────────────────
   A FILTER YOU HAVE APPLIED, and can take off.

   Not a tag. A tag is a fact about a thing and is never pressable; a chip is a
   choice YOU made and it always carries the way to undo it. The distinction is
   worth two components because the moment they merge, a table grows a row of
   things that look identical and only half of them do anything.

   It states the field as well as the value - "Customer: Northwind BV", not
   "Northwind BV" - because four chips reading Northwind / .com / 2026 / Off
   is a filter set nobody can reconstruct the meaning of. */
.chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: var(--h-control-sm);
  padding: 0 var(--s-2) 0 var(--s-3);
  border-radius: var(--r-md);
  background: var(--calm-wash); color: var(--calm-ink);
  border: 1px solid color-mix(in srgb, var(--calm) 26%, transparent);
  font-size: var(--t-sm); white-space: nowrap;
}
.chip__key { color: var(--ink-mute); }
.chip__x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: none;
  border-radius: var(--r-sm); color: inherit; opacity: .6;
  transition: background var(--fast) var(--ease), opacity var(--fast) var(--ease);
}
.chip__x:hover { background: color-mix(in srgb, var(--calm) 18%, transparent); opacity: 1; }

/* ───────────────────────────── METER ───────────────────────────────────────
   A quantity against its limit, drawn.

   "1,240 of 5,000 operations" is a sentence you have to do arithmetic on.
   A bar answers "am I near the edge" before you have finished reading it, and
   that is the only question anyone asks of a usage figure.

   It takes a state colour from how full it is, not from a class - so a caller
   cannot draw a 96%-full bar in calm blue. The thresholds are the same ones
   the rest of the system uses for time: fine, soon, at risk. */
.meter {
  display: block; width: 100%; height: 8px;
  border-radius: var(--r-full);
  background: var(--tint-cool); border: 1px solid var(--well-edge);
  overflow: hidden;
}
.meter__fill {
  display: block; height: 100%;
  border-radius: var(--r-full);
  background: var(--calm-fill);
  transition: width var(--base) var(--ease);
}
.meter[data-level="soon"] .meter__fill { background: var(--soon-fill); }
.meter[data-level="risk"] .meter__fill { background: var(--risk-fill); }
.meter[data-level="good"] .meter__fill { background: var(--good-fill); }
.meter--tall { height: 12px; }

/* .meter says WHERE you are (the fill, coloured by data-level). It has no way
   to say what a specific point on the track MEANS - e.g. "everything due
   within 30 days" is a real threshold on the same track as "everything due
   this year", and a reseller reading the bar has no way to see it without a
   mark. (E.2, funds top-up.)

   The mark lives OUTSIDE .meter, in this wrapper, because .meter clips its
   own corners with overflow:hidden - a mark drawn inside it gets clipped at
   the edges. Set --at (a %) once on .meter-wrap; both the tick and its label
   read it, so a caller places one mark in one spot.

   Boundary: for a REAL threshold the data already defines (a sum, a date) -
   never decorative, and never more than one per bar, or the bar stops
   reading as a single line. */
.meter-wrap { position: relative; padding-bottom: 22px; }
.meter__mark {
  /* bottom:22px (not 0) - a % or 100% offset on an absolute child resolves
     against the WRAPPER's padding box (track + the 22px gutter below it),
     so "bottom:0"/"top:100%" land at the bottom of the gutter, not at the
     track's own edge. Subtracting the fixed gutter cancels it back out,
     which is what pins the tick to the track's height and the label just
     under it - for .meter AND .meter--tall alike, without a second rule. */
  position: absolute; top: 0; bottom: 22px; left: var(--at, 50%);
  width: 2px; background: var(--ink); opacity: .35;
}
.meter__mark-label {
  position: absolute; top: calc(100% - 22px); left: var(--at, 50%); margin-top: 4px;
  transform: translateX(-50%);
  font-size: var(--t-sm); color: var(--ink-mute); white-space: nowrap;
}

/* ───────────────────────────── BUTTON SET ──────────────────────────────────
   Two to four mutually exclusive choices, when the choice is a VIEW rather than
   an action - density, a date range, a chart period.

   It is radio behaviour, so it is built from radios: the keyboard gets arrow
   keys for free, the group gets one tab stop instead of four, and a screen
   reader hears "1 of 3" without anything being announced by hand. A row of
   buttons with aria-pressed would need all of that written, and would still be
   four tab stops. */
.btnset {
  display: inline-flex; align-items: center;
  padding: 3px;
  background: var(--tint-cool);
  border: 1px solid var(--well-edge);
  border-radius: var(--r-md);
}
.btnset label {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 32px; padding: 0 var(--s-3);
  border-radius: calc(var(--r-md) - 3px);
  font-size: var(--t-sm); font-weight: var(--w-medium);
  color: var(--ink-mute); cursor: pointer; white-space: nowrap;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.btnset input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.btnset label:hover { color: var(--ink); }
.btnset label:has(input:checked) {
  background: var(--surface); color: var(--ink);
  box-shadow: 0 1px 2px rgba(60, 40, 24, .12);
}
.btnset label:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ───────────────────────────── PRESENCE ────────────────────────────────────
   A person, and whether they are here. Two pixels of humanity for a component
   that is otherwise a coloured circle with initials in it. */
.avatar { position: relative; }
.avatar[data-here]::after {
  content: ""; position: absolute; right: -1px; bottom: -1px;
  width: 11px; height: 11px; border-radius: var(--r-full);
  background: var(--c-slate-300);
  box-shadow: 0 0 0 2px var(--surface);
}
.avatar[data-here="now"]::after { background: var(--good-fill); }
.avatar[data-here="today"]::after { background: var(--soon-fill); }

/* ───────────────────────────── COUNTER ─────────────────────────────────────
   Characters left, and it only appears when it starts to matter. A counter
   showing "0 / 500" the moment a field is focused is a limit announced before
   anyone was near it, which makes the field feel smaller than it is. */
.counter { font-variant-numeric: var(--nums); color: var(--ink-mute); }
.counter[data-near] { color: var(--soon); }
.counter[data-over] { color: var(--action-text); font-weight: var(--w-medium); }

/* ───────────────────────────── FLAG ────────────────────────────────────────
   A language, drawn as its code rather than its flag.

   Flags are countries and languages are not: Portuguese is not Portugal to a
   Brazilian, Spanish is not Spain to a Mexican, and English has no flag at all
   without picking a side. The two-letter code is unambiguous, it is the thing
   in the URL and the API, and it never insults anybody. */
.flag {
  /* inline-flex, so the chip's OWN baseline is its inner text's baseline -
     which is what lets .keyed align it to the label with `align-items:
     baseline` instead of nudging it with a magic translate. */
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 26px; height: 20px; padding: 0 5px; flex: none;
  border-radius: var(--r-sm);
  background: var(--tint-cool); border: 1px solid var(--well-edge);
  font-family: var(--font-mono); font-size: 11px; font-weight: var(--w-semi);
  letter-spacing: 0; color: var(--ink-soft); text-transform: uppercase;
}

/* .flag-emoji - PROPOSED, DS reconciliation pending, onboarding 2026-08-21.
   A COUNTRY, drawn as its actual flag - the opposite case from `.flag` just
   above, and not a conflict with it: `.flag`'s own rule is that a LANGUAGE is
   not a country and should never borrow one's flag, but a country picker is
   squarely the case that rule carves out (`.lang-select__flag`'s own comment
   names the one language exception; this is the country's own class, not a
   reuse of that one). Added for the billing-country combobox
   (screens/funds.py, owner #62) - `font-size: 1.05em` so the glyph reads at
   the same weight as the name beside it, `line-height: 1` so it adds no
   leading of its own, the same two declarations `.lang-select__flag`
   (molecules.css) already uses for the same reason. */
.flag-emoji { font-size: 1.05em; line-height: 1; }

/* ───────────────────────────── KEYED ───────────────────────────────────────
   A SHORT CODE AND THE THING IT NAMES. `EN English`, `INR ₹`, `NL Netherlands`.

   It exists because the pair is a unit and was being assembled by hand every
   time - and assembled wrongly. Two defects, both measured on the language row:

     · THE CHIP CENTRED, IT DID NOT SIT ON THE LINE. A generic flex row uses
       `align-items: center`, which centres a 20px chip against a 17px label's
       LINE BOX rather than its baseline: the chip's own text landed 3.20px
       above the label's. Baseline is correct here because a code chip is read
       as a word, not as an icon, and words sit on the line.

     · THE GAP INSIDE A PAIR EQUALLED THE GAP BETWEEN PAIRS, so five pairs read
       as ten items. Proximity is the only thing binding a code to its label,
       and it has to be the shortest distance in the row. 8px inside against
       24px between - a 3:1 ratio, which is where a group stops being
       ambiguous.

   The chip is `flex: none` so a long label never squeezes it, and the pair does
   not wrap: a code on one line and its language on the next is not a pair. */
.keyed {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  white-space: nowrap;
}
.keyed > .flag { flex: none; }

/* A row OF pairs. The gap here is the one that has to be bigger. */
.keyed-row { display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-6); }

/* ───────────────────────────── STRENGTH ────────────────────────────────────
   How good a password is, and - the part almost every implementation skips -
   WHAT WOULD MAKE IT BETTER.

   A bar that turns from red to green tells someone they have failed without
   telling them how to stop failing, so they add "1!" to the end and the bar
   goes green on a password that is still guessable. The advice line is the
   component; the bar is decoration for it.

   Four segments, not a gradient: a continuous bar invites people to optimise a
   number, and there is no number here worth optimising. */
.strength { display: flex; flex-direction: column; gap: var(--s-2); margin-top: var(--s-2); }
.strength__bars { display: flex; gap: 4px; }
.strength__bars i {
  flex: 1; height: 4px; border-radius: var(--r-full);
  background: var(--well-edge);
  transition: background var(--base) var(--ease);
}
.strength[data-level="1"] i:nth-child(-n+1) { background: var(--risk-fill); }
.strength[data-level="2"] i:nth-child(-n+2) { background: var(--soon-fill); }
.strength[data-level="3"] i:nth-child(-n+3) { background: var(--calm-fill); }
.strength[data-level="4"] i { background: var(--good-fill); }
.strength__say { font-size: var(--t-sm); color: var(--ink-mute); }
.strength__say b { color: var(--ink); font-weight: var(--w-medium); }

/* .gauge - a single continuous length gauge, sibling of .strength. One bar that
   fills with length, not four segments graded against invisible rules. It informs
   (where you stand + a symbol nudge), never blocks. PROPOSED, onboarding 2026-08-21,
   design-system reconciliation pending (built on design-system branch, not yet committed). */
.gauge { margin-top: var(--s-3); display: flex; flex-direction: column; gap: var(--s-2); }
.gauge__track { height: 4px; border-radius: var(--r-full); background: var(--well-edge); overflow: hidden; }
.gauge__fill {
  height: 100%; width: 0%; border-radius: var(--r-full);
  background: var(--risk-fill);
  transition: width var(--base) var(--ease), background var(--base) var(--ease);
}
.gauge__row { display: flex; align-items: baseline; justify-content: space-between; }
.gauge__status { font-size: var(--t-sm); font-weight: var(--w-medium); color: var(--ink); }
.gauge__status--win { color: var(--good); }
.gauge__teach { font-size: var(--t-sm); color: var(--ink-mute); margin: 0; }

/* ───────────────────────────── OTP ─────────────────────────────────────────
   A one-time code, typed in. ONE FIELD, NOT SIX BOXES.

   Six single-character boxes are the fashion and they are worse in every way
   that can be measured. Pasting a code out of a mail client fills the first box
   and silently drops five characters. A screen reader announces six unlabelled
   fields and no relationship between them. Backspacing across them does not work
   at all without JavaScript, so the one interaction everybody performs - I
   mistyped the third one - is the one that needs code to rescue it.

   One input with `autocomplete="one-time-code"` gets the code OFFERED by the
   operating system from the mail or the SMS, which is faster than any number of
   boxes, and paste simply works.

   Mono and tracked because the failure mode here is a transposed or misread
   character, and the whole job of the field is to make that visible before it is
   submitted. Sized to its content: a field the width of the page for six
   characters tells someone the wrong thing about what to type.

   The ADVICE LINE is the component, exactly as it is in .strength above - where
   the code was sent, and what to do when it does not arrive. A field that only
   says "invalid code" has told somebody off. */
.otp { display: flex; flex-direction: column; gap: var(--s-2); }
.otp__in {
  /* 11ch, MEASURED, not guessed. 9ch clipped the sixth digit: six mono
     characters are ~6ch, the .18em tracking adds ~1.8ch more at this size, and
     .input's own horizontal padding takes ~1.6ch on top - about 9.4ch of content
     in a 9ch box, so the field scrolled and the last digit sat behind the border.
     Found by rendering it and looking, which is the only way this class of defect
     has ever been found here; the CSS reads perfectly well either way. */
  /* FULL WIDTH OF ITS MEASURE, NOT 11ch OF IT. 11ch fitted the six characters
     and nothing more, which left the code - the only thing the screen is asking
     for - as a small box with the whole line empty either side of it. The field
     is the subject, so it takes the width and the digits are spread across it;
     the tracking does the counting rather than the border crowding it. */
  width: 100%;
  font-family: var(--font-mono);
  /* THE PLACEHOLDER HAS TO HAVE PRESENCE, NOT JUST POSITION. At --t-2xl the six
     hyphens were 2px tall hairlines covering about 1% of a 504px box - measured -
     so the field read as one wide empty container with a rule floating in it, and
     the box outline was the faintest line on the screen. Bigger type and much
     wider tracking put the marks across the field they are describing, so the
     shape says "six of something go here" before anybody reads the label. */
  /* --t-2xl, NOT --t-3xl. At 44px the type overran --h-control-lg and made the
     field 72.6px tall against the button's 56.4 - a box 29% taller than the
     action, holding 328px of hyphen ink in 33,747px of area, under a token whose
     own comment names "the hero field and its button" as one pair. Measured. */
  font-size: var(--t-2xl);
  letter-spacing: .62em;
  /* The tracking is applied AFTER the last character too, which pulls centred
     text left by half a space. Indenting by the same amount puts it back. */
  text-indent: .62em;
  text-align: center;
}
/* SIX HYPHENS, AND NEVER A PREFILLED CODE.
   The placeholder is the SHAPE of what is wanted - six marks, so the length is
   answered before anybody counts a box - and it has to survive the tracking, so
   it is set here rather than left to a default that renders five faint dots.
   A prefilled code is worse than no help at all: it is the one field on the
   screen whose value cannot be guessed by the product, so a value sitting in it
   is either a lie or somebody else's code. */
/* THE PLACEHOLDER MUST NOT OUT-RANK THE CONSEQUENCE. At --ink-soft the six
   meaningless hyphens measured 6.47:1 while "It expires in 10 minutes" - the only
   dated consequence on the screen - measured 5.23:1 at --ink-mute. There is no ink
   token fainter than mute, and taking a placeholder below 3:1 to win a ranking
   argument would be trading a real defect for a worse one. So the placeholder
   comes DOWN to the same rank as the advice line, and the consequence inside that
   line is raised by weight instead. Equal colour, unequal emphasis. */
.otp__in::placeholder { color: var(--ink-mute); opacity: 1; }
/* The field is the subject of its screen, so its edge is drawn like one. At the
   default input border it was lighter than the placeholder inside it, which is a
   container losing an argument with its own contents. */
/* A REAL BORDER, MEASURED AGAINST THE PAGE. --line-strong resolves to slate-300
   at 1.89:1 - under the 3:1 floor for a meaningful graphic, and lighter than the
   placeholder inside it, which is the exact defect the previous version of this
   comment claimed to have fixed. slate-400 is 2.96:1 and still fails; slate-500
   is 5.22:1. The one control on the screen gets an edge you can see. */
.otp__in { border-color: var(--c-slate-500); }
.otp__say { font-size: var(--t-sm); color: var(--ink-mute); }
.otp__say b { color: var(--ink); font-weight: var(--w-semi); }
.strength[data-level="4"] .strength__say b { color: var(--good); }

/* ───────────────────────────── COPY ────────────────────────────────────────
   For the strings this product is full of and nobody types by hand: auth
   codes, API keys, nameservers, IP addresses.

   It confirms IN PLACE and in words. A copy button that silently succeeds
   leaves the person wondering whether it worked, and one that fires a toast
   sends the confirmation to the opposite corner of the screen from the thing
   they just pressed. */
.copy {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: var(--s-1) var(--s-2); margin: calc(var(--s-1) * -1) 0;
  border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--t-sm);
  color: var(--ink); background: var(--tint-cool);
  border: 1px solid var(--well-edge);
  transition: border-color var(--fast) var(--ease);
}
.copy:hover { border-color: var(--line-strong); }
.copy .icon { color: var(--ink-mute); }
.copy[data-done] { border-color: color-mix(in srgb, var(--good) 45%, transparent); }
.copy[data-done] .icon { color: var(--good); }
/* THE SAME --tint-cool/--surface GAP organisms.css DOCUMENTS ON
   .endpoint__verb - measured there at three points apart on night, close
   enough to read as one continuous dark shape. `.copy` carries the identical
   fill, so any `.copy` a screen puts on a dark surface needs the same escape
   to `--well`. Added 2026-08-18, CLAUDE.md's colour/warmth pass, alongside
   the `.key` fix in organisms.css - the two components share this contract. */
.on-night .copy { background: var(--well); border-color: var(--line-strong); }
.on-night .copy[data-done] { border-color: color-mix(in srgb, var(--good) 45%, transparent); }

/* ───────────────────────────── TIP ─────────────────────────────────────────
   For a fact that clarifies, NEVER for a fact that is required.

   If the interface does not make sense without it, it is not a tooltip, it is
   missing copy - and tooltips do not exist on touch, do not print, and are not
   read aloud in order. The rule this system enforces: a tooltip may be deleted
   without the screen becoming wrong. If it cannot, write it into the page.

   Built on the title-less pattern so it works from the keyboard as well as the
   pointer, and it is aria-describedby rather than a label, because it describes
   rather than names. */
.tip { position: relative; display: inline-flex; }
.tip__say {
  /* CLAMPED TO THE VIEWPORT, in CSS. A tooltip near the left edge rendered at
     -3px on a 390px screen - off the side of the phone, which is the one place
     a tooltip is least likely to be re-openable. `max-width: min(18rem, 90vw)`
     plus a margin keeps it inside without JS measuring anything, and without a
     second positioning system that has to be kept in step with this one. */
  position: absolute; bottom: calc(100% + 8px); left: 50%;
  transform: translateX(-50%) translateY(3px);
  margin-inline: var(--s-3);
  z-index: var(--z-over);
  min-width: 10rem; max-width: min(18rem, calc(100vw - var(--s-8)));
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  background: var(--c-slate-950); color: #E8EAEE;
  font-size: var(--t-sm); line-height: var(--lh-snug); text-align: left;
  box-shadow: var(--lift-2);
  opacity: 0; pointer-events: none;
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.tip__say::after {
  content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px;
  border: 5px solid transparent; border-top-color: var(--c-slate-950);
}
.tip:hover .tip__say, .tip:focus-within .tip__say {
  opacity: 1; transform: translateX(-50%) translateY(0);
}
