Exclusivebeta

Pass Card <mv-pass-card>

A small, visible budget of rule overrides (“passes”) for the moments a rule has to bend: merging during a release freeze, skipping a required check, a late submission, one purchase above a spending limit, a rescheduling without fee, an SLA exception. The wallet reads “2 of 3 left this quarter · Refills Oct 1” with one notched ticket per pass: available ones are solid, a pass in effect carries a bolt, one awaiting approval is striped, a spent one is hollow and punched, and the ticket visibly flips when a pass is spent. Where a rule blocks the action (blocked, rule, action), the gate offers “Use a pass”: an inline form that lets the user pick a pass type (each with its own duration, per-period limit and approval rule), states in two columns exactly what the pass covers and what it does not, requires a labelled reason with a minimum length, asks for an approver when the pass needs one (it is then held, not in effect), and shows the count left after use before anything is spent. Using a pass goes through a cancelable mv-pass-use whose waitUntil(promise) lets your server record it (a rejection spends nothing); the gate then turns into “Pass in effect · covers PR #4821 until 3:40 PM” and the pass expires on time. Every use lands in a pass log table (when, who, pass and target, reason, status: in effect, awaiting approval, used, returned, declined) that reviewers can read; unused passes can be handed back (mv-pass-return) and do not count, approvals are settled with approve() / deny(), and “Request more” emits mv-pass-request. Declarative config through a JSON child, English strings all overridable.

CategoryForms
TypeWeb Component (<mv-pass-card>)
Statusbeta
KitSafe destructive actions and secrets
Also installsbutton, field, radio, checkbox, select, textarea
Keywordsexclusive, culture, exception, override, waiver, exemption, late-pass, extension, merge-freeze, bypass, allowance, quota, budget, audit-log, accountability, approval, policy, compliance, ticket

When to use

  • Engineers may occasionally bypass a merge freeze or a required check, but each bypass must be justified and reviewable
  • Students or customers get a few extensions, late passes or free reschedules per term, and should see how many they have left
  • A spending limit, approval step or SLA rule can be exceeded a limited number of times, sometimes with a manager's sign-off
  • Admins want exceptions to stay rare and accountable without making every one a support ticket

Avoid when

  • Every critical action needs a second person to co-sign it, not a limited budget of exceptions → use Two-Key instead
  • The limit is a metered quota or rate that refills continuously, like API calls or AI messages → use Recharge instead
  • Elevated rights are granted for a time window and the UI must revert when it ends, with no per-period budget → use Midnight instead

Install

node scripts/add.mjs pass-card --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["pass-card"], 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/pass-card/pass-card.js, components/pass-card/pass-card.css, components/button/button.css, components/field/field.css, components/radio/radio.css, components/checkbox/checkbox.css, components/select/select.css, components/textarea/textarea.css, components/textarea/char-count.js.

Usage

Quick start, the smallest working markup:

<mv-pass-card label="Freeze passes" total="3" blocked rule="Merging to main is frozen until Oct 3" action="PR #4821"></mv-pass-card>

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

<div id="pc-demo" style="width:min(100%,60rem);margin-inline:auto">
  <style>
    #pc-demo { display:grid; gap:1.25rem; align-content:start }
    #pc-demo .pc-pr { display:grid; gap:0; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle); overflow:hidden }
    #pc-demo .pc-pr-head { display:grid; gap:.375rem; padding:1rem 1.25rem; background:var(--mv-surface); border-bottom:1px solid var(--mv-border) }
    #pc-demo .pc-pr-title { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; margin:0; font-size:1.0625rem; font-weight:650; letter-spacing:-.01em }
    #pc-demo .pc-pr-title span { color:var(--mv-fg-subtle); font-weight:400 }
    #pc-demo .pc-pr-sub { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-pr-sub code { padding:.0625rem .375rem; border-radius:var(--mv-radius-sm); background:var(--mv-bg-muted); font:.75rem var(--mv-font-mono); color:var(--mv-fg) }
    #pc-demo .pc-checks { display:grid; gap:0; margin:0; padding:0; list-style:none; background:var(--mv-surface); border-bottom:1px solid var(--mv-border) }
    #pc-demo .pc-checks li { display:flex; align-items:center; gap:.625rem; padding:.625rem 1.25rem; font-size:.8125rem; border-top:1px solid var(--mv-border) }
    #pc-demo .pc-checks li:first-child { border-top:0 }
    #pc-demo .pc-checks svg { width:1rem; height:1rem; flex:none }
    #pc-demo .pc-ok { color:color-mix(in oklab, var(--mv-success), var(--mv-fg) 25%) }
    #pc-demo .pc-no { color:color-mix(in oklab, var(--mv-danger), var(--mv-fg) 20%) }
    #pc-demo .pc-checks .mv-badge { margin-inline-start:auto }
    #pc-demo .pc-body { display:grid; gap:1rem; padding:1rem 1.25rem 1.25rem }
    #pc-demo .pc-merge { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap }
    #pc-demo .pc-merge p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-controls { display:flex; align-items:center; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.875rem 1rem; border:1px dashed var(--mv-border-strong); border-radius:var(--mv-radius-lg) }
    #pc-demo .pc-controls .mv-choice { font-size:.8125rem }
    #pc-demo .pc-log { flex:1 1 14rem; min-width:0; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end }
    #pc-demo .pc-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,24rem),1fr)); gap:1.25rem; align-items:start }
    #pc-demo .pc-panel { display:grid; gap:.875rem; padding:1rem 1.125rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #pc-demo .pc-kicker { margin:0; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #pc-demo .pc-assign { display:grid; gap:.25rem }
    #pc-demo .pc-assign strong { font-size:.9375rem; letter-spacing:-.01em }
    #pc-demo .pc-assign span { color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-expense { display:flex; align-items:baseline; justify-content:space-between; gap:.75rem; flex-wrap:wrap }
    #pc-demo .pc-expense strong { font-size:.9375rem }
    #pc-demo .pc-expense span { font-variant-numeric:tabular-nums; font-weight:600 }
    #pc-demo .pc-note { margin:0; color:var(--mv-fg-muted); font-size:.75rem }
  </style>

  <!-- Main: a pull request blocked by a release freeze, with a team budget of freeze passes -->
  <section class="pc-pr" aria-label="Pull request #4821">
    <header class="pc-pr-head">
      <p class="pc-pr-title">Fix rounding in checkout totals <span>#4821</span></p>
      <p class="pc-pr-sub">Maya Chen wants to merge 3 commits into <code>main</code> from <code>fix/checkout-rounding</code></p>
    </header>
    <ul class="pc-checks">
      <li><svg class="pc-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>All 14 checks passed</li>
      <li><svg class="pc-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>Approved by Jonas Weber and Leila Haddad</li>
      <li id="pc-freeze-row"><svg class="pc-no" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg>Release freeze for 4.2 (until Oct 3)<span class="mv-badge" data-variant="danger" id="pc-freeze-badge">Blocking</span></li>
    </ul>
    <div class="pc-body">
      <mv-pass-card id="pc-main" label="Freeze passes" total="3" period="this quarter" refills="2026-10-01"
        blocked rule="Merging to main is frozen until Oct 3 for the 4.2 release"
        action="PR #4821 · Fix rounding in checkout totals" user="Maya Chen" requestable log-limit="4">
        <script type="application/json">{
          "approvers": ["Priya Nair · Release captain", "Tomás Rivera · Engineering manager"],
          "kinds": [
            { "id": "freeze", "label": "Merge during freeze", "duration": "24h",
              "covers": ["Merging this pull request to main", "The automatic deploy to staging that follows"],
              "excludes": ["Production deploys (the release captain ships those)", "Skipping required reviews or failing checks"] },
            { "id": "hotfix", "label": "Production hotfix", "description": "Ship to production now", "duration": "4h", "limit": 1, "approval": true,
              "covers": ["Merging and deploying this pull request to production", "One rollback if the deploy misbehaves"],
              "excludes": ["Database migrations", "Any other pull request"] }
          ]
        }</script>
      </mv-pass-card>
      <div class="pc-merge">
        <p id="pc-merge-note">Merging is blocked by the release freeze.</p>
        <button type="button" class="mv-button" id="pc-merge" aria-disabled="true" aria-describedby="pc-merge-note">Merge pull request</button>
      </div>
    </div>
  </section>

  <div class="pc-controls">
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="pc-fail"> Server rejects the next pass</label>
    <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="pc-approve" aria-disabled="true">Approve as Priya Nair</button>
    <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="pc-reset">Reset demo</button>
    <p class="pc-log" id="pc-log" aria-live="polite">Pick “Use a pass” to try it.</p>
  </div>

  <div class="pc-grid">
    <!-- Inline: a student's late passes, right on the closed assignment -->
    <section class="pc-panel" aria-label="Assignment">
      <p class="pc-kicker">Course portal · Writing 210</p>
      <div class="pc-assign">
        <strong>Essay 3 · The ethics of automation</strong>
        <span>Due Sep 22, 11:59 PM · 1,500 words · 15% of the grade</span>
      </div>
      <mv-pass-card id="pc-late" variant="inline" label="Late passes" total="4" period="this semester" refills="2027-01-19"
        blocked rule="Submissions closed on Sep 22 at 11:59 PM" action="Essay 3 · The ethics of automation" user="Samuel Adeyemi" min-reason="10">
        <script type="application/json">{
          "kinds": [
            { "id": "late", "label": "48-hour extension", "duration": "48h",
              "covers": ["Submitting this essay up to 48 hours late, with no penalty"],
              "excludes": ["Exams and in-class quizzes", "Group project deadlines"] }
          ],
          "log": [
            { "id": "sa-1", "kind": "late", "by": "Samuel Adeyemi", "at": "2026-09-09T21:40:00", "reason": "Flu, doctor's note sent to the TA", "target": "Reading response 2", "status": "used" }
          ]
        }</script>
      </mv-pass-card>
      <p class="pc-note">Your instructor sees every late pass and its reason.</p>
    </section>

    <!-- Card, exhausted: spending-limit overrides need a manager's approval -->
    <section class="pc-panel" aria-label="Expense">
      <p class="pc-kicker">Company card · Elena Petrova</p>
      <div class="pc-expense">
        <strong>Figma Organization · annual plan</strong>
        <span>$1,440.00</span>
      </div>
      <mv-pass-card id="pc-limit" label="Limit overrides" total="2" period="this month" refills="2026-10-01"
        blocked rule="This purchase is over your $1,000.00 per-transaction limit" action="Figma Organization · $1,440.00"
        user="Elena Petrova" require-approval requestable log-limit="2">
        <script type="application/json">{
          "approvers": ["Tomás Rivera · Engineering manager"],
          "kinds": [{ "id": "limit", "label": "One purchase over limit", "covers": ["This one purchase, up to $5,000.00"], "excludes": ["Recurring charges after the first one", "Cash withdrawals"] }],
          "log": [
            { "id": "ep-2", "kind": "limit", "by": "Elena Petrova", "at": "2026-09-16T10:12:00", "reason": "Booth deposit for DevWorld Berlin, due today", "target": "DevWorld Berlin · $1,250.00", "status": "used", "approver": "Tomás Rivera" },
            { "id": "ep-1", "kind": "limit", "by": "Elena Petrova", "at": "2026-09-03T16:48:00", "reason": "Reserved instances renewal before the price change", "target": "AWS · $2,180.00", "status": "used", "approver": "Tomás Rivera" }
          ]
        }</script>
      </mv-pass-card>
    </section>
  </div>

  <script type="module">
    const main = document.getElementById("pc-main");
    const merge = document.getElementById("pc-merge");
    const mergeNote = document.getElementById("pc-merge-note");
    const badge = document.getElementById("pc-freeze-badge");
    const fail = document.getElementById("pc-fail");
    const approveBtn = document.getElementById("pc-approve");
    const out = document.getElementById("pc-log");
    const say = (text) => { out.textContent = text; };

    // Relative dates keep the main log recent whenever the demo is opened.
    const ago = (days, hh, mm) => { const t = new Date(); t.setDate(t.getDate() - days); t.setHours(hh, mm, 0, 0); return t.toISOString(); };
    const initialLog = () => [
      { id: "fz-2", kind: "freeze", by: "Aiko Tanaka", at: ago(2, 16, 5), target: "PR #4802 · Update currency rounding tables", reason: "Needed the new tables before the Tokyo launch. Turned out the fix landed before the freeze, so I handed it back.", status: "returned" },
      { id: "fz-1", kind: "freeze", by: "Daniel Okafor", at: ago(6, 11, 42), target: "PR #4790 · Patch image upload validation", reason: "Security fix for the image upload endpoint. Waiting 9 days for the freeze to lift would leave it exposed.", status: "used" },
    ];
    main.log = initialLog();

    const sync = () => {
      const state = main.state;
      const covered = state === "covered";
      merge.setAttribute("aria-disabled", String(!covered));
      mergeNote.textContent = covered ? "A freeze pass covers this merge." : state === "pending" ? "Waiting for the release captain to approve the hotfix pass." : "Merging is blocked by the release freeze.";
      badge.textContent = covered ? "Pass in effect" : state === "pending" ? "Awaiting approval" : "Blocking";
      badge.dataset.variant = covered ? "success" : state === "pending" ? "warning" : "danger";
      approveBtn.setAttribute("aria-disabled", String(!main.log.some((e) => e.status === "pending")));
    };
    customElements.whenDefined("mv-pass-card").then(sync);

    // A fake server that records each pass (and can refuse it).
    const server = (ms = 700) => new Promise((resolve, reject) => {
      const bad = fail.checked;
      setTimeout(() => (bad ? reject(new Error("503")) : resolve({ id: `srv-${Math.random().toString(36).slice(2, 7)}` })), ms);
    });

    for (const el of document.querySelectorAll("#pc-demo mv-pass-card")) {
      el.addEventListener("mv-pass-use", (e) => {
        e.detail.waitUntil(server());
        say(`mv-pass-use · ${e.detail.kind.label} · ${e.detail.remaining} left after`);
      });
      el.addEventListener("mv-pass-return", (e) => {
        e.detail.waitUntil(server(400));
        say(`mv-pass-return · ${e.detail.remaining} left after`);
      });
      el.addEventListener("mv-pass-request", (e) => {
        e.detail.waitUntil(server(600));
        say(`mv-pass-request · ${e.detail.remaining} of ${e.detail.total} left`);
      });
      el.addEventListener("mv-pass-error", (e) => {
        fail.checked = false;
        say(`mv-pass-error · ${e.detail.action} failed (${e.detail.error.message}), nothing was spent`);
      });
    }
    main.addEventListener("mv-pass-change", (e) => { sync(); say(`mv-pass-change · ${e.detail.reason} · ${e.detail.remaining} of ${e.detail.total} left`); });

    merge.addEventListener("click", () => {
      if (merge.getAttribute("aria-disabled") === "true") return;
      say("Merged #4821 into main under a freeze pass.");
    });
    approveBtn.addEventListener("click", () => {
      const pending = main.log.find((e) => e.status === "pending");
      if (pending) main.approve(pending.id, { by: "Priya Nair" });
    });
    document.getElementById("pc-reset").addEventListener("click", () => {
      main.close();
      main.log = initialLog();
      main.requested = false;
      fail.checked = false;
      sync();
      say("Demo reset.");
    });
  </script>
</div>

Cultural reference

Monopoly, Parker Brothers (from Lizzie Magie's The Landlord's Game) (1935, game). The “Get Out of Jail Free” card is a rare, physical exemption from a rule: you can hold it until it really matters, it frees you exactly once, and then you must hand it back to the deck. Here it becomes a visible wallet of a few rule overrides per period, each one spent deliberately with a stated reason, scoped to what it covers, logged for reviewers and returned if unused.

API

Attributes

NameTypeDefaultDescription
totalnumber3Passes in the budget for the current period. Passes in effect, awaiting approval or used count against it; returned and declined ones do not.
labelstringPassesWallet title (“Freeze passes”, “Late passes”, “Limit overrides”).
periodstringthis periodHow the period reads after the count: “2 of 3 left this quarter”.
refillsdate (ISO, a bare YYYY-MM-DD is local)When the budget refills, shown as “Refills Oct 1” in the wallet, the gate and the empty state. Display only: your app resets the log when the period changes.
blockedbooleanA rule currently blocks the action: shows the gate with “Use a pass”. The gate reads “Pass in effect” or “Waiting for approval” while a pass for the same action is active or pending.
rulestringThe rule that blocks, as the gate title (“Merging to main is frozen until Oct 3 for the 4.2 release”). Children with slot="rule" replace it with rich content.
actionstringWhat is blocked (“PR #4821 · Fix rounding in checkout totals”). Stored as the target of the pass; a pass in effect covers the gate only when its target matches.
userstringYouName recorded as “who” in the log. Only this user’s passes in effect or pending show Return / Withdraw.
require-approvalbooleanEvery pass needs an approver (a kind can also require it with approval: true). The approver select becomes required and the pass is held until approve().
min-reasonnumber15Minimum reason length in characters (at least 1: a reason is always required).
requestablebooleanShows “Request more” in the wallet (and in the gate once no pass is left).
requestedbooleanSet by the component once a request is sent (the button reads “Requested” and is disabled). Remove it to allow another request.
variant"card" | "inline"cardcard: wallet, gate and log in one card. inline: only the gate (with the count in its hint), to sit in a list row or next to a blocked button.
hide-logbooleanHides the pass log (always hidden in the inline variant).
log-limitnumber4Rows shown before “Show all N”.
localeBCP 47 tagen-USLocale for dates and times (Intl.DateTimeFormat).
data-stateavailable | empty | blocked | covered | pendingSet by the component (styleable). data-empty when no pass is left, data-open while the form is open, data-busy while a waitUntil promise is pending.

Properties

NameTypeDescription
kindsArray<{ id, label, description?, covers?: string[], excludes?: string[], duration?: "24h" | "48h" | "7d" | ms, limit?: number, approval?: boolean }>Pass types. With several, the form shows them as radio cards; limit caps that kind per period (“1 of 1 left”, disabled at 0); duration sets how long a pass stays in effect once used (none = one action). Also settable from the JSON child.
logArray<{ id, kind, by, at, reason, target?, status: "active" | "pending" | "used" | "returned" | "denied", approver?, until? }>Uses this period (dates as ISO strings, Date or epoch ms). Read it to persist; an active pass past its until reads as used. Also settable from the JSON child.
approversstring[]Names offered in the approver select.
remainingnumberPasses left this period (read-only).
usednumberPasses spent this period: in effect, awaiting approval or used (read-only).
state"available" | "empty" | "blocked" | "covered" | "pending"Current gate state, or wallet state when nothing is blocked (read-only).
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (English defaults: title, use, useFor, afterUse, noneLeft, covers, excludes, validFor, reason, reasonHint, reasonShort, approver, summary, summaryHeld, accountable, confirm, confirmApproval, coveredTitle, coveredUntil, pendingTitle, pendingText, logTitle, logNote, status*, announce*, …).

Methods

NameDescription
use({ kind?, reason, approver?, target? })Uses a pass programmatically (same path as the form, including the cancelable mv-pass-use and waitUntil). Resolves to the logged entry, or null if refused, cancelled, failed or no pass is left.
returnPass(id)Hands back a pass in effect or awaiting approval (cancelable mv-pass-return): it is marked returned and no longer counts. Resolves true when done.
approve(id, { by? })Settles a pending pass: it takes effect now for its kind’s duration. Call it when your server confirms the approval.
deny(id, { by?, note? })Settles a pending pass as declined: it is not counted.
request()Asks for more passes (cancelable mv-pass-request with waitUntil), then sets requested. Resolves true once sent.
open() / close()Opens or closes the “Use a pass” form (open only while the gate is blocked and a pass is left).

Events

NameDescription
mv-pass-useCancelable, before a pass is spent. detail: { entry, kind, reason, approver, target, remaining (after use), total, waitUntil(promise) }. Call waitUntil() synchronously with your request: the confirm button shows “Logging…”, a rejection spends nothing and shows an error, an object it resolves to is merged into the entry (server id, until…). preventDefault() refuses the use.
mv-pass-returnCancelable, before a pass is handed back. detail: { entry, remaining (after return), total, waitUntil(promise) }.
mv-pass-requestCancelable, “Request more” was chosen. detail: { remaining, total, target, waitUntil(promise) }.
mv-pass-changeThe log changed. detail: { reason: "use" | "return" | "approve" | "deny" | "expire", entry, log, remaining, total }. Use it to persist.
mv-pass-errorA waitUntil promise rejected. detail: { action: "use" | "return" | "request", error }.

Content structure

NameDescription
ruleRich content (a link to the freeze calendar, a policy name…) shown as the gate title instead of the rule attribute while the action is blocked.

CSS classes

NameDescription
mv-pass-card-walletWallet: -head, -mark, -title, -summary, -request, -meter, -count (-count-value, -count-of), -legend.
mv-pass-card-ticketOne ticket per pass in .mv-pass-card-tickets, data-state available | pending | active | used; -ticket-glyph holds its icon. data-dense on the strip above 10 passes.
mv-pass-card-gateBlocked-action strip, data-state blocked | empty | covered | pending: -gate-icon, -gate-title, -rule (slot), -gate-text, -gate-hint, -use, -gate-return, -gate-request.
mv-pass-card-formUse form: -form-title, -kinds (mv-choice-group), -scope (-scope-col[data-scope=covers|excludes], -scope-list, -validity), -reason-field, -approver, -after (-pip[data-state]), -form-error, -form-actions, -confirm.
mv-pass-card-logLog: -log-title, -log-note, -log-toggle, -table (rows with data-status; cells with data-col), -status[data-status], -status-sub, -row-return. Rows stack as cards under a 40rem container width.

CSS variables

NameDefaultDescription
--mv-pass-card-ticketvar(--mv-accent)Color of available tickets, the wallet mark and the after-use pips.
--mv-pass-card-activevar(--mv-success)Pass in effect: ticket, covered gate, status pill.
--mv-pass-card-heldvar(--mv-warning)Pass awaiting approval: striped ticket, pending gate, status pill.
--mv-pass-card-blockedvar(--mv-danger)Tint of the blocked gate and of declined passes.
--mv-pass-card-spentvar(--mv-fg-subtle)Used tickets and neutral status pills.

Accessibility

Counts are always text: the wallet is a labelled section whose description reads “2 of 3 left this quarter · Refills Oct 1”, and the legend names each state in words (“1 in effect · 1 used”); the ticket strip and after-use pips are decorative (aria-hidden) and states also differ by icon and border style (bolt, stripes, hollow dashed and punched), never by color alone. The gate is a labelled group; “Use a pass” is a real button with aria-expanded / aria-controls, described by the hint (“1 of 3 left after this”, or “No passes left this quarter. Refills Oct 1.”). With no pass left it stays focusable with aria-disabled so the reason can be read. The form is a native <form> (novalidate) labelled by its title: pass types are native radios in a fieldset with a legend, covers / does-not-cover are headed lists, the reason is a <textarea> with a visible label, required and minlength, a hint and an error message linked through aria-describedby with aria-invalid; the approver is a labelled, required <select>. Submitting with a missing field focuses the first invalid control. Escape closes the form and returns focus to “Use a pass”; Ctrl/Cmd+Enter submits from the reason. While a waitUntil promise is pending the confirm button is aria-busy and aria-disabled. After a pass is used, focus moves to the gate title (“Pass in effect”) and a polite live region announces “Pass used for PR #4821. 1 of 3 left this quarter. Logged for reviewers.”; returns, approvals and declines are announced the same way, failures assertively. The log is a real table with a caption-like heading (aria-labelledby), column headers and <time datetime> cells; Return / Withdraw buttons carry a full label (“Return the pass used Sep 24, 3:40 PM for PR #4821”). Under 40rem the rows stack as cards with explicit table roles so the semantics survive. Motion (ticket flip, pulsing next pip, new-row highlight) is off under prefers-reduced-motion or data-motion="reduce"; forced-colors mode keeps borders and states visible.