/* ============================================================
   Feedback — messages, labels/badges, statistics, error blocks,
   empty states, loading/skeletons
   docs/08-design-system-v2.md §5.3 (badges), §3.3 (semantics), §2 (type scale)
   Message/empty-state rationale: docs/08-design-refresh.md §5.6 / §5.7

   Cascade note that the whole file depends on: App.razor links the
   vendored Fomantic sheet *before* every css/ file we own. A rule here
   at the same specificity as Fomantic's therefore wins on source order
   alone, so !important is never needed — and since TM-167 there is
   none left in this file at all. Where Fomantic outranks us it does it
   by repeating `.ui` in its own selector (`.ui.ui.ui.basic.green.label`
   is 0-6-0); the answer is to meet that weight, not to escalate. The
   label colour block below is written that way and says why.
   ============================================================ */

/* ── Messages ─────────────────────────────────────────────────
   One quiet family, not four coloured ones. Previously each state
   painted its own tinted background, which made every message look
   like a status report — §5.6: the Dashboard's "No upcoming sessions
   scheduled" was blue and "No requirements expiring within 30 days"
   was green, implying a status that does not exist. Now all four share
   the neutral --tm-surface-2 and differ only in a 3px left indicator,
   the text colour, and a leading glyph. */
.ui.message {
    position: relative;
    background: var(--tm-surface-2);
    border: 1px solid var(--tm-border);
    border-radius: var(--tm-radius-sm);
    box-shadow: none;
    /* --tm-text-md, the v2 default. On the renumbered scale --tm-text-base means
       "dense table content" (13px); a message is body text (§2). */
    font-size: var(--tm-text-md);
    line-height: var(--tm-leading-md);
    color: var(--tm-text-secondary);
}

/* Fomantic draws a message's frame as a *coloured inset box-shadow*
   (`box-shadow: 0 0 0 1px #E0B4B4 inset`) declared at .ui.negative.message
   specificity, so the `box-shadow: none` on .ui.message above never reached
   the state variants — the old pink/green/amber rings were still being drawn
   under our own border. Re-stating it at matching specificity removes them. */
.ui.info.message,
.ui.positive.message,
.ui.success.message,
.ui.warning.message,
.ui.negative.message,
.ui.error.message {
    background: var(--tm-surface-2);
    box-shadow: none;
}

/* Info is deliberately colourless. It is the state the empty-state boxes on
   the Dashboard use, and §5.6 is explicit that they must stop implying a
   status. --tm-text-secondary on --tm-surface-2 measures 5.56:1. */
.ui.info.message {
    color: var(--tm-text-secondary);
    border-left: 3px solid var(--tm-text-secondary);
}

/* The three literals that used to sit here (#166534 / #92400e / #991b1b) are
   gone. They existed because the v1 semantic tokens were chosen for solid fills
   and were too light to be read as text — amber-500 could not even reach the
   3:1 non-text floor. §3.3 re-picked all three against BOTH --tm-surface and
   their own tint, so the token is now the text colour and the indicator at once.

   Measured on --tm-surface-2, which is what a message is drawn on:
     --tm-success 4.80:1 ✔ · --tm-warning 4.58:1 ✔ · --tm-danger 6.02:1 ✔ */
.ui.positive.message,
.ui.success.message {
    color: var(--tm-success);
    border-left: 3px solid var(--tm-success);
}

/* The bar may use --tm-warning now. It could not before: amber-500 measured
   2.05:1 on --tm-surface-2, under the 3:1 non-text floor, which is why a
   --tm-warning-strong token was noted as missing. §3.3 darkened the token to
   #9A6206 (4.58:1 here) and the gap closed at the palette instead. */
.ui.warning.message {
    color: var(--tm-warning);
    border-left: 3px solid var(--tm-warning);
}

/* ErrorBoundaryTests.ErrorBoundary_ErrorMessage_ShouldMeetWcagAaContrast reads
   this text and refuses white-on-solid-danger. It used to pin the literal
   rgb(153, 27, 27); it now resolves --tm-danger and compares against the token,
   which is the same change §8 made to ThemeTokenTests and for the same reason —
   the requirement is "dark red on a pale ground, AA", not one particular hex. */
.ui.negative.message,
.ui.error.message {
    color: var(--tm-danger);
    border-left: 3px solid var(--tm-danger);
}

/* ── A message that is not a validation result (TM-143 step 2) ─
   Fomantic treats `.ui.message` inside a `.ui.form` as validation output and
   hides it until the form turns it on:
     .ui.form .info.message, .ui.form .warning.message,
     .ui.form .success.message, .ui.form .error.message { display: none }
   and it is only shown again by `.ui.form.info`, `.ui.form.warning`, … or by a
   `.field.info` ancestor — classes SemForm never sets, because Blazor validation
   does not go through Fomantic's form module. (`.negative` is not on that list,
   which is why the one SemMessage already in UserFormModal works and the others
   would not have.)

   A form can also hold a message that is not a result at all — "an invitation
   email will be sent", "Save is disabled because this user has no role". Those
   used to be hand-written divs with Fomantic's own info/warning palette copied
   into a `style=` attribute (#f8ffff on #a9d5de, #fffaf3 on #c9ba9b), which is
   how they survived the v2 palette and would have stayed lit panels on a dark
   card. Marking them instead lets them be real messages and inherit everything
   above, in both themes.

   0,5,0 against Fomantic's 0,3,0, so no !important. `:not(:empty)` is kept from
   Fomantic's own show rules: an empty message stays hidden, exactly as there. */
.ui.form .ui.message.tm-static-message:not(:empty) {
    display: block;
}

/* WCAG 1.4.1 — with the tinted backgrounds gone, hue is the only thing left
   telling the four states apart, and hue alone is not allowed to carry
   meaning. A leading glyph in the left gutter carries it instead. It is
   absolutely positioned, so it cannot disturb the message's own content flow,
   and it is pseudo-element content, so it stays out of textContent and the
   E2E `:has-text('Trainer is required')` style selectors keep matching. Same
   device, and the same ✓ / ! vocabulary, as .attendance-result-glyph in
   pages.css.

   There used to be a `:not(.icon)` here, skipping Fomantic's icon messages —
   the ones that render a real <i class="icon"> in this same gutter, where a
   second glyph would stack on top of the first. TM-166 removed it, because the
   case it guarded against could not arise. SemMessage only puts `icon` on the
   wrapper when a `Header` *and* an `Icon` are both set, and no call in this
   application sets a Header; the exemption never once fired. Worse, it read as
   proof that icon messages existed and were handled, which is how TM-147 came
   to be filed against an <i> that was never in the DOM. The `Icon=` arguments
   that suggested otherwise are gone from the razor too.

   Removing it also closed a hole rather than opening one, because SemMessage
   decides those two things independently. The wrapper class is set by
   `.If("icon", () => Header != "" && Icon != null)`, which never looks at
   ChildContent; the <i> is drawn only in the ChildContent == null branch. Add
   a Header and an Icon to a message that has child content — the only kind
   this app writes — and you got `.ui.icon.message` with no <i> inside it. The
   old exemption then suppressed the ::before glyph too, leaving that message
   with nothing at all distinguishing it but hue: a silent 1.4.1 regression
   with no visible symptom. Now it keeps the glyph, which is correct, because
   there is no <i> for it to collide with.

   Genuine stacking needs the wrapper class *and* a real <i>, so it needs
   Header + Icon + no child content, i.e. the `Message="…"` parameter form.
   Nothing in this application uses that form. If you introduce it, restore a
   `:not(.icon)` on these rules, and pair the icon with the colour — Warning
   gets a triangle, not an info circle. An icon contradicting its colour
   re-opens the 1.4.1 hole and no rule here will catch it.

   `:not(.attached)` skips messages glued to a panel. Every use of the attached
   variant in this app is the footer strip under the auth card — Login,
   ForgotPassword, ResetPassword — where the message carries no status at all,
   just "Forgot password? | Contact your administrator". A state glyph there
   renders as a lone "i" floating beside the link and reads as a stray character,
   which is the opposite of what this rule is for. */
.ui.info.message:not(.attached),
.ui.positive.message:not(.attached),
.ui.success.message:not(.attached),
.ui.warning.message:not(.attached),
.ui.negative.message:not(.attached),
.ui.error.message:not(.attached) {
    padding-left: var(--tm-space-6);
}

.ui.info.message:not(.attached)::before,
.ui.positive.message:not(.attached)::before,
.ui.success.message:not(.attached)::before,
.ui.warning.message:not(.attached)::before,
.ui.negative.message:not(.attached)::before,
.ui.error.message:not(.attached)::before {
    position: absolute;
    left: var(--tm-space-3);
    top: var(--tm-space-3);
    width: var(--tm-space-4);
    text-align: center;
    font-weight: var(--tm-weight-semibold);
    line-height: var(--tm-leading-base);
}

.ui.info.message:not(.attached)::before {
    content: "i";
}

.ui.positive.message:not(.attached)::before,
.ui.success.message:not(.attached)::before {
    content: "\2713"; /* ✓ */
}

.ui.warning.message:not(.attached)::before {
    content: "!";
}

.ui.negative.message:not(.attached)::before,
.ui.error.message:not(.attached)::before {
    content: "\2715"; /* ✕ */
}

/* ── Badges (§5.3) ────────────────────────────────────────────
   A status badge is a SOFT TINT PLUS A DOT, not a saturated pill: the tint is
   --tm-*-light, the label and the dot are --tm-*. A solid fill is reserved for
   the genuinely exceptional, which in this application means nothing on a list
   screen. That is the sentence §5.3 actually contains, and it is why every
   foreground token in §3.3 was picked to clear AA on its own tint as well as on
   --tm-surface — a tinted badge only works if the text on the tint is legible.

   Measured, foreground on its own tint:
     --tm-success on --tm-success-light  4.67:1 ✔ AA
     --tm-warning on --tm-warning-light  4.56:1 ✔ AA
     --tm-danger  on --tm-danger-light   5.81:1 ✔ AA
     --tm-primary on --tm-primary-light  5.17:1 ✔ AA
     --tm-text-secondary on --tm-surface-2  5.56:1 ✔ AA   (the neutral badge)

   THE CLASS NAMES ARE FOMANTIC'S, UNCHANGED. The razor markup is not touched by
   this file and E2E fixtures select by colour class, so the mapping happens
   here — the same device the button roles use:

     .ui.green .positive          → success   tint + dot
     .ui.red .negative            → danger    tint + dot
     .ui.yellow .orange           → warning   tint + dot
     .ui.blue .primary .teal      → primary   tint + dot
     plain .ui.label, .ui.grey    → neutral   tint, no dot

   Fomantic's own label sizing is what made the Status page unreadable: the
   primary data of that screen (`52`, `0`) rendered through `.ui.mini.label` at
   0.643rem ≈ 9px. One badge size for the whole application, --tm-text-sm. */
.ui.label {
    display: inline-flex;
    align-items: center;
    /* justify-content matters for exactly one variant: Fomantic gives
       `.ui.circular.label` a min-width of 2em and centres its digit with
       `text-align`, which inline-flex ignores. */
    justify-content: center;
    gap: var(--tm-space-1);
    border: none;
    border-radius: var(--tm-radius-sm);
    background: var(--tm-surface-2);
    color: var(--tm-text-secondary);
    font-weight: var(--tm-weight-semibold);
    font-size: var(--tm-text-sm);
    line-height: var(--tm-leading-sm);
    font-variant-numeric: tabular-nums;
    padding: var(--tm-space-1) var(--tm-space-2);
}

/* Semibold rather than the mock-up's medium, and deliberately so: the
   attendance result labels (TM-31) have to be readable from across a classroom
   and the badge is only 12px. The weight is the cheapest way to keep that
   without breaking the one-size rule. */

/* Every size class restated at Fomantic's own three-class specificity so
   `<span class="ui mini label">` and `<span class="ui label">` render
   identically. The markup keeps whatever size word it has; a razor sweep can
   drop them later without changing anything visually. */
.ui.mini.label,
.ui.tiny.label,
.ui.small.label,
.ui.large.label,
.ui.big.label,
.ui.huge.label {
    font-size: var(--tm-text-sm);
    line-height: var(--tm-leading-sm);
    /* Padding has to come along, not just the type: Fomantic sizes it per
       variant at this same three-class specificity, so leaving it behind would
       give two badges the same text at two different heights — which is the
       §3.4 audit finding this rule exists to close. `.ui.circular.label` keeps
       its own, Fomantic declares that one !important. */
    padding: var(--tm-space-1) var(--tm-space-2);
}

/* The dot. It is a pseudo-element, so it stays out of textContent and the E2E
   `:has-text('Overdue')` style selectors keep matching, and it is drawn in
   `currentColor`, so it can never drift from the label it belongs to.

   WCAG 1.4.1 is the reason it exists at all: with the saturated fills gone, hue
   would otherwise be the only thing separating one badge from another, and hue
   alone may not carry meaning. The dot plus the word does.

   `.circular` is excluded because those are counts, not states (see below), and
   `.pointing` because that variant is a form-validation callout with an arrow —
   a dot in front of "Current password is required" reads as a bullet. */
.ui.green.label:not(.circular):not(.pointing)::before,
.ui.positive.label:not(.circular):not(.pointing)::before,
.ui.red.label:not(.circular):not(.pointing)::before,
.ui.negative.label:not(.circular):not(.pointing)::before,
.ui.yellow.label:not(.circular):not(.pointing)::before,
.ui.orange.label:not(.circular):not(.pointing)::before,
.ui.blue.label:not(.circular):not(.pointing)::before,
.ui.primary.label:not(.circular):not(.pointing)::before,
.ui.teal.label:not(.circular):not(.pointing)::before {
    content: "";
    flex: none;
    width: var(--tm-space-1);
    height: var(--tm-space-1);
    border-radius: 50%;
    background: currentColor;
}

/* ── Badge colours — DO NOT EDIT WITHOUT RE-MEASURING ─────────
   Two E2E fixtures pin these: SessionStatusContrastTests (orange/Overdue) and
   SessionAttendanceAccessibilityTests (yellow "Pending", green "Passed"), both
   asserting >= 4.5:1. The ratios are listed at the top of this section.

   THE `.ui` IS REPEATED FOUR TIMES ON PURPOSE, AND IT REPLACES SIXTEEN
   `!important` (TM-167). This block used to carry one on every declaration, and
   the reason was never a missing token — the tokens are all here. It was pure
   specificity. Fomantic writes its colour variants with `.ui` tripled:

     .ui.ui.ui.green.label                     0-5-0   the solid fill
     .ui.ui.ui.basic.green.label               0-6-0   the outlined variant

   and the application renders the second one — the "reg." counter on /status is
   `ui blue basic label mini`, and every validation callout is
   `ui pointing red basic label`. A rule of ours written `.ui.green.label` is
   0-3-0 and loses to both, so the redesign bought its way past them with
   `!important`, which then had to be repeated on every declaration in the block
   and on `.ui.circular.label` below just to stay in the argument.

   Four `.ui` makes this 0-6-0 — level with the strongest of them — and this sheet
   is linked after semantic.min.css, so a tie falls to us. Nothing here needs
   `!important` any more, and neither does anything added to it.

   IT IS UGLY AND THAT IS THE CHEAPER OF TWO UGLINESSES. The alternative measured
   for TM-167 was a cascade layer around Fomantic, which settles this whole class
   of fight by origin instead of by selector length. It works, and it is the right
   destination, but it also inverts every place the redesign leans on Fomantic
   being MORE specific — there are 34 such comments in these stylesheets — and a
   measured before/after of computed styles turned up seven live regressions the
   983-test suite did not catch. The finding is written up in
   docs/08-design-system-v2.md §4.2; the spike is on
   `spike/2026-08-25-cascade-layer-measured`.

   IF YOU ADD A COLOUR HERE, GIVE IT FOUR `.ui` TOO. Three is 0-5-0 and beats only
   the solid variant; the outlined one will silently keep Fomantic's #21BA45. */
.ui.ui.ui.ui.green.label,
.ui.ui.ui.ui.positive.label {
    background: var(--tm-success-light);
    color: var(--tm-success);
    border: none;
}

.ui.ui.ui.ui.red.label,
.ui.ui.ui.ui.negative.label {
    background: var(--tm-danger-light);
    color: var(--tm-danger);
    border: none;
}

/* Blue and teal were two more accents with no meaning of their own (§1); they
   collapse onto the brand tint, the same way the button roles collapse. */
.ui.ui.ui.ui.blue.label,
.ui.ui.ui.ui.primary.label,
.ui.ui.ui.ui.teal.label {
    background: var(--tm-primary-light);
    color: var(--tm-primary);
    border: none;
}

/* Yellow and orange both mean "attention, not yet failed" — Pending, Expiring,
   Overdue — so they share one warning tint instead of being two near-identical
   ambers. Without this override .ui.orange.label falls back to Fomantic's
   #F2711C with white text (2.94:1). */
.ui.ui.ui.ui.yellow.label,
.ui.ui.ui.ui.orange.label {
    background: var(--tm-warning-light);
    color: var(--tm-warning);
    border: none;
}

.ui.ui.ui.ui.grey.label {
    background: var(--tm-surface-2);
    color: var(--tm-text-secondary);
    border: none;
}

/* ── Counts as numbers, not chips (§5.3) ──────────────────────
   On /status the primary data of the whole screen renders as
   `<span class="ui red label circular mini">52</span>` — a number dressed as a
   coloured pill — and the Dashboard repeats it twenty times. Removing the pill
   needs the razor to stop emitting a label; what this file can do is stop the
   fill from competing with the numeral. One quiet neutral tint for all of them,
   and the colour that carries meaning moves entirely into the digits, which is
   what §5.3 asks for.

   The tint is --tm-surface-2 and NOT --tm-border-light, which is a measured
   choice rather than a stylistic one: on --tm-border-light a warning-coloured
   count measures 4.46:1 and misses the AA floor by a hair. On --tm-surface-2
   every digit colour clears it —
     --tm-danger 6.02:1 · --tm-warning 4.58:1 · --tm-success 4.80:1
     --tm-primary 5.32:1 · --tm-text-secondary 5.56:1

   FOUR `.ui` FOR THE SAME REASON AS THE COLOUR BLOCK ABOVE, and one more besides.
   A count is `<span class="ui red label circular mini">`, so it is matched by the
   colour block too; this rule has to be at least as specific as that block *and*
   come after it, or the count keeps the red tint instead of the neutral one. Both
   are 0-6-0 now and this one is second, which is what settles it — the pair used
   to be held together by `!important` on both sides. Keep it below. */
.ui.ui.ui.ui.circular.label {
    background: var(--tm-surface-2);
    font-variant-numeric: tabular-nums;
}

/* `.tm-count` is what the razor emits where the pill has already been removed:
   the number, in the text colour, in tabular figures. Put it on the <td> or on
   a <span> inside it, and pair it with `.tm-num` from tables.css on both the
   <th> and the <td> when the column is numeric.

   WCAG 1.4.1: a coloured count is a count the reader is meant to act on, so the
   cell also needs a word — the column header, a badge beside it, or a `title`.
   Do not ship a red number as the only signal. */
.tm-count {
    font-variant-numeric: tabular-nums;
    font-weight: var(--tm-weight-medium);
    color: var(--tm-text);
}

.tm-count--attention {
    color: var(--tm-danger);
    font-weight: var(--tm-weight-semibold);
}

/* A zero is data, not an absence: UiConsistencyTests pins `0` rendering as `0`
   rather than a dash. --tm-text-secondary is 6.18:1 on --tm-surface. */
.tm-count--zero {
    color: var(--tm-text-secondary);
    font-weight: var(--tm-weight-normal);
}

/* Pending is the only state that still demands an action, so it is the most
   conspicuous of the scale. The ring is drawn in the badge's own --tm-warning
   (it used to be a #ca8a04 literal), and .attendance-result-glyph in
   css/pages/sessions.css carries the same meaning as a glyph, so the pair
   satisfies WCAG 1.4.1 without relying on the ring's hue. */
.ui.label[data-result="Pending"] {
    box-shadow: inset 0 0 0 1px var(--tm-warning);
}

/* ── Statistics ───────────────────────────────────────────────
   §2 retires weights 700 and 800 from the system; the value used to be 800,
   which is the loudest thing on the Dashboard. Semibold is the ceiling. */
.ui.statistic > .value {
    font-size: var(--tm-text-2xl);
    line-height: var(--tm-leading-2xl);
    font-weight: var(--tm-weight-semibold);
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    color: var(--tm-text);
}

/* Fomantic sizes the value per statistic size at four-class specificity, so
   the scale above has to be restated for the two sizes the app actually uses
   (`mini` on the Dashboard KPI strip, `small` on the four-up grid). Colour is
   deliberately NOT set here: it would tie with .ui.<colour>.statistic > .value
   on specificity and, being later, would silently erase the red/green/blue
   value colours below. */
.ui.mini.statistic > .value,
.ui.small.statistic > .value {
    font-size: var(--tm-text-2xl);
    line-height: var(--tm-leading-2xl);
}

/* Uppercase + .06em letter-spacing on a 12px label is the same 2014 tic §5.2
   removes from table headers. Plain sentence case at --tm-text-sm instead.
   --tm-text-muted is not an option here: 2.55:1 on --tm-surface, under the AA
   4.5:1 floor for normal text. --tm-text-secondary measures 6.18:1 and is the
   quietest token that actually passes. */
.ui.statistic > .label,
.ui.mini.statistic > .label,
.ui.small.statistic > .label {
    color: var(--tm-text-secondary);
    font-weight: var(--tm-weight-medium);
    font-size: var(--tm-text-sm);
    line-height: var(--tm-leading-sm);
    text-transform: none;
    letter-spacing: normal;
    margin-top: var(--tm-space-2);
}

.ui.red.statistic > .value {
    color: var(--tm-danger);
}

.ui.yellow.statistic > .value {
    color: var(--tm-warning);
}

.ui.blue.statistic > .value {
    color: var(--tm-primary);
}

.ui.green.statistic > .value {
    color: var(--tm-success);
}

/* ── Error blocks ─────────────────────────────────────────────
   §3d: these join the message family above rather than inventing a third
   look. .blazor-error-boundary used to be white text on a solid --tm-danger
   fill — the exact white-on-danger pattern ErrorBoundaryTests warns against.
   It is Blazor's built-in fallback and this app never renders it (AppError-
   Boundary always supplies its own ErrorContent), but leaving it as the one
   loud red slab in the system is how it creeps back. --tm-danger on
   --tm-surface-2 measures 6.02:1. */
.blazor-error-boundary {
    background: var(--tm-surface-2);
    border: 1px solid var(--tm-border);
    border-left: 3px solid var(--tm-danger);
    border-radius: var(--tm-radius-sm);
    color: var(--tm-danger);
    padding: var(--tm-space-4) var(--tm-space-5);
    margin: var(--tm-space-4);
    font-weight: var(--tm-weight-medium);
}

.blazor-error-boundary::after {
    content: "An error has occurred.";
}

.tm-error-boundary {
    padding: var(--tm-space-4);
}

.tm-error-boundary .ui.message {
    max-width: 800px;
    margin: var(--tm-space-6) auto;
}

.tm-error-details {
    margin-top: var(--tm-space-4);
    padding: var(--tm-space-4);
    background: var(--tm-surface-2);
    border-radius: var(--tm-radius-sm);
    border: 1px solid var(--tm-border);
}

/* The stack trace has to stay readable — it is the whole point of the block.
   --tm-text on --tm-surface measures 18.32:1. The family comes from
   --tm-font-mono; the hardcoded 'JetBrains Mono', 'Fira Code' stack it used to
   name is the same defect as a hardcoded colour, and §2 puts identifiers, codes
   and traces in one mono face. */
.tm-error-details pre {
    margin: var(--tm-space-2) 0 0 0;
    padding: var(--tm-space-3);
    background: var(--tm-surface);
    border: 1px solid var(--tm-border-light);
    border-radius: var(--tm-radius-sm);
    color: var(--tm-text);
    font-size: var(--tm-text-xs);
    line-height: var(--tm-leading-base);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    font-family: var(--tm-font-mono);
}

.tm-error-details details {
    margin-top: var(--tm-space-2);
}

/* Not --tm-text-muted, which measures 2.41:1 on --tm-surface-2 — under the
   WCAG AA 4.5:1 floor for what is an interactive disclosure control.
   --tm-text-secondary measures 5.56:1, and --tm-text on hover 16.48:1. */
.tm-error-details summary {
    cursor: pointer;
    color: var(--tm-text-secondary);
    font-weight: var(--tm-weight-medium);
    transition: color var(--tm-transition);
}

.tm-error-details summary:hover {
    color: var(--tm-text);
}

.tm-error-actions {
    margin-top: var(--tm-space-4);
    display: flex;
    gap: var(--tm-space-2);
}

/* ── Loading states ───────────────────────────────────────────
   §5.6 wants a skeleton matching the shape of the content; that needs a razor
   primitive and is phase 2's shared-component work, not this file's. What is
   removed here is the part §5.6 names directly: the backdrop-filter blur that
   smeared the stale content behind the veil. The veil itself is derived from
   --tm-surface via color-mix rather than a hardcoded white, so it follows the
   dark theme instead of flashing white in it. */
.tm-loading {
    position: relative;
    min-height: 100px;
}

.tm-loading::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: color-mix(in srgb, var(--tm-surface) 85%, transparent);
    border-radius: var(--tm-radius-md);
}

/* Loading spinner override */
.ui.loader::after {
    border-color: var(--tm-primary) transparent transparent;
}
