Exclusivebeta
Same Again — <mv-same-again>
Recurring forms that remember the previous loop: wrap a daily standup, timesheet, shift handover, checklist or weekly report and every submit stores the answers with their date (browser storage under a key, or your server through the previous property and mv-loop-saved). Next time, each field offers its last answer right where it is typed: a tinted ghost placeholder inside empty text fields and a small chip under any field (“Yesterday · Remote · 5 in a row”); → in an empty field (Tab with tab-accepts) or a click reuses it, Escape dismisses it. A bar sums up the loop (“Loop 42 · Last filled yesterday at 9:12 AM”, a 7-period dot strip and “14-day streak”) with “Same as yesterday”, which fills only the fields the user has not touched and marks each one “Carried over from yesterday · Confirm · Undo” until it is confirmed or edited, so nobody submits stale answers without noticing; require-review blocks submit until then. Fields whose answer changed on every recent loop are not suggested, fields under data-same-again="never" (and dates, passwords, card data, one-time codes, hidden and file inputs) are never stored, cadence (daily, weekdays, weekly, none) drives the streak and replaces a same-period resubmission instead of counting a new loop, and every reuse goes through a cancelable mv-carry whose values the app can rewrite. Native forms, radio and checkbox groups, multiple selects, switches and form-associated custom elements are supported, and values are written through native setters with input/change events so framework state stays in sync.
| Category | Forms |
|---|---|
| Type | Web Component (<mv-same-again>) |
| Status | beta |
| Also installs | button |
| Keywords | exclusive, culture, form, recurring, repeat, prefill, autofill, previous-answers, standup, timesheet, handover, checklist, check-in, weekly-report, streak, suggestion, ghost-text, carry-over, review, form-state |
When to use
- People fill the same form every day or week (standup, timesheet, habit or health check-in) and most answers rarely change
- A shift handover or site log should start from the previous shift’s answers without anyone submitting them unread
- A weekly report or expense claim repeats last period’s structure and only a few fields need new values
- Compliance requires that reused answers be confirmed one by one before a recurring form is sent
Avoid when
- The goal is to recover an unfinished draft after a crash or reload, not to reuse a submitted one → use Bloodstain instead
- Users need to browse or restore earlier versions of the same form while editing it → use Rewind instead
- Answers must be re-attested from scratch every time (safety checks, clinical observations): mark those fields data-same-again="never" or skip the component
Install
node scripts/add.mjs same-again --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["same-again"], 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/same-again/same-again.js, components/same-again/same-again.css, components/button/button.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="sa-demo" style="width:min(100%,64rem);margin-inline:auto">
<style>
#sa-demo { display:grid; gap:1.25rem }
#sa-demo .sa-layout { display:grid; grid-template-columns:minmax(0,1fr) 16.5rem; gap:1.25rem; align-items:start }
#sa-demo .sa-card { min-width:0; padding:1.25rem 1.375rem 1.375rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#sa-demo .sa-head { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem; margin:0 0 1rem }
#sa-demo .sa-head h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
#sa-demo .sa-head p { margin:.1875rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#sa-demo .sa-radios { display:flex; gap:.5rem 1.25rem; flex-wrap:wrap; padding-top:.25rem }
#sa-demo .sa-checks { display:grid; gap:.625rem; justify-items:start }
#sa-demo .sa-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; margin-top:1.25rem; padding-top:1rem; border-top:1px solid var(--mv-border) }
#sa-demo .sa-foot small { color:var(--mv-fg-muted); font-size:.75rem }
#sa-demo .sa-side { display:grid; gap:1rem; position:sticky; top:1rem }
#sa-demo .sa-panel { padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#sa-demo .sa-panel h4 { margin:0 0 .625rem; font-size:.8125rem }
#sa-demo .sa-steps { margin:0 0 1rem; padding-inline-start:1.125rem; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.5; display:grid; gap:.375rem }
#sa-demo .sa-steps b { color:var(--mv-fg); font-weight:600 }
#sa-demo .sa-next { width:100% }
#sa-demo .sa-panel .mv-choice { font-size:.8125rem; margin-top:.875rem }
#sa-demo .sa-log { display:grid; gap:.375rem; margin:0; padding:0; list-style:none; font:.75rem/1.4 var(--mv-font-mono); color:var(--mv-fg-muted) }
#sa-demo .sa-log li { overflow-wrap:anywhere }
#sa-demo .sa-log b { color:var(--mv-fg); font-weight:600 }
#sa-demo .sa-ho-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem 1.25rem; align-items:start }
#sa-demo .sa-ho-grid .sa-wide { grid-column:1 / -1 }
@media (max-width:54rem) {
#sa-demo .sa-layout { grid-template-columns:minmax(0,1fr) }
#sa-demo .sa-side { position:static }
}
@media (max-width:36rem) { #sa-demo .sa-ho-grid { grid-template-columns:minmax(0,1fr) } }
</style>
<div class="sa-layout">
<!-- Main: an async daily standup, weekdays cadence, history from the "server" -->
<section class="sa-card" aria-labelledby="sa-title">
<div class="sa-head">
<div>
<h3 id="sa-title">Daily check-in</h3>
<p>Payments squad · async standup, due by 10:00 AM</p>
</div>
<span class="mv-badge" data-variant="secondary">Weekdays</span>
</div>
<mv-same-again id="sa-main" cadence="weekdays" storage="none">
<form id="sa-form" novalidate>
<div class="mv-field-group">
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="sa-date">Date</label>
<input class="mv-input" type="date" id="sa-date" name="date">
</div>
<div class="mv-field">
<label class="mv-label" for="sa-project">Main project</label>
<select class="mv-select" id="sa-project" name="project">
<option value="">Choose a project…</option>
<option value="checkout">Checkout redesign</option>
<option value="retries">Payment retries</option>
<option value="fraud">Fraud rules v2</option>
<option value="tools">Internal tools</option>
</select>
</div>
</div>
<fieldset class="mv-field" style="border:0;margin:0;padding:0;min-width:0">
<legend class="mv-label">Working from</legend>
<div class="sa-radios">
<label class="mv-choice"><input type="radio" class="mv-radio" name="location" value="remote"> Remote</label>
<label class="mv-choice"><input type="radio" class="mv-radio" name="location" value="office"> Office</label>
<label class="mv-choice"><input type="radio" class="mv-radio" name="location" value="client"> Client site</label>
</div>
</fieldset>
<div class="mv-field">
<label class="mv-label" for="sa-done">Since the last check-in</label>
<textarea class="mv-textarea" id="sa-done" name="done" rows="2" placeholder="What you shipped, reviewed or learned"></textarea>
</div>
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="sa-ticket">Focus ticket</label>
<input class="mv-input" id="sa-ticket" name="ticket" placeholder="PAY-000 · Short title">
</div>
<div class="mv-field">
<label class="mv-label" for="sa-blockers">Blockers</label>
<input class="mv-input" id="sa-blockers" name="blockers" placeholder="Anything slowing you down?">
</div>
</div>
<div class="sa-checks">
<label class="mv-choice"><input type="checkbox" class="mv-switch" role="switch" name="pairing"> Open to pairing today</label>
<label class="mv-choice" data-same-again="never"><input type="checkbox" class="mv-checkbox" name="resolved"> A blocker was resolved since the last check-in</label>
</div>
</div>
<div class="sa-foot">
<small>In an empty field, press <kbd class="mv-kbd" data-size="sm">→</kbd> to reuse the last answer.</small>
<button type="submit" class="mv-button">Submit check-in</button>
</div>
</form>
</mv-same-again>
</section>
<aside class="sa-side" aria-label="Try it">
<div class="sa-panel">
<h4>Try it</h4>
<ol class="sa-steps">
<li>Press <b>→</b> in an empty field, or click a suggestion under it.</li>
<li><b>Same as yesterday</b> fills only the fields you haven’t touched, and marks them until you confirm or edit them.</li>
<li>Submit, then <b>start tomorrow</b>: today’s answers become the next suggestions.</li>
</ol>
<button type="button" class="mv-button sa-next" data-variant="outline" id="sa-next" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
Start tomorrow’s check-in
</button>
<label class="mv-choice"><input type="checkbox" class="mv-switch" role="switch" data-size="sm" id="sa-review"> Require review before submit</label>
</div>
<div class="sa-panel">
<h4>Events</h4>
<ul class="sa-log" id="sa-log" aria-live="polite"><li>“Since the last check-in” changed every day, so it gets no suggestion.</li></ul>
</div>
</aside>
</div>
<!-- Variant: shift handover, one loop per submission, carried at load, review required -->
<section class="sa-card" aria-labelledby="sa-ho-title">
<div class="sa-head">
<div>
<h3 id="sa-ho-title">Shift handover · Packaging line 3</h3>
<p>Plant 2, Rotterdam · filled at the end of every shift</p>
</div>
<span class="mv-badge" data-variant="warning">Review required</span>
</div>
<mv-same-again id="sa-ho" cadence="none" storage="none" require-review>
<form id="sa-ho-form" novalidate>
<div class="sa-ho-grid">
<div class="mv-field">
<label class="mv-label" for="sa-ho-status">Line status</label>
<select class="mv-select" id="sa-ho-status" name="status">
<option value="">Choose…</option>
<option value="running">Running normally</option>
<option value="issues">Running with issues</option>
<option value="stopped">Stopped</option>
</select>
</div>
<div class="mv-field">
<label class="mv-label" for="sa-ho-next">Handing over to</label>
<input class="mv-input" id="sa-ho-next" name="next" placeholder="Name of the incoming lead">
</div>
<div class="mv-field sa-wide">
<label class="mv-label" for="sa-ho-issues">Open issues</label>
<textarea class="mv-textarea" id="sa-ho-issues" name="issues" rows="2"></textarea>
</div>
<div class="mv-field">
<label class="mv-label" for="sa-ho-units">Cases packed this shift</label>
<input class="mv-input" type="number" id="sa-ho-units" name="units" inputmode="numeric">
</div>
<div class="mv-field" style="align-self:end" data-same-again="never">
<label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="safety"> Safety walk done this shift</label>
</div>
</div>
<div class="sa-foot">
<small>Carried-over answers must be confirmed or edited before the handover can be sent.</small>
<button type="submit" class="mv-button" id="sa-ho-submit">Submit handover</button>
</div>
</form>
</mv-same-again>
</section>
<script type="module">
await customElements.whenDefined("mv-same-again");
const main = document.getElementById("sa-main");
const form = document.getElementById("sa-form");
const ho = document.getElementById("sa-ho");
const log = document.getElementById("sa-log");
const next = document.getElementById("sa-next");
const review = document.getElementById("sa-review");
const say = (html) => {
const li = document.createElement("li");
li.textContent = html;
log.prepend(li);
while (log.children.length > 5) log.lastElementChild.remove();
};
const at = (d, h, m) => { const x = new Date(d); x.setHours(h, m, 0, 0); return x.getTime(); };
const prevWeekday = (t) => { const d = new Date(t); do d.setDate(d.getDate() - 1); while (d.getDay() === 0 || d.getDay() === 6); return d.getTime(); };
const today = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; };
// Fourteen weekdays of history, as a server would return them (oldest first).
const DONE = [
"Kicked off the checkout copy review with Design.", "Fixed the currency rounding bug in receipts.", "Shipped the new address form to 5% of traffic.",
"Paired with Amara on the tax line breakdown.", "Wrote the rollout note for the checkout redesign.", "Closed the last checkout copy tickets.",
"Mapped every webhook the replay tool must cover.", "Built the replay queue and its dead-letter view.", "Reviewed PR #398 and added replay metrics.",
"Shipped webhook replay to 10% of merchants.", "Handled two replay escalations from support.", "Wrote the retry backoff RFC and shared it.",
"Paired with Kenji on idempotency keys; merged PR #409.", "Reviewed PR #412 and fixed the flaky refund test.",
];
let t = at(prevWeekday(Date.now()), 9, 12);
const loops = [];
for (let i = 13; i >= 0; i--) {
loops.unshift({ t, values: {
project: i >= 6 ? "retries" : "checkout",
location: i % 4 === 2 && i < 11 ? "office" : "remote",
done: DONE[i],
ticket: i >= 11 ? "PAY-231 · Retry backoff" : i >= 6 ? "PAY-219 · Webhook replay" : "PAY-188 · Checkout copy",
blockers: i >= 12 ? "Waiting on sandbox credentials from the Risk team" : "None",
pairing: i % 3 !== 0,
} });
t = at(prevWeekday(t), 9, [4, 9, 12, 7, 15, 2, 11][i % 7]);
}
main.previous = { count: 41, loops };
// Today, before anyone clicked: the date is set, and two answers were already typed.
document.getElementById("sa-date").value = today();
const done = document.getElementById("sa-done");
done.value = "Merged the retry backoff (PR #415) and updated the runbook.";
done.dispatchEvent(new Event("input", { bubbles: true }));
const office = form.querySelector('input[name="location"][value="office"]');
office.checked = true;
office.dispatchEvent(new Event("change", { bubbles: true }));
main.addEventListener("mv-carry", (e) => say(`mv-carry · ${e.detail.source} · ${e.detail.fields.join(", ")}`));
main.addEventListener("mv-loop-saved", (e) => {
say(`mv-loop-saved · loop ${e.detail.loop} · ${e.detail.streak}-day streak`);
next.disabled = false;
});
main.addEventListener("mv-review-required", (e) => say(`mv-review-required · ${e.detail.fields.join(", ")}`));
form.addEventListener("submit", (e) => e.preventDefault()); // a fetch() would go here
// Simulated tomorrow: every stored loop moves one weekday back, the form starts empty.
next.addEventListener("click", () => {
const p = main.previous;
main.previous = { count: p.count, loops: p.loops.map((l) => ({ ...l, t: prevWeekday(l.t) })), dates: p.dates.map(prevWeekday) };
form.reset();
document.getElementById("sa-date").value = today();
next.disabled = true;
say(`New day · loop ${main.loop}`);
});
review.addEventListener("change", () => { main.requireReview = review.checked; });
// Handover: one loop per shift, the unchanged answers carried as soon as the form opens.
const shift = (daysAgo, h, m) => at(Date.now() - daysAgo * 864e5, h, m);
ho.previous = { count: 212, loops: [
{ t: shift(1, 14, 3), values: { status: "issues", next: "Tomás Herrera", issues: "Label printer jams about every 2 hours. Spare rolls are in cabinet B.", units: "1840" } },
{ t: shift(1, 22, 1), values: { status: "issues", next: "Yuki Tanaka", issues: "Label printer jams about every 2 hours. Spare rolls are in cabinet B.", units: "1712" } },
{ t: shift(0, 6, 2), values: { status: "issues", next: "Ines Duarte", issues: "Label printer jams about every 2 hours. Spare rolls are in cabinet B. Maintenance booked for Thursday.", units: "1655" } },
] };
ho.carry();
document.getElementById("sa-ho-form").addEventListener("submit", (e) => e.preventDefault());
</script>
</div>Cultural reference
Groundhog Day — Harold Ramis (1993, film). A man wakes up to the same day again and again, and each loop he reuses everything he learned in the previous ones, so the repeated day gets faster and better. In the UI, a recurring form remembers the previous loop and offers each answer again, field by field, while anything carried over stays visibly marked until the user confirms it.
API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
key | string | <pathname>:<form id | name | action> | Storage key of the loop history (stored as mv-same-again:<key>). Set it per form and per person when several recurring forms share a URL. |
storage | local | session | none | local | Where loops are kept in the browser. none keeps nothing locally: feed the history with the previous property and persist it from mv-loop-saved. |
cadence | daily | weekdays | weekly | none | daily | Rhythm of the loop. A resubmission in the same period (day, ISO week) updates the last loop instead of counting a new one; the streak counts consecutive periods (weekdays skips Saturday and Sunday). none: every submission is a new loop and no streak is shown (shift handovers). |
keep | number | 10 | How many loops are retained (the loop counter keeps counting beyond it). |
volatile-after | number | 3 | A field whose last N answers are all different is considered volatile and gets no suggestion (a “what I did yesterday” field). 0 or 1 disables the rule; data-same-again="always" overrides it per field. |
max-age | time ("30d", "2w", "12h", ms number) | Beyond this age the last loop is too old to suggest: the bar says so and no field is offered. No limit by default. | |
require-review | boolean | Submit is blocked (preventDefault + stopImmediatePropagation in the capture phase, before your handlers) while carried-over fields are unconfirmed: the bar turns into a review message listing them, focus moves to the first one and mv-review-required fires. For fetch flows outside a <form>, call checkReview(). | |
tab-accepts | boolean | Tab also reuses the suggestion of an empty text field (focus stays; the next Tab moves on). Off by default so Tab always navigates. | |
suggest | inline | chip | inline | inline shows the previous answer as a tinted placeholder inside empty text fields (the chip then reads “Same as yesterday →”); chip keeps the author’s placeholder and shows the value in the chip only. |
save-on | submit | manual | submit | submit stores a loop on every submit that the review gate lets through (even if your handler calls preventDefault for a fetch). manual: call saveLoop() once your server has accepted the answers. |
locale | BCP 47 tag | en-US | Locale of weekdays, dates and times (“Friday”, “Sep 12”, “9:12 AM”). |
data-same-again (on a field or ancestor) | never | always | manual | never: not stored and never suggested (dates, “blocker resolved”, signatures). always: suggested even when volatile, and allowed on date/time inputs (excluded by default). manual: suggested field by field but left out of “Same as yesterday”. | |
data-same-again-bar (on a descendant) | boolean | An element that receives the loop bar (a card header…) instead of the top of the component. | |
data-same-again-state (set on controls) | suggest | carried | volatile | Styling hook written by the component on each control. data-same-again-ghost is set while the placeholder shows the previous answer. |
Properties
| Name | Type | Description |
|---|---|---|
previous | { t, values, loop? } | Array<{ t, values, loop? }> | { count, loops, dates? } | History from your server (t as ms, Date or ISO string; values keyed by field name in the shapes below; dates optionally lists older submission times so the streak can outlive the loops you send). Takes precedence over storage and is updated in memory by saveLoop(). Value shapes: string for text and single selects, string | null for radio groups, boolean for a lone checkbox or switch, string[] for checkbox groups and multiple selects, the element’s value for form-associated custom elements. |
loops | Array<{ t, values, loop? }> | Stored loops, oldest first (read-only copy). |
loop | number | Number of the loop being filled now: the last loop’s number when it belongs to the current period, the next one otherwise (read-only). |
streak | number | Consecutive periods with a loop, ending with the current period or the previous one (read-only). |
carried | string[] | Names of the carried-over fields not confirmed yet (read-only). |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (loop, first, last, again, stale, saved, updated, streak, day, week, today, yesterday, same, sameLabel, chipLabel, run, carried, confirm, confirmLabel, undoField, undoFieldLabel, undo, carriedNote, carriedNoteReview, review, describeKey, describe, describeCarried, keyRight, keyLeft, keyTab, checked, unchecked, on, off, empty, announce*). English defaults. |
Methods
| Name | Description |
|---|---|
carry(names?) | Same as the bar button: carries the previous answers into the untouched, unchanged, non-volatile fields (or only the given names) and marks them as carried over. Emits mv-carry (source "api"). Returns the names carried. |
accept(name) | Reuses the previous answer for one field as a deliberate, already confirmed choice. Emits mv-carry (source "api"). Returns false when there is no suggestion or it was cancelled. |
confirm(name?) | Confirms one carried-over field, or all of them. Emits mv-confirm. |
undo(name?) | Puts carried-over fields (all, or one) back to what they held before the carry, with input/change events. |
checkReview() | With require-review: true when nothing carried is left unconfirmed, otherwise shows the review message, focuses the first field, emits mv-review-required and returns false. |
saveLoop() | Stores the current answers as a loop now (replacing the last one in the same period), resets touched/carried state, emits mv-loop-saved. Returns { loop, t, values }. |
clearHistory() | Forgets every stored loop (browser storage and the previous property). |
Events
| Name | Description |
|---|---|
mv-carry | Cancelable, before any reuse. detail: { fields, values, source: "key" | "chip" | "bulk" | "api", from: { t, loop } }. preventDefault() cancels; editing or deleting entries of detail.values rewrites what is written (bump a week number, drop a field). |
mv-loop-saved | A loop was stored. detail: { loop, t, values, updated (same period replaced), streak, loops, dates }. Persist it to your server here when storage is none. |
mv-confirm | Carried-over fields were confirmed. detail: { fields }. |
mv-review-required | Submit (or checkReview()) was blocked by require-review. detail: { fields }. |
Content structure
| Name | Description |
|---|---|
(children) | A <form> or any region with named controls (the component may also sit inside a form). Controls are grouped by name (or id); the chip is appended inside the closest .mv-field, fieldset or [data-same-again-field], or right after the control. |
CSS classes
| Name | Description |
|---|---|
mv-same-again-bar | Loop bar (role=group): .mv-same-again-loop (icon + “Loop 42”), -info (-meta, -streak with -dots / -dot[data-done][data-current] and -streak-text), -same (mv-button with -count) and -note (carried-over / review message with Undo; data-blocked when submit was refused). data-saved after a save. |
mv-same-again-slot | Under each field (data-state="suggest | carried", data-focus while its field is focused): .mv-same-again-chip (-icon, -when, -value, -run, -key keycap) or .mv-same-again-badge (-tag, then .mv-same-again-link buttons Confirm and Undo; data-blocked during review). |
[data-same-again-flash] | Set for ~0.7 s on a control whose value was just reused: its text settles from the ghost color, staggered across a bulk carry. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-same-again-color | var(--mv-accent) | Tint of chips, carried-over badges and fields, streak dots and the loop pill. |
--mv-same-again-ghost | accent mixed into --mv-fg-subtle | Color of the inline ghost placeholder and of the landing animation. |
--mv-same-again-review | var(--mv-warning) | Color of the review message and of the badges of fields blocking submit. |
Accessibility
Nothing is ever filled or submitted behind the user’s back: suggestions are only offered, “Same as yesterday” acts on a click, and carried-over answers stay marked in text (“Carried over from yesterday”, not only a tint) until confirmed or edited. Each control with a suggestion gets an aria-describedby pointing to a hidden description that names the previous answer and the shortcut (“Answer from yesterday: Remote. Press Right Arrow to reuse it, Escape to dismiss.”; the button path is described for selects, radios and checkboxes), replaced by “Carried over from yesterday, not confirmed yet.” after a carry and removed once the field is settled. The shortcut only acts in an empty text field with no modifier (Left Arrow in right-to-left text), so caret movement is never hijacked, and Tab keeps navigating unless tab-accepts is set; the tip is written in the demo and in every chip’s keycap. Chips, Confirm and Undo are real <button>s whose accessible names contain their visible text plus the field (“Same as yesterday: Remote”, “Confirm Blockers”). Reuse, carry (with the list of fields), confirmation, undo, dismissal and saving are announced in a polite live region; a blocked submit is announced assertively with the fields to review and focus moves to the first one. The bar is a labelled group (“Loop 42”) and its streak dots are aria-hidden next to the streak written in words. Reused values are written through native setters and dispatch input/change, so framework state and validation stay in sync, and the author’s placeholder is restored as soon as the ghost is no longer needed. Reduced motion (OS or data-motion="reduce"): no landing, chip or note animation. Forced colors: dashed system outline on carried-over controls, bordered chips and tags, Highlight streak dots.