Exclusivebeta

Trust Ladder <mv-trust-ladder>

Graduated autonomy earned from a track record, for AI agents, automations, integrations and new team members alike. Every capability (“Refund customers”, “Deploy to staging”, “Approve expenses”) is a row of a trust matrix whose rising rungs show its level on its own ladder (Suggest only → Act with approval → Act and report → Act silently under $50), and each level states what it takes to get there: so many approved in a row, a minimum number of actions, an approval rate, days at the current level, reverted actions allowed. Opening a row shows the ladder as a staircase with the current step, a checklist of the next level’s criteria with values and bars (“14 of 20”, “9 of 14 days”) plus the plain-language remainder (“Still needed: 6 more approved in a row and 5 more days”), a track-record strip whose unbroken tail is the streak, a tally and a table of recent actions with their outcome (approved, kept, edited, rejected, reverted) and reviewer. record() feeds outcomes in; a reverted action immediately suggests stepping down (“Step down to Act with approval until the record is clean again?”) or, with auto-demote, does it; “Keep level, restart count” resets the window instead. Promotion is always a person’s decision with require-owner (only the capability’s owner is offered it, never automatic), and “Promote now” before the record is complete asks for confirmation and lists what is missing. mv-promote and mv-demote are cancelable and take waitUntil(promise) for your server: the component recommends, your app enforces. Compact view for dashboards; every text overridable.

CategoryData display
TypeWeb Component (<mv-trust-ladder>)
Statusbeta
KitAI you can check
Also installsbutton
Keywordsexclusive, culture, autonomy, trust, ai-agent, agent, automation, human-in-the-loop, approval, permissions, graduated, track-record, promotion, demotion, governance, onboarding, matrix, ladder

When to use

  • An AI agent or automation should earn more autonomy per task from its approved, edited and reverted actions
  • A settings page must show what an assistant may do on its own today and what it takes to unlock the next level
  • New team members or junior approvers get wider rights step by step, based on a reviewed track record
  • A reverted action by a bot or integration should immediately prompt a human to reduce its autonomy

Avoid when

  • Each critical action needs a second person to co-sign it, whatever the track record → use Two-Key instead
  • A few one-off exceptions to a rule are budgeted per period rather than earned over time → use Pass Card instead
  • Rights are static roles or permissions set by an admin, with no track record behind them: a plain settings table is clearer

Install

node scripts/add.mjs trust-ladder --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["trust-ladder"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/trust-ladder/trust-ladder.js, components/trust-ladder/trust-ladder.css, components/button/button.css.

Usage

Quick start, the smallest working markup:

<mv-trust-ladder label="Support assistant">
  <script type="application/json">{ "capabilities": [{ "id": "refunds", "label": "Refund customers", "level": "approve", "since": "-12d",
    "evidence": [{ "at": "-2h", "title": "Refund $42 · Order 4821", "outcome": "approved" }, { "at": "-1d", "title": "Refund $18 · Order 4790", "outcome": "reverted" }] }] }</script>
</mv-trust-ladder>

Canonical markup, to start from and customize with attributes, data-* and CSS variables:

<div id="tl-demo" style="width:min(100%,62rem);margin-inline:auto">
  <style>
    #tl-demo { display:grid; gap:1.25rem; align-content:start }
    #tl-demo .tl-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap }
    #tl-demo .tl-kicker { margin:0; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #tl-demo .tl-top h3 { margin:.125rem 0 0; font-size:1.125rem; letter-spacing:-.01em }
    #tl-demo .tl-top p.tl-sub { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #tl-demo .tl-grid { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1.25rem; align-items:start }
    #tl-demo .tl-panel { display:grid; gap:.875rem; padding:1rem 1.125rem 1.125rem; border:1px dashed var(--mv-border-strong); border-radius:var(--mv-radius-xl) }
    #tl-demo .tl-panel h4 { margin:0; font-size:.8125rem; font-weight:600 }
    #tl-demo .tl-row { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #tl-demo .tl-row > span { color:var(--mv-fg-muted); font-size:.75rem; margin-inline-end:.25rem }
    #tl-demo .tl-panel .mv-choice { font-size:.8125rem }
    #tl-demo .tl-log { margin:0; min-height:2.5em; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
    #tl-demo .tl-side { display:grid; gap:.5rem }
    #tl-demo .tl-side p { margin:0; color:var(--mv-fg-muted); font-size:.75rem }
    @media (max-width:48rem) { #tl-demo .tl-grid { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="tl-top">
    <div>
      <p class="tl-kicker">Helpdesk · Automation settings</p>
      <h3>What the support assistant may do on its own</h3>
      <p class="tl-sub">Autonomy is earned per capability from its track record. Your app applies the level.</p>
    </div>
  </div>

  <!-- Main: an AI support assistant, four capabilities, refunds expanded -->
  <mv-trust-ladder id="tl-main" label="Support assistant" user="Priya Nair" require-owner evidence-limit="5"></mv-trust-ladder>

  <div class="tl-grid">
    <section class="tl-panel" aria-label="Demo controls">
      <h4>Try it on “Refund customers”</h4>
      <div class="tl-row">
        <span>Next refund:</span>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tl-approve">Approved</button>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tl-edit">Edited</button>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tl-revert">Reverted</button>
      </div>
      <div class="tl-row">
        <button type="button" class="mv-button" data-variant="secondary" data-size="sm" id="tl-week">Skip ahead a week</button>
        <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="tl-reset">Reset demo</button>
      </div>
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="tl-owner" checked> Viewing as the owner, Priya Nair</label>
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="tl-fail"> Server rejects the next change</label>
      <p class="tl-log" id="tl-log" aria-live="polite">Add a few approved refunds, then promote.</p>
    </section>

    <!-- Compact: a new team member's approval rights, same mechanic -->
    <div class="tl-side">
      <mv-trust-ladder id="tl-compact" compact label="Elena Petrova · Junior approver" user="Tomás Rivera" require-owner>
        <script type="application/json">{
          "levels": [
            { "id": "shadow", "label": "Shadow review", "description": "Reviews alongside a senior approver. Decides nothing." },
            { "id": "co", "label": "Co-approve", "description": "Approves together with a second approver.", "criteria": { "actions": 10, "maxReverted": 0 } },
            { "id": "solo", "label": "Approve up to $1,000", "description": "Approves alone up to $1,000.", "criteria": { "streak": 15, "days": 30, "maxReverted": 0 } },
            { "id": "senior", "label": "Approve up to $10,000", "description": "Approves alone up to $10,000.", "criteria": { "streak": 40, "days": 90, "approvalRate": 0.97, "maxReverted": 0 } }
          ],
          "capabilities": [
            { "id": "expenses", "label": "Expense reports", "owner": "Tomás Rivera", "level": "co", "since": "-41d",
              "evidence": [
                { "at": "-4h", "title": "Expense report · Lisbon offsite · $812.40", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-1d", "title": "Expense report · Client dinner · $236.00", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-3d", "title": "Expense report · Conference pass · $699.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-5d", "title": "Expense report · Taxi receipts · $84.10", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-7d", "title": "Expense report · Laptop stand · $59.99", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-9d", "title": "Expense report · Team lunch · $148.25", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-12d", "title": "Expense report · Train to Lyon · $118.00", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-14d", "title": "Expense report · Coworking day pass · $35.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-17d", "title": "Expense report · Hotel, Berlin · $412.80", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-19d", "title": "Expense report · Software license · $240.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-22d", "title": "Expense report · Parking · $27.50", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-25d", "title": "Expense report · Flight to Toronto · $684.20", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-28d", "title": "Expense report · Printer ink · $46.99", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-31d", "title": "Expense report · Client gift · $75.00", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-34d", "title": "Expense report · Mileage · $92.40", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-38d", "title": "Expense report · Headset · $129.00", "outcome": "approved", "by": "Tomás Rivera" }
              ] },
            { "id": "invoices", "label": "Vendor invoices", "owner": "Tomás Rivera", "level": "co", "since": "-12d",
              "evidence": [
                { "at": "-1d", "title": "Invoice INV-7731 · Cloud hosting · $940.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-3d", "title": "Invoice INV-7702 · Cleaning services · $610.00", "outcome": "edited", "by": "Tomás Rivera", "note": "Wrong cost center" },
                { "at": "-6d", "title": "Invoice INV-7688 · Office supplies · $182.35", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-9d", "title": "Invoice INV-7654 · Translation · $420.00", "outcome": "approved", "by": "Tomás Rivera" }
              ] },
            { "id": "travel", "label": "Travel requests", "owner": "Tomás Rivera", "level": "shadow", "since": "-6d",
              "evidence": [
                { "at": "-2d", "title": "Travel · Madrid, 3 nights", "outcome": "approved", "by": "Grace Liu" }
              ] }
          ]
        }</script>
      </mv-trust-ladder>
      <p>Compact view: the same ladder for a new team member’s approval rights. Open a row for details.</p>
    </div>
  </div>

  <script type="module">
    const el = document.getElementById("tl-main");
    const $ = (id) => document.getElementById(id);
    const log = $("tl-log");
    const H = 36e5, D = 864e5;
    const reviewers = ["Daniel Kim", "Sofia Rossi", "Kenji Watanabe", "Lucas Moreau"];
    const reasons = ["damaged in transit", "late delivery", "wrong size", "duplicate charge", "missing item", "not as described"];
    let order = 48213;
    const amount = (i) => ((i * 37) % 180 + 12 + ((i * 13) % 100) / 100).toFixed(2);
    const refund = (i, at, outcome, extra = {}) => ({
      at, outcome, by: reviewers[i % reviewers.length],
      title: `Refund $${amount(i)} · Order #${order - i * 7} · ${reasons[i % reasons.length]}`, ...extra,
    });
    const kept = (label, i, at, extra = {}) => ({ at, outcome: "kept", title: label(i), ...extra });

    const replyLevels = [
      { id: "suggest", label: "Suggest only", description: "Drafts a reply for an agent to send." },
      { id: "approve", label: "Act with approval", description: "Sends once an agent approves each reply.", criteria: { streak: 10, maxReverted: 0 } },
      { id: "report", label: "Act and report", description: "Sends on its own and logs every reply for review.", criteria: { streak: 20, days: 14, maxReverted: 0 } },
      { id: "silent", label: "Act silently", description: "Sends on its own. Weekly spot checks.", criteria: { streak: 50, days: 30, maxReverted: 0 } },
    ];

    const initial = () => {
      const now = Date.now();
      // Refunds: 14 approved in a row since an edit, 9 days at "Act with approval".
      const refunds = [];
      for (let i = 0; i < 14; i++) refunds.push(refund(i, now - (3 + i * 14) * H, "approved"));
      refunds.push(refund(14, now - 8 * D, "edited", { note: "Amount lowered to $32.00: shipping isn’t refundable" }));
      refunds.push(refund(15, now - 8.6 * D, "approved"));
      for (let i = 16; i < 26; i++) refunds.push(refund(i, now - (10 + (i - 16)) * D, "approved"));
      const replies = [];
      const ticket = (i) => `Reply to ticket #${91540 - i * 3} · ${["order status", "return label", "password reset", "invoice copy", "delivery window"][i % 5]}`;
      for (let i = 0; i < 52; i++) replies.push(kept(ticket, i, now - (2 + i * 13) * H));
      const routes = [];
      const route = (i) => `Tag and route ticket #${91602 - i * 2} → ${["Billing", "Shipping", "Returns", "Accounts"][i % 4]}`;
      for (let i = 0; i < 24; i++) routes.push(kept(route, i, now - (1 + i * 6) * H));
      const credits = [];
      const credit = (i) => `Store credit $${(15 + (i * 11) % 60).toFixed(2)} · Account #${20931 + i * 17}`;
      credits.push({ at: now - 20 * H, outcome: "reverted", title: "Store credit $120.00 · Account #20931", by: "Lucas Moreau", note: "Duplicate: the customer was already credited by phone" });
      for (let i = 1; i < 10; i++) credits.push(kept(credit, i, now - (1 + i) * D));
      return [
        {
          id: "refunds", label: "Refund customers", owner: "Priya Nair", level: "approve", since: now - 9 * D, open: true, evidence: refunds,
          levels: [
            { id: "suggest", label: "Suggest only", description: "Drafts the refund. An agent issues it." },
            { id: "approve", label: "Act with approval", description: "Issues a refund once an agent approves it.", criteria: { streak: 10, maxReverted: 0 } },
            { id: "report", label: "Act and report", description: "Issues refunds under $200 and posts each one for review.", criteria: { streak: 20, days: 14, maxReverted: 0 } },
            { id: "silent", label: "Act silently under $50", description: "Issues small refunds without a report. Weekly spot checks.", criteria: { streak: 50, days: 30, approvalRate: 0.98, maxReverted: 0 } },
          ],
        },
        { id: "replies", label: "Reply to customers", owner: "Priya Nair", level: "report", since: now - 32 * D, levels: replyLevels, evidence: replies },
        { id: "routing", label: "Tag and route tickets", owner: "Amara Okafor", level: "silent", since: now - 61 * D, levels: replyLevels, evidence: routes },
        { id: "credit", label: "Issue store credit", owner: "Priya Nair", level: "report", since: now - 12 * D, levels: replyLevels, evidence: credits },
      ];
    };

    let n = 30;
    const reset = () => { n = 30; el.capabilities = initial(); log.textContent = "Add a few approved refunds, then promote."; };
    await customElements.whenDefined("mv-trust-ladder");
    reset();

    $("tl-approve").addEventListener("click", () => el.record("refunds", refund(n++, Date.now(), "approved")));
    $("tl-edit").addEventListener("click", () => el.record("refunds", refund(n++, Date.now(), "edited", { note: "Agent changed the amount before approving" })));
    $("tl-revert").addEventListener("click", () => el.record("refunds", refund(n++, Date.now(), "reverted", { note: "The card issuer had already refunded the customer" })));
    $("tl-week").addEventListener("click", () => {
      const caps = el.capabilities;
      for (const c of caps) {
        if (c.since) c.since -= 7 * D;
        if (c.resetAt) c.resetAt -= 7 * D;
        for (const e of c.evidence) e.at -= 7 * D;
      }
      el.capabilities = caps;
      log.textContent = "A week went by: every “days at this level” count moved forward.";
    });
    $("tl-reset").addEventListener("click", reset);
    $("tl-owner").addEventListener("change", (e) => { el.user = e.target.checked ? "Priya Nair" : "Daniel Kim"; });

    const save = (e, verb) => {
      const fail = $("tl-fail").checked;
      // Your server applies the new level here; a rejection changes nothing.
      e.detail.waitUntil(new Promise((ok, ko) => setTimeout(() => (fail ? ko(new Error("403")) : ok()), 650)));
      log.textContent = `mv-${verb} · ${e.detail.label}: ${e.detail.fromLabel} → ${e.detail.toLabel} · reason: ${e.detail.reason}${e.detail.early ? " · early" : ""}${fail ? " · server rejects…" : ""}`;
      if (fail) $("tl-fail").checked = false;
    };
    el.addEventListener("mv-promote", (e) => save(e, "promote"));
    el.addEventListener("mv-demote", (e) => save(e, "demote"));
    el.addEventListener("mv-demote-dismiss", (e) => { log.textContent = `mv-demote-dismiss · ${e.detail.capability}: level kept, count restarted`; });
    el.addEventListener("mv-trust-evidence", (e) => {
      const { entry, status, progress } = e.detail;
      const streak = progress.criteria.find((c) => c.key === "streak");
      log.textContent = `mv-trust-evidence · ${entry.outcome} · status: ${status}${streak ? ` · streak ${streak.value}/${streak.target}` : ""}`;
    });
    el.addEventListener("mv-trust-error", () => { log.textContent = "mv-trust-error · the server refused: nothing changed."; });
  </script>
</div>

Cultural reference

The Little Prince, Antoine de Saint-Exupéry (1943, book). The fox explains that taming takes patience: sit a little closer each day, at the same hour, and trust is built step by step, never all at once. In the UI, each capability climbs a ladder of autonomy only as its track record accumulates (so many approved actions in a row, so many days), a person grants each step, and a broken promise means stepping back and starting the count again.

API

Attributes

NameTypeDefaultDescription
labelstringAutonomyHeading of the matrix (who or what is being trusted: “Support assistant”, “Deploy bot”, “Elena Petrova · Junior approver”). Also names the group for assistive tech.
userstringName of the person viewing. With require-owner, promotions are only offered when it matches the capability’s owner; everyone can still step a capability down.
require-ownerbooleanPromotion is always a person’s decision: auto-promote is ignored, promote({ reason: "criteria" }) is refused, and the Promote button is disabled (with an explanation) for anyone but the owner.
auto-promotebooleanWhen a record() completes the next level’s criteria, promote automatically (mv-promote with reason "criteria", still cancelable). Ignored with require-owner.
auto-demotebooleanA reverted action steps the capability down right away (mv-demote with reason "reverted", cancelable) instead of only suggesting it.
compactbooleanDense matrix for dashboards and side panels: name, rungs and status only; rows still open for details.
evidence-limitnumber5Recent actions shown in the table before “Show all”.
heading-levelnumber (2-5)3Heading level of each capability row (section titles inside use the next level).
localestringen-USLocale for dates, times and lists.

Properties

NameTypeDescription
capabilitiesArray<{ id, label, owner?, level, since?, resetAt?, levels?, evidence[], open? }>The rows. level = id (or index) of the current level; since = when it was reached; evidence = [{ id?, at, title, outcome: "approved" | "kept" | "edited" | "rejected" | "reverted", by?, note? }]. Times accept Date, epoch ms, ISO strings or relative offsets ("-3d", "-5h", "now"). Reading it returns the live state, including status. Also accepted as a <script type="application/json"> child: { levels, capabilities }.
levelsArray<{ id, label, description?, criteria? }>Default ladder for capabilities without their own levels, lowest first. criteria (to reach that level, counted since the current level was reached or the count restarted): streak (approved or kept in a row), actions, approvalRate (0-1 or %), days, maxReverted. Defaults: Suggest only → Act with approval → Act and report → Act silently.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (English defaults), e.g. statusReady, promote ({level}), stepDown, alertDemote ({action}, {when}, {level}), keep, ownerOnly ({owner}), critStreak, needStreak ({n}).

Methods

NameDescription
record(id, { title, outcome, at?, by?, note? })Adds an outcome to a capability’s record, re-evaluates it and emits mv-trust-evidence. A reverted action raises the step-down suggestion (or demotes with auto-demote). Returns the stored entry, or null.
promote(id, { reason? })One level up. Emits the cancelable mv-promote; resolves true once applied (after any waitUntil promises). Early promotions are allowed from code; detail.early and detail.unmet say what was missing.
demote(id, { to?, reason?, trigger? })One level down (or to level id/index to). Emits the cancelable mv-demote; resolves true once applied.
keep(id)Keeps the level after a reverted action and restarts the count (the criteria window starts now). Emits the cancelable mv-demote-dismiss.
progress(id){ level, next, status, met, ratio, criteria: [{ key, label, value, target, met, ratio, text }], stats: { streak, actions, clean, corrected, reverted, days, rate, since } }.
expand(id) / collapse(id) / toggle(id)Opens or closes a row’s details.

Events

NameDescription
mv-promoteCancelable, before a promotion. detail: { capability, label, from, to, fromLabel, toLabel, reason: "owner" | "criteria" | "api", early, unmet[], stats, user, waitUntil(promise) }. A rejected promise changes nothing and announces an error.
mv-demoteCancelable, before a step down. detail: { capability, label, from, to, fromLabel, toLabel, reason: "owner" | "reverted" | "api", trigger (the reverted entry, if any), user, waitUntil(promise) }.
mv-trust-evidenceAn outcome was recorded. detail: { capability, entry, status: "building" | "ready" | "demote" | "reverted" | "top", suggestion: "promote" | "demote" | null, progress }.
mv-demote-dismissCancelable: someone chose “Keep level, restart count” after a reverted action. detail: { capability, level, trigger, user }.
mv-trust-errorA waitUntil promise rejected. detail: { capability, action: "promote" | "demote", error }.

Content structure

NameDescription
(json)An optional <script type="application/json"> child with { levels, capabilities } for declarative setup (ignored when the properties are set first).

CSS classes

NameDescription
mv-trust-ladder-rowOne capability (li): data-status="building | ready | demote | reverted | top", data-open, data-busy. Holds -trigger (disclosure button with -cap, -track of -rung[data-state], -level, -status, -chevron) and -detail.
mv-trust-ladder-steps / -stepThe ladder as an ordered list drawn as a staircase; each step has data-state="reached | current | next | locked" (aria-current="step" on the current one) and data-ready on a next step whose criteria are met.
mv-trust-ladder-criteria / -criterionNext level’s criteria: data-met, -criterion-icon, -label, -value, -bar (--_p).
mv-trust-ladder-strip / -cellDecorative track-record strip (aria-hidden): one cell per outcome, data-outcome, data-streak, data-before.
mv-trust-ladder-tableRecent actions table (When, Action, Outcome, Reviewed by); rows carry data-outcome and data-before.
mv-trust-ladder-alert / -actions / -confirmStep-down suggestion, action bar (note + Step down / Promote) and early-promotion confirmation.

CSS variables

NameDefaultDescription
--mv-trust-ladder-accentvar(--mv-accent)Reached rungs and steps, streak cells, progress bars.
--mv-trust-ladder-readyvar(--mv-success)Met criteria and “Ready to promote”.
--mv-trust-ladder-riskvar(--mv-danger)Reverted actions and the step-down suggestion.
--mv-trust-ladder-correctedvar(--mv-warning)Edited and rejected outcomes, early-promotion confirmation.
--mv-trust-ladder-rise0.625remHeight of each stair in the ladder.

Accessibility

The element is a group labelled by its heading; each capability is a list item whose heading contains a real disclosure <button> (aria-expanded, aria-controls) that reads the whole row in words: name, owner, current level with “Level 2 of 4”, and the status (“Ready to promote”, “Step down suggested”, “64% to next level”). The rungs and the track-record strip are aria-hidden; the same facts are always in text. The opened region is labelled by its trigger. Levels are an ordered list with the current step marked by aria-current="step" and a visible “Current level” label; reached, next (with its percentage) and locked steps are named in text too. Criteria are a list where each item says its value (“14 of 20”) and, for screen readers, “met” or “not met yet”, followed by a sentence of what is still needed. Evidence is a real table with a caption and column headers; outcomes combine an icon and a word, and rows from before the current count say so. Status never relies on color: icons, words and dashed or solid borders carry it. Promote and Step down are buttons whose names include the target level; a Promote the viewer may not use is aria-disabled and described by the “Only Priya Nair can promote” note. Early promotion opens an inline confirmation (focus moves to Cancel, Escape closes it and returns focus to Promote). While a change awaits the server the button is aria-busy and the row says “Saving…”. Changes are announced politely (“Refund customers promoted to Act and report. The count starts again at this level.”, “…is ready to promote”), a reverted action assertively; errors assertively. If a change hides the focused button, focus moves to the next sensible control in the row. Reduced motion (OS or data-motion="reduce"): the step highlight, row reveal and new-row fade are instant. Forced colors: rungs and cells get system-color borders and fills, the current step a Highlight outline.