🌐 한국어
Class-based, framework-neutral CSS components. Everything references semantic
tokens only (var(--dds-*)) — load
@devslab/dds-tokens/tokens.css first, then @devslab/dds-css/dds.css
(or per-component files from components/). Dark mode needs no component
change: set data-theme="dark" on <html> or any subtree
(data-theme="light" pins a subtree back).
Every interactive component defines the full state set of spec §4.2 (default → hover → pressed → focus-visible → disabled → loading) and passes the §6 accessibility floor: 4.5:1 text contrast, ≥24×24 web click targets, keyboard reachability with a visible 2px focus ring, no color-only signaling.
Live reference: preview/components.html (run pnpm build first).
Shipped (spec §4.3 v1 inventory): Button, IconButton, TextField, Textarea, Select, Checkbox/Radio, Switch, Badge, Chip, Avatar, Spinner, Skeleton, Divider, Card, ListRow, Tabs, Dialog (web Modal), Toast, Tooltip (web), EmptyState. BottomSheet is deliberately absent: it is the native counterpart of Dialog, and this package is web CSS — the role mapping (Modal on web ↔ BottomSheet on native) is documented in spec §4.3/§5 and the native implementation waits for a native consumer (Phase 3).
<button class="dds-btn dds-btn--primary">Save</button>
<button class="dds-btn dds-btn--secondary dds-btn--sm">Cancel</button>
<button class="dds-btn dds-btn--primary" aria-busy="true">
<span class="dds-spinner" aria-hidden="true"></span>Saving…
</button>- Variants:
--primary--secondary--ghost--danger. Sizes:--sm(32px) / default (40px) /--lg(48px) — heights from the spacing scale. - States: hover/pressed via tokens (
bg.brand-hover, alpha tints); disabled =[disabled](opacity 0.45 — the one allowed opacity state); loading =aria-busy="true"+ a.dds-spinnerchild (blocks pointer events; the accessible name stays, e.g. "Saving…"). - A11y: primary text is
on-brand(zinc.950, ~8:1 — spec §3.1 note); danger text ison-status(white/light, dark/zinc.950); focus ring 2pxborder.focusoffset 2px; icon-only buttons must addaria-label. - Links as buttons:
<a class="dds-btn dds-btn--primary" href="…">gets the same look — the base rule resets the anchor underline, so no consumer shim is needed. - Do: one primary per view; pair danger with a confirm step.
- Don't: don't fake a disabled state with a ghost variant — use
[disabled]so it's out of the tab order and announced correctly.
<button class="dds-iconbtn" aria-label="Close">
<svg aria-hidden="true" width="20" height="20">…</svg>
</button>
<button class="dds-iconbtn dds-iconbtn--secondary dds-iconbtn--sm" aria-label="Edit">…</button>
<button class="dds-iconbtn dds-iconbtn--danger" aria-label="Delete source">…</button>- Variants: default (ghost) /
--secondary(bordered) /--danger. Sizes:--sm(32) / default (40) /--lg(48) — the same heights as.dds-btn, so an icon button never sits a few px off the text button beside it. - The icon inherits
currentColor; give the<svg>aria-hidden="true". - A11y:
aria-labelis mandatory — an icon has no accessible name.--sm(32×32) clears the 24×24 web floor but not the 44×44 touch target: on touch surfaces use the default size. - Do: pair with a Tooltip for discoverability, but keep the
aria-labelas the real name (the tooltip isaria-describedby, not the label). - Don't: don't mix sizes inside one action row — pick one and keep every row action on it.
<div class="dds-field">
<label class="dds-field__label" for="email">Email</label>
<input class="dds-input" id="email" type="email"
aria-invalid="true" aria-describedby="email-help">
<span class="dds-field__help" id="email-help">Not an email address.</span>
</div>- Error:
.dds-field--erroron the wrapper (oraria-invalid="true"on the input — both style the border; use aria-invalid so AT hears it too). - A11y: always a real
<label for>; error/help text linked viaaria-describedby; the red border is never the only error signal — the help line carries the message (§6 no-color-only). - Do: keep help text present in both normal and error states so the layout doesn't jump.
- Don't: don't use
placeholderas the label — it disappears on input and fails contrast as a label substitute.
<div class="dds-field">
<label class="dds-field__label" for="intro">Introduction</label>
<textarea class="dds-textarea" id="intro" rows="4"
aria-describedby="intro-help"></textarea>
<span class="dds-field__help" id="intro-help">Shown to visitors.</span>
</div>- Lives in the same
.dds-fieldwrapper as.dds-input: label, help and error (.dds-field--error/aria-invalid) behave identically. min-heightis two control heights,resize: vertical(horizontal resize breaks the grid the form sits in).- Do: give a textarea its own form row. Mixing a one-line control and a textarea in one row is a rejection criterion in this house — their heights cannot be reconciled without shrinking one of them.
- Don't: don't grow it by shrinking the control next to it; don't disable resize entirely when the content is genuinely long-form.
<div class="dds-field">
<label class="dds-field__label" for="locale">Language</label>
<span class="dds-select">
<select class="dds-select__input" id="locale">
<option>한국어</option><option>English</option>
</select>
</span>
</div>- A real
<select>(keyboard, mobile picker, form semantics) with the native arrow replaced by the wrapper's chevron. Leaving the OS arrow in place is a rejection criterion — the same control then looks different on every platform and mismatches the input next to it. - The wrapper
.dds-selectis required (it draws the chevron); the class on the element itself is.dds-select__input. - A11y: label via
<label for>like any field; the chevron ispointer-events: noneso clicks always reach the select. - Do: build option lists from the data (distinct values), not a hardcoded list.
- Don't: don't replace a
<select>with a div+listbox unless you are also implementing the full ARIA listbox pattern.
<label class="dds-check">
<input class="dds-check__input" type="checkbox" checked>
Send me the weekly digest
</label>
<label class="dds-check">
<input class="dds-check__input" type="radio" name="cadence" value="daily">
Daily
</label>- One class for both: the input's
typepicks the shape (square + tick vs circle + dot).:indeterminateis styled for "some selected" headers. - The tick/dot is
on-brand(zinc.950), never white — same rule as the primary button (spec §3.1). - A11y: wrap the text in the
<label>so the whole row is the hit area (the 18px box alone is below the touch floor); disabled dims the label once, not the box twice. - Do: radio for one-of-many that takes effect on submit; checkbox for independent options.
- Don't: don't use a checkbox for a setting that applies immediately — that is a Switch.
<label class="dds-switch">
<input class="dds-switch__input" type="checkbox" role="switch" checked>
Handoff alerts
</label>- An immediate on/off. Track 36×20, knob 16 (glyph geometry, not spacing).
The knob turns
on-brandon the brand-filled track — the same rule that keeps white off cyan. - Transitions are removed under
prefers-reduced-motion. - A11y:
role="switch"on the input so AT announces on/off rather than checked/unchecked; the label text is the accessible name. - Do: apply the change immediately and report failure with a toast — silent no-op is a rejection criterion.
- Don't: don't put a Save button next to a switch; if the value needs saving, it's a checkbox.
<span class="dds-badge dds-badge--success">Connected</span>- Variants:
--brand--success--warning--danger--info. - Colors are always the paired set (dark foreground +
-bgtint, spec §3.1); the leading dot repeats the tone so color isn't the only signal. - Do: use for state, not for actions.
- Don't: don't invent new fg/bg combinations — the pairs are the tokens'
contract (
status.*+status.*-bg).
<button class="dds-chip" aria-pressed="true">Unanswered</button>
<button class="dds-chip">Web pages</button>
<button class="dds-chip" disabled>Archived</button>- A small interactive control: filter toggle, suggested question,
removable tag. Badge and Chip are different components on purpose — a badge
is state you read, a chip is something you press. Never restyle one into
the other (a duplicated
.chipdefinition has already cost this house a live bug). - Selected =
aria-pressed="true"(announced);--selectedexists only for markup that can't usearia-pressed(e.g. a link chip). - Do: keep chips one line high and let the row scroll; label them with the value, not "Filter 1".
- Don't: don't use a chip as the primary action of a view — that's a Button.
<span class="dds-avatar" aria-hidden="true">KS</span>
<span class="dds-avatar dds-avatar--lg">
<img class="dds-avatar__img" src="/u/12.jpg" alt="">
</span>
<span class="dds-avatar dds-avatar--square dds-avatar--sm" aria-hidden="true">DL</span>- Sizes 32 / 40 / 48 from the spacing scale, so an avatar lines up with the
control of the same size in a list row.
--squarefor company/tenant marks (a logo in a circle gets clipped). - Initials use the brand-subtle pair, so they re-tint with the product brand.
- A11y: the avatar is decorative when the name is already in the row —
aria-hidden="true"on initials,alt=""on the image. Only give it an alt/label when it is the only identification. - Do: derive initials from the same name string the row shows.
- Don't: don't put status text inside the avatar — use a Badge next to it.
<span class="dds-spinner" role="status" aria-label="Loading"></span>
<div aria-busy="true">
<span class="dds-skeleton dds-skeleton--circle" aria-hidden="true" style="width:40px;height:40px"></span>
<span class="dds-skeleton dds-skeleton--text" aria-hidden="true" style="width:60%"></span>
</div>- Spinner inherits
currentColor(correct on any surface, including inside a primary button). Standalone spinners needrole="status"+aria-label. Under reduced motion it slows instead of freezing — a stopped spinner reads as a hang. - Skeleton fill is an alpha tint (works on default and subtle backgrounds in
both themes). Size is the consumer's (width/height inline or via layout);
--textis one line,--circlefor avatars. Mark the regionaria-busy="true", keep skeleton elementsaria-hidden="true"; the pulse stops under reduced motion. - Do: skeleton for content-shaped waits, spinner for action-shaped waits.
- Don't: don't announce each skeleton to AT — one busy region is enough.
<hr class="dds-divider">
<div style="display:flex">A<hr class="dds-divider dds-divider--vertical">B</div>--verticalneeds a stretchable parent (flex row / grid cell).- Do: use
<hr>when the split is meaningful; addrole="presentation"when it is purely decorative. - Don't: don't use a divider where spacing already separates the groups — two separators in a row is visual noise.
<section class="dds-card">
<h3 class="dds-card__title">Reindex schedule</h3>
<p class="dds-card__body">Sources are recrawled daily at 04:00 KST.</p>
</section>
<section class="dds-card dds-card--subtle">…</section>- Border +
bg.elevated+elevation.1. The shadow token ships in both themes: on a zinc.950 page it resolves to almost nothing, which is the intended dark look — a component never branches on theme (spec §3.1). --subtleis the flat variant for a card inside an already elevated surface (a card in a dialog), where a second shadow reads as noise.- Do: one card = one subject; put its actions in a
.dds-cardfooter row of buttons. - Don't: don't nest full cards inside cards — use
--subtleor a Divider.
<ul style="list-style:none;margin:0;padding:0">
<li><button class="dds-listrow dds-listrow--interactive">
<span class="dds-avatar dds-avatar--sm" aria-hidden="true">KS</span>
<span class="dds-listrow__body">
<span class="dds-listrow__title">강신</span>
<span class="dds-listrow__sub">대표 · 초대함</span>
</span>
<span class="dds-badge dds-badge--success">활성</span>
</button></li>
</ul>- Slots: leading (any flex child),
__body(__title+__sub),__actions. Rows separate themselves (+ .dds-listrowdraws the rule). --interactivefor rows that are the control — use a real<button>or<a>. The focus ring is inset (a full-bleed row has no outer margin).- Long titles ellipsize; the row height stays constant.
- Do: make the row self-contained — if the information fits in the row, don't defer it to a detail page.
- Don't: don't put an inline accordion in a row that can be deleted or edited — a re-render after the action collapses it. Use a dedicated view.
<div class="dds-tabs" role="tablist">
<button class="dds-tab" role="tab" aria-selected="true" aria-controls="p1" id="t1">Sources</button>
<button class="dds-tab" role="tab" aria-selected="false" aria-controls="p2" id="t2">Visitors</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">…</div>- Ink-underline tabs, horizontally scrollable when they overflow (the mobile behavior of the admin shell).
- Selection is
aria-selected, never a class alone — the state must be announced, and the underline repeats it so color isn't the only signal. - A11y:
role="tablist"/tab/tabpanelwitharia-controls+aria-labelledby; arrow-key roving focus is the consumer's JS. - Do: group tabs by kind and put a new feature in the obvious existing group — a flat list of everything is just a stack with a new name.
- Don't: don't hide a tab's panel with a
displayrule that lacks a[hidden]guard (that exact collision once painted an empty state over a populated table).
<div class="dds-dialog-overlay">
<div class="dds-dialog" role="dialog" aria-modal="true" aria-labelledby="t">
<h2 class="dds-dialog__title" id="t">Reindex sources?</h2>
<p class="dds-dialog__body">Changes apply to answers immediately.</p>
<div class="dds-dialog__actions">
<button class="dds-btn dds-btn--ghost dds-btn--sm">Later</button>
<button class="dds-btn dds-btn--primary dds-btn--sm">Reindex</button>
</div>
</div>
</div>- Native
<dialog class="dds-dialog">is also styled (incl.::backdrop). - Behavior contract (spec §5): closes on outside tap and Esc; focus moves in
on open, is trapped while open, returns to the opener on close — that's
the opening code's job (native
<dialog>.showModal()gives you most of it). - A11y:
aria-modal="true"+aria-labelledbyto the title. - Do: actions right-aligned, primary last (matches the modal convention used across DevsLab admin surfaces).
- Don't: don't stack dialogs; don't use a dialog for non-blocking notices — that's the toast's job.
<div class="dds-toast-region" role="status">
<div class="dds-toast dds-toast--success">Indexing finished.</div>
<div class="dds-toast dds-toast--danger" role="alert">Sending failed.</div>
</div>- Region is fixed bottom-end; toasts stack with a grid gap. Variants add the
status dot (same 6px dot as the badge). Entry animation uses
duration.base+easing.enterand is removed under reduced motion. - A11y: region
role="status"(polite); userole="alert"only on danger toasts. Auto-dismiss should pause on hover/focus (consumer JS). - Do: every failed action gets a toast — silent failure is a rejection criterion in this house.
- Don't: don't put required actions only inside a toast (it disappears); don't stack more than ~3 — collapse older ones.
<span class="dds-tooltip">
<button class="dds-iconbtn" aria-label="Recrawl" aria-describedby="tp1">
<svg aria-hidden="true" width="20" height="20">…</svg>
</button>
<span class="dds-tooltip__bubble" role="tooltip" id="tp1">Recrawl now</span>
</span>- Web only (spec §4.3). On native the same information goes in a label or a sheet — there is no hover.
- Opens on hover and
:focus-within, so it is keyboard-reachable; the bubble sits onbg.inverse/text.on-inverse(D-011). - A11y: reference it with
aria-describedby, notaria-label— the control keeps its own name, and a describedby bubble reaches AT even when it never visually opens. - Do: use it for the name of an icon-only control or a short unit hint.
- Don't: never put information required to use the control in a tooltip — it is unavailable on touch and disappears on scroll.
<div class="dds-empty">
<p class="dds-empty__title">등록된 자료가 없습니다</p>
<p class="dds-empty__desc">웹페이지 주소나 PDF를 추가하면 AI가 그 내용으로 답합니다.</p>
<div class="dds-empty__actions">
<button class="dds-btn dds-btn--primary dds-btn--sm">자료 추가</button>
</div>
</div>- The empty view keeps the same skeleton as the filled one — summary chips and filters stay put; the empty state replaces the rows, not the screen.
- Always name the next action in
__actions; an empty state without a way forward is a dead end. - A11y: it is ordinary content, not an alert. When it appears after a search, put the result count in the live region instead.
- Do: distinguish "nothing yet" (onboarding copy) from "nothing matched" (offer to clear the filter).
- Don't: don't toggle it with a
displayrule that outranks[hidden]—.dds-empty[hidden]is guarded here for exactly that reason.
The two sections below shipped after the spec §4.3 v1 inventory above and
are not part of it — Table and Console shell arrived with the console
migration into DDS (docs/decisions.md D-021). They follow the same
reference conventions as every section above (real shipped classes only,
usage snippet, A11y notes, Do/Don't).
<div class="dds-table-wrap">
<table class="dds-table dds-table--fixed">
<caption class="dds-visually-hidden">Jobs</caption>
<colgroup>
<col style="width: 60%" />
<col data-fold style="width: 40%" />
</colgroup>
<thead>
<tr>
<th scope="col" data-column="name" aria-sort="ascending">
<button type="button" class="dds-table__sort" aria-label="Sort by Name">
Name
<span class="dds-table__sort-mark" aria-hidden="true" data-sort-state="ascending"></span>
</button>
</th>
<th scope="col" data-column="count" data-fold data-numeric>Count</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Reindex</th>
<td data-fold data-numeric>12</td>
</tr>
</tbody>
</table>
</div>
<nav class="dds-table__pagination" aria-label="Next page">
<a class="dds-btn dds-btn--secondary" href="?cursor=abc">Next page</a>
</nav>- Wrap:
.dds-table-wrapscrolls horizontally on overflow and is the positioned ancestor an absolutely-clipped.dds-visually-hiddencaption resolves against;--tallcaps it at 70vh and pins the header (thead th) withposition: sticky, and switches its table toborder-collapse: separate(zero spacing) — collapsed borders can keep a sticky header from sticking. - Density and layout:
.dds-table--denseshrinks row height from the--dds-space-40token (40px) to 2.25rem (36px);.dds-table--fixedswitches totable-layout: fixedand expects a<colgroup>— the component applies both automatically the moment any column declares awidth. - Sort:
.dds-table__sortis a sortable header's button (rendered only for a client-sortable column);.dds-table__sort-markis its chevron, itsdata-sort-stateattribute holding"none"/"ascending"/"descending"— the same three values the header's ownaria-sortcarries. .dds-table__orderrenders the stated-order sentence above the table when the caller supplies one instead of a sort button (see thesortprop below)..dds-table__actionsright-aligns the trailing actions cell's content; the actions column's own header uses the.dds-visually-hiddenutility so it still has an accessible name without a visible one.- Row actions: a
.dds-btninside.dds-table__actionsis 32px whatever its size modifier and never wraps, and the actions cell trims its block padding to 4px — a row with a button is as tall as the same row without one. Any cell holding a.dds-btn--sm(a copy button beside an id) trims its padding the same way. In.dds-table--dense, row actions and every.dds-btn--smin a cell are 24px. On a coarse pointer all of them return to the 44px touch floor and the row grows instead (D-025). - A
<code>in a cell takes the mono family at 13px, for ids, keys and hashes read character by character. .dds-table__paginationis a labeled<nav>wrapping a plain.dds-btn.dds-btn--secondarylink — never a button — so a cursor-paged next page keeps working with JavaScript off; it is[hidden]entirely when there is no next page.- Attribute hooks:
[data-fold]marks a column (<col>,<th>,<td>) droppable — hidden outright below56.25rem(900px) by a plain media query, no JavaScript;[data-numeric]right-aligns a cell and gives it tabular figures;[data-sort-state](above) carries the three sort states;[data-column]names the column for a consumer's own hooks and carries no CSS of its own. - A11y: the caption is real, only visually hidden; a sortable header is
a real
<button>witharia-label, never a bare clickable<th>; exactly one column per table should render as<th scope="row">rather than<td>. - Do: give every table a caption, even a visually hidden one — it is the table's accessible name.
- Don't: don't offer a sort button on a table whose data is server-
ordered — a client sort of one paged slice would misreport the whole
list's order, so this markup never renders one in that case (
sort's{ statedOrder }form; see@devslab/dds-table's README). - Don't: don't re-size row actions or cell buttons in product CSS — the table already holds them to its row height, and a product override out-specifying these rules will also defeat their touch-floor restatement.
<div class="dds-console-shell" data-surface="dashboard" data-hydrated="true">
<a class="dds-console-skip dds-visually-hidden" href="#dds-console-main">Skip to content</a>
<button class="dds-console-rail__toggle" type="button" aria-controls="dds-console-rail" aria-expanded="false">
<span aria-hidden="true"></span>
</button>
<div class="dds-console-rail__scrim" data-open="false"></div>
<aside id="dds-console-rail" class="dds-console-rail" data-open="false">
<a class="dds-console-rail__brand" href="/">
<img src="/mark.svg" alt="" width="20" height="20" /><strong>Acme</strong>
</a>
<nav class="dds-console-rail__nav" aria-label="Dashboard navigation">
<section class="dds-console-rail__group" aria-labelledby="g1">
<h2 id="g1" class="dds-console-rail__group-label">Today</h2>
<ul>
<li><a class="dds-console-rail__item" href="/overview" aria-current="page">
Overview
<span class="dds-console-rail__badge" aria-label="3 pending">3</span>
</a></li>
</ul>
</section>
</nav>
</aside>
<main id="dds-console-main" class="dds-console-main" tabindex="-1">
<header class="dds-console-page-header">
<div class="dds-console-page-header__text">
<p class="dds-console-page-eyebrow">Acme</p>
<h1>Overview</h1>
</div>
</header>
…
</main>
</div>- Layout:
.dds-console-shellis the 14.5rem (232px)-rail-plus-content CSS grid; below56.25rem(900px) — the same breakpoint the table's[data-fold]uses, so a rail and a table folding column change shape at the same viewport width — the grid collapses to one column and.dds-console-railbecomes a fixed, off-canvas drawer. - Rail parts:
.dds-console-rail__brand,__context(a<dl>of label/value rows),__nav, one__groupper labeled section (+__group-label),__item(with hover /:focus-visible/aria-current="page"states),__badge(a pending-count pill), and__foot(a bottom slot, pinned withmargin-block-start: auto). - Drawer (active only below 900px):
.dds-console-rail__toggleisdisplay: noneabove the breakpoint and the fixed hamburger button below it — it exposes its state asaria-expanded, notdata-open, since it's a disclosure control rather than the thing being disclosed; its own accessible name (see the visually-hidden section below) switches with that same state..dds-console-rail__scrimis likewise hidden above the breakpoint and a full-screen dimmer below it. The scrim and the rail itself (not the toggle) readdata-open="true"ordata-open="false"— always one or the other, never absent — which is what lets the drawer's open/close CSS (translate,visibility, the scrim's opacity/pointer-events) target[data-open="true"]directly rather than relying on the attribute's presence, the way[data-fold]or[hidden]do elsewhere in this catalog.prefers-reduced-motion: reduceremoves the drawer's transition. data-surfacenames the console ("dashboard","admin", …) for a consumer's own hooks and carries no CSS.data-hydrated="true"appears only once client-side hydration has actually run — it is absent, not"false", on first paint and in any environment hydration never reaches..dds-console-mainis the skip link's target (id="dds-console-main",tabindex="-1") and holds.dds-console-page-header—__text(.dds-console-page-eyebrow+<h1>) and, when the caller supplies one,__actionsto the header's right.- Density: any
.dds-btn,.dds-input,.dds-select, or.dds-textarearendered inside.dds-console-shellrenders at the body-2 (14px) size instead of its own default — a console establishes this once for everything inside it rather than each control overriding itself. - A11y: the skip link is real markup, hidden with
.dds-visually-hiddenuntil it receives focus; the rail's<nav>requires a caller-suppliedaria-label— a console page also has the table's own pagination<nav>(above), so an unnamed landmark next to a named one would be a regression; every class in this stylesheet that setsdisplayre-declares[hidden] { display: none }beside it, including inside the drawer media query, so a hidden rail/toggle/scrim can never be forced back on screen regardless of viewport width. - Do: use the badge for a real pending count, not a decorative dot — its accessible name is filled from a caller-supplied label template, not a word this package chose.
- Don't: don't reach for
[hidden]to open or close the drawer — the component already ownsdata-open, and the CSS above depends on that exact string value, not on the attribute's presence or absence.
Visually hidden — .dds-visually-hidden
<span class="dds-visually-hidden">Actions</span>- A utility in
base.css, not tied to any one component — it clips content to a 1px box (clip-path: inset(50%)) so it takes no visual space but stays in the accessibility tree, unlike[hidden](ordisplay: none), which removes an element from the tree too. - Its containing block should be
position: relativefor the clip to resolve against that box rather than the page —.dds-table-wrapsets this for exactly that reason, so the table's own caption clips correctly even while the wrap is scrolled horizontally. - Shipped consumers: the table's caption and its actions-column header
above; the console shell's skip link (visible only once it receives
focus, via its own
:focusrule); and the console shell's rail-toggle button, whose accessible name (themenuOpen/menuCloselabel) is a visually-hidden span that swaps text as the drawer opens and closes. - Do: use it for text a screen reader needs but a sighted user does not — a caption, a landmark's accessible name, an icon-only control's label.
- Don't: don't reach for it to hide something temporarily from
everyone — that removes nothing from the accessibility tree, which is
exactly wrong for content nobody should perceive right now; use
[hidden]for that instead.