acme-assets-prod
Bucket · Sample region A
- Objects
- 1,204
- Stored
- 62.4 GB
- Environment
- Production
- Status
- Active
Tactile Precision
A calm, precise interface language for cloud and object-storage infrastructure. Tokens, components and infrastructure patterns in plain HTML, CSS and JavaScript, built to be read by people and by AI coding agents.
No build step is required. Link one stylesheet and the scripts you need; icons are an inline SVG sprite.
<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Buckets · Afara</title>
<link rel="stylesheet" href="styles/afara.css">
</head>
<body>
<!-- inline the icon sprite (assets/icons/sprite.inline.html) as the first child of body -->
<main class="af-container">…</main>
<script src="scripts/theme.js"></script>
<script src="scripts/icons.js"></script>
<script src="scripts/component-interactions.js"></script>
<script src="scripts/component-widgets.js"></script>
</body>
</html>Start
Infrastructure should be a foundation, not a barrier. The interface has to feel dependable before it feels beautiful.
Afara's interface is quiet, exact and physical. Controls behave like well-made instruments: a button is a key that you press in, a field you type into is recessed, a panel is paper on a bench. Structure is drawn with thin, deliberate lines and tonal steps, never with glow, gradients or decorative shadow.
Precision is a promise as much as a look. Numbers align in tabular columns, units are stated, the resource you are acting on is always named, and a status is never inferred from colour alone.
Reliability, security and clarity outrank feature claims. No ambient animation, no decoration that implies infrastructure Afara does not operate.
Progressive disclosure: the common path is short; advanced controls are one step away, never hidden.
Contrast, keyboard operation, focus and reduced motion are properties of every component, not an audit at the end.
Plain HTML, CSS and JavaScript. Tokens in an open standard. No framework lock-in.
Precise language, specific error messages and predictable behaviour are part of the design.
There are exactly four ways a surface can sit in space. Every component uses one of them.
| Treatment | What it looks like | Used for |
|---|---|---|
| Flat | Hairline border, no shadow | Cards, tables, panels. About 90% of surfaces. |
| Recessed | Inset shadow | Inputs, code, meter tracks, wells. |
| Raised | Light top edge, hard 1px lower edge | Buttons and other controls. Never cards. |
| Floating | Soft elevated shadow plus a border | Menus, popovers, drawers, dialogs, toasts. |
Do write
“Upload files directly to your storage bucket using a short-lived, secure URL.”
“Upload failed: backup.tar.zst is larger than the 5 GB single-request limit. Use multipart upload.”
Don't write
“Experience the future of limitless, next-generation file management.”
“Something went wrong.” · “Success!” · “Unlimited”, “instant”, “zero downtime”, “the fastest”.
Source of truth: brand/brand.md v1.0. Where the brand document and this system disagree, the brand document wins and the disagreement is recorded in DESIGN_SYSTEM.md.
Foundations
Three tiers of tokens, seven cascade layers, one naming scheme. Everything else in the system is built on these.
| Tier | Meaning | May reference | Example |
|---|---|---|---|
| Primitive | A raw value with no meaning attached | Primitives only | --prim-color-teal-600, --space-4 |
| Semantic | A role: what the value is for | Primitives and semantic tokens | --color-accent-solid, --color-border-control |
| Component | A decision for one component | Semantic tokens for colour; any non-colour token | --input-border-focus, --modal-radius |
Colour primitives are private (--prim-color-*) and are never used in component CSS. Components read semantic or component tokens, which is what makes the dark theme a designed theme rather than an inversion.
88 component tokens in 22 groups. Each one aliases a semantic token (colour) or a primitive (everything else).
--button-height-xs--button-height-sm--button-height-md--button-height-lg--button-height-touch-xs--button-height-touch-sm--button-padding-x-xs--button-padding-x-sm--button-padding-x-md--button-padding-x-lg--button-gap--button-radius--button-font-size-xs--button-font-size-sm--button-font-size-md--button-font-size-lg--button-font-weight--input-height-sm--input-height-md--input-height-lg--input-padding-x--input-radius--input-bg--input-border--input-border-hover--input-border-focus--input-border-invalid--input-border-valid--input-placeholder--input-shadow--card-radius--card-padding-sm--card-padding-md--card-padding-lg--card-border--card-border-hover--card-gap--badge-height--badge-padding-x--badge-radius--badge-gap--table-row-height-comfortable--table-row-height-compact--table-row-height-dense--table-header-height--table-cell-padding-x--table-border--modal-radius--modal-width-sm--modal-width-md--modal-width-lg--modal-padding--modal-shadow--drawer-width--drawer-padding--drawer-shadow--sheet-radius--sheet-max-height--popover-radius--popover-min-width--popover-shadow--tooltip-radius--tooltip-max-width--tooltip-bg--tooltip-text--toast-width--toast-radius--check-size--check-radius--switch-width--switch-height--switch-thumb--slider-track--slider-thumb--menu-item-height--menu-item-radius--tab-height--meter-height--meter-height-lg--meter-radius--progress-height--pip-size--avatar-size-sm--avatar-size-md--avatar-size-lg--file-row-icon-size--nav-item-height--nav-item-radiusOne stylesheet entry, styles/afara.css, declares the layer order. A later layer always wins regardless of selector specificity, so product code needs no !important.
@layer afara-reset, afara-base, afara-type, afara-layout,
afara-components, afara-patterns, afara-utilities;
/* Unlayered product CSS beats every layer above. */af- + block, __ element, -- modifier: .af-card__header, .af-button--primary.data-*, never .is-* classes: aria-current="page", aria-expanded, [disabled], data-level="warning".--_bg) and are private to the component.--value, --threshold, --share. Design values are never inline.Light is the default and is applied unless data-theme="dark" is present. Dark is a designed theme: surfaces step through a deep teal ramp, borders are re-balanced, and status colours are re-derived so every pair still meets contrast. The attribute works on any element, so a dark band can sit inside a light page.
Surface, text, border and status colours all come from semantic tokens.
Surface, text, border and status colours all come from semantic tokens.
<div class="af-card"><div class="af-card__body af-stack af-stack--3">
<span class="t-component-title">Theme-aware card</span>
<p class="doc-muted">Surface, text, border and status colours all come from semantic tokens.</p>
<div class="af-cluster"><button class="af-button af-button--primary af-button--sm" type="button">Primary</button><button class="af-button af-button--secondary af-button--sm" type="button">Secondary</button><span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Warning</span></div>
</div></div>Foundations
A restrained palette: deep teal and warm neutrals carry the interface; green, amber, red and blue are reserved for status. Accent colour marks what is actionable or selected, never decoration.
teal-600 and teal-700 for accent text and uses Brand Teal only for large fills and marks.--color-border-control (WCAG 1.4.11).Raw values. Generated from tokens/tokens.json.
#0BA89E#073B37#47704C#5FD6C8#CDEBE7#FFFFFF#FFFFFF#F7FAF9#EEF4F2#E4EEEB#DCE8E5#BCCFCA#9DB5B0#7F9893#688580#5A736E#526B67#3A524E#1F403C#073B37#F2FAF8#CDEBE7#B5D8D2#9CC5BF#5FA9A0#2B7A72#1B625B#0F544E#0B4944#073B37#05302D#031F1D#E4F5F2#CDEBE7#B3E2DC#5FD6C8#18BDB2#10B5AA#0BA89E#0A8F86#087068#06524C#EAF4EB#B9D6BD#8FD39B#47704C#2E5634#0A3A2B#2F7A58#FEF4DC#F2D08A#F2C265#B45309#7A4100#3D3010#8A6A1F#FDECEA#F2B8B2#FFAFA6#FFC2BB#C42B1E#B42318#A3261B#8F1D13#3B0E0A#45201D#9A4A42#E8F0FA#B8CFEA#A3CBF7#2563A8#1B4F8F#12334F#3C6FA3#04A99C#CA7F00#5C93E3These are the tokens you use. The swatches are live: switch theme in the header and they change.
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--color-canvas | #F7FAF9 | #073B37 | Page background. | |
--color-surface | #FFFFFF | #0B4944 | Cards, tables, menus, form controls. | |
--color-surface-raised | #FFFFFF | #0F544E | Raised tactile controls and floating layers (popovers, menus). Separated by border + shadow, not by fill, in light. | |
--color-surface-inset | #EEF4F2 | #05302D | Recessed wells: code blocks, meter tracks, segmented-control tracks, inset panels. | |
--color-surface-hover | #F7FAF9 | #0F544E | Row and list-item hover. | |
--color-surface-selected | #E4F5F2 | #0F544E | Selected row/item background. | |
--color-surface-inverse | #073B37 | #CDEBE7 | Tooltips, toasts, code-on-dark panels, production env badge. | |
--color-surface-disabled | #EEF4F2 | #05302D | ||
--color-scrim | #031F1D | #000000 | Modal/drawer backdrop. |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--color-text | #073B37 | #F2FAF8 | Primary text. brand.md Main Text. | |
--color-muted-text | #526B67 | #B5D8D2 | Secondary text. brand.md Muted Text. | |
--color-subtle-text | #5A736E | #9CC5BF | Tertiary text, placeholders, metadata. Meets 4.5:1 on canvas and surface only — not on inset surfaces. | |
--color-disabled-text | #7F9893 | #5FA9A0 | Disabled controls (WCAG-exempt, kept legible). | |
--color-inverse-text | #FFFFFF | #073B37 | Text on surface-inverse and primary action. | |
--color-inverse-muted-text | #CDEBE7 | #3A524E | Secondary text on surface-inverse. | |
--color-link | #087068 | #5FD6C8 | ||
--color-link-hover | #06524C | #CDEBE7 | ||
--color-accent-text | #087068 | #5FD6C8 | Teal used as text or focus ring. ≥4.5:1. |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--color-border | #DCE8E5 | #1B625B | Decorative dividers and card outlines. brand.md Border. Not sufficient to identify a control. | |
--color-border-strong | #BCCFCA | #2B7A72 | Secondary button outline, emphasised dividers. | |
--color-border-control | #688580 | #5FA9A0 | Boundary of inputs, checkboxes, radios, switches. ≥3:1 on every surface (WCAG 1.4.11). |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--color-accent-solid | #0A8F86 | #5FD6C8 | Non-text accent: progress fill, selected indicator, switch-on track. ≥3:1 on canvas, surface and inset. | |
--color-accent-bg | #E4F5F2 | #0F544E | Subtle teal tint (info-ish selected backgrounds). | |
--color-accent-bg-strong | #CDEBE7 | #1B625B | Active navigation item, selected chip. |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--color-chart-1 | #0A8F86 | #04A99C | Series 1 — teal. Fixed order; assign by entity, never by rank. | |
--color-chart-2 | #B45309 | #CA7F00 | Series 2 — amber. | |
--color-chart-3 | #2563A8 | #5C93E3 | Series 3 — blue. Charts carry at most three series; fold the rest into Other. | |
--color-chart-other | #688580 | #9CC5BF | Folded remainder ("Other"). Neutral, never a fourth hue. | |
--color-chart-grid | #DCE8E5 | #1B625B | Gridlines and axes: hairline, solid, one step off the surface. |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--action-primary-bg | #073B37 | #CDEBE7 | ||
--action-primary-bg-hover | #0B4944 | #E4F5F2 | ||
--action-primary-bg-active | #031F1D | #B5D8D2 | ||
--action-primary-text | #FFFFFF | #073B37 | ||
--action-primary-border | #031F1D | #B5D8D2 | ||
--action-secondary-bg | #FFFFFF | #0F544E | ||
--action-secondary-bg-hover | #F7FAF9 | #1B625B | ||
--action-secondary-bg-active | #EEF4F2 | #0B4944 | ||
--action-secondary-text | #073B37 | #F2FAF8 | ||
--action-secondary-border | #BCCFCA | #2B7A72 | ||
--action-tertiary-bg | #E4F5F2 | #0B4944 | ||
--action-tertiary-bg-hover | #CDEBE7 | #0F544E | ||
--action-tertiary-bg-active | #B3E2DC | #05302D | ||
--action-tertiary-text | #073B37 | #5FD6C8 | ||
--action-ghost-bg-hover | #EEF4F2 | #0F544E | ||
--action-ghost-bg-active | #E4EEEB | #1B625B | ||
--action-ghost-text | #073B37 | #F2FAF8 | ||
--action-destructive-bg | #B42318 | #FFAFA6 | ||
--action-destructive-bg-hover | #8F1D13 | #FFC2BB | ||
--action-destructive-bg-active | #7A1810 | #F2B8B2 | ||
--action-destructive-text | #FFFFFF | #3B0E0A | ||
--action-accent-bg | #10B5AA | #5FD6C8 | Lightest teal that holds Deep Afara text at ≥4.5:1. Brand Teal itself reaches only 4.2:1. | |
--action-accent-bg-hover | #18BDB2 | #7FE0D4 | ||
--action-accent-bg-active | #0FAEA4 | #4BC2B4 | ||
--action-accent-text | #073B37 | #073B37 | ||
--action-disabled-bg | #EEF4F2 | #05302D | ||
--action-disabled-text | #7F9893 | #5FA9A0 | ||
--action-disabled-border | #DCE8E5 | #1B625B |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--status-success-bg | #EAF4EB | #0A3A2B | ||
--status-success-border | #B9D6BD | #2F7A58 | ||
--status-success-text | #2E5634 | #8FD39B | ||
--status-success-solid | #47704C | #8FD39B | ||
--status-warning-bg | #FEF4DC | #3D3010 | ||
--status-warning-border | #F2D08A | #8A6A1F | ||
--status-warning-text | #7A4100 | #F2C265 | ||
--status-warning-solid | #B45309 | #F2C265 | ||
--status-danger-bg | #FDECEA | #45201D | ||
--status-danger-border | #F2B8B2 | #9A4A42 | ||
--status-danger-text | #A3261B | #FFAFA6 | ||
--status-danger-solid | #C42B1E | #FFAFA6 | ||
--status-info-bg | #E8F0FA | #12334F | ||
--status-info-border | #B8CFEA | #3C6FA3 | ||
--status-info-text | #1B4F8F | #A3CBF7 | ||
--status-info-solid | #2563A8 | #A3CBF7 | ||
--status-neutral-bg | #EEF4F2 | #05302D | ||
--status-neutral-border | #BCCFCA | #2B7A72 | ||
--status-neutral-text | #3A524E | #B5D8D2 | ||
--status-neutral-solid | #688580 | #5FA9A0 |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--env-production-bg | #073B37 | #CDEBE7 | ||
--env-production-text | #FFFFFF | #073B37 | ||
--env-production-border | #073B37 | #CDEBE7 | ||
--env-staging-bg | #FEF4DC | #3D3010 | ||
--env-staging-text | #7A4100 | #F2C265 | ||
--env-staging-border | #B45309 | #F2C265 | ||
--env-development-bg | #FFFFFF | #0B4944 | ||
--env-development-text | #3A524E | #B5D8D2 | ||
--env-development-border | #688580 | #5FA9A0 |
| Token | Swatch | Light | Dark | Role |
|---|---|---|---|---|
--focus-ring-color | #087068 | #5FD6C8 | ≥4.5:1 on white, canvas and pale-mint selection; mint in dark. |
80 declared pairs, calculated from the resolved token values with the WCAG relative-luminance formula. Every pair meets its minimum in both themes.
| Foreground on background | Use | Min | Light | Dark |
|---|---|---|---|---|
text on canvas | Primary text on page | 4.5:1 | 11.83:1 ✓ | 11.72:1 ✓ |
text on surface | Primary text on cards | 4.5:1 | 12.43:1 ✓ | 9.66:1 ✓ |
text on surface-raised | Primary text in menus/popovers | 4.5:1 | 12.43:1 ✓ | 8.26:1 ✓ |
text on surface-inset | Primary text in wells | 4.5:1 | 11.16:1 ✓ | 13.50:1 ✓ |
text on surface-selected | Selected rows | 4.5:1 | 11.03:1 ✓ | 8.26:1 ✓ |
text on accent-bg-strong | Active navigation item | 4.5:1 | 9.84:1 ✓ | 6.73:1 ✓ |
muted-text on canvas | Secondary text on page | 4.5:1 | 5.47:1 ✓ | 8.12:1 ✓ |
muted-text on surface | Secondary text on cards | 4.5:1 | 5.74:1 ✓ | 6.69:1 ✓ |
muted-text on surface-raised | Secondary text in menus | 4.5:1 | 5.74:1 ✓ | 5.72:1 ✓ |
muted-text on surface-inset | Secondary text in wells | 4.5:1 | 5.16:1 ✓ | 9.35:1 ✓ |
muted-text on surface-selected | Secondary text in selected rows | 4.5:1 | 5.10:1 ✓ | 5.72:1 ✓ |
subtle-text on canvas | Tertiary text / placeholder on page | 4.5:1 | 4.86:1 ✓ | 6.60:1 ✓ |
subtle-text on surface | Tertiary text / placeholder on cards | 4.5:1 | 5.11:1 ✓ | 5.44:1 ✓ |
subtle-text on surface-raised | Tertiary text in menus | 4.5:1 | 5.11:1 ✓ | 4.65:1 ✓ |
link on canvas | Links on page | 4.5:1 | 5.67:1 ✓ | 7.07:1 ✓ |
link on surface | Links on cards | 4.5:1 | 5.95:1 ✓ | 5.82:1 ✓ |
link on surface-raised | Links in popovers | 4.5:1 | 5.95:1 ✓ | 4.98:1 ✓ |
link-hover on surface | Link hover | 4.5:1 | 9.06:1 ✓ | 8.11:1 ✓ |
accent-text on surface | Teal text on cards | 4.5:1 | 5.95:1 ✓ | 5.82:1 ✓ |
accent-text on accent-bg | Teal text on tint | 4.5:1 | 5.28:1 ✓ | 4.98:1 ✓ |
inverse-text on surface-inverse | Tooltip / toast text | 4.5:1 | 12.43:1 ✓ | 9.84:1 ✓ |
inverse-muted-text on surface-inverse | Secondary text in toast | 4.5:1 | 9.84:1 ✓ | 6.66:1 ✓ |
border-control on canvas | Input boundary on page | 3:1 | 3.81:1 ✓ | 4.54:1 ✓ |
border-control on surface | Input boundary on cards | 3:1 | 4.00:1 ✓ | 3.74:1 ✓ |
border-control on surface-inset | Control boundary in wells | 3:1 | 3.59:1 ✓ | 5.23:1 ✓ |
accent-solid on canvas | Progress fill / selected indicator on page | 3:1 | 3.78:1 ✓ | 7.07:1 ✓ |
accent-solid on surface | Progress fill on cards | 3:1 | 3.97:1 ✓ | 5.82:1 ✓ |
accent-solid on surface-inset | Progress fill on meter track | 3:1 | 3.57:1 ✓ | 8.14:1 ✓ |
focus.ring.color on canvas | Focus ring on page | 3:1 | 5.67:1 ✓ | 7.07:1 ✓ |
focus.ring.color on surface | Focus ring on cards | 3:1 | 5.95:1 ✓ | 5.82:1 ✓ |
focus.ring.color on surface-raised | Focus ring in popovers | 3:1 | 5.95:1 ✓ | 4.98:1 ✓ |
focus.ring.color on surface-selected | Focus ring on selected rows | 3:1 | 5.28:1 ✓ | 4.98:1 ✓ |
focus.ring.color on accent-bg-strong | Focus ring on active nav item | 3:1 | 4.71:1 ✓ | 4.05:1 ✓ |
chart-1 on surface | Chart series 1 (teal) on card | 3:1 | 3.97:1 ✓ | 3.49:1 ✓ |
chart-2 on surface | Chart series 2 (amber) on card | 3:1 | 5.02:1 ✓ | 3.19:1 ✓ |
chart-3 on surface | Chart series 3 (blue) on card | 3:1 | 6.12:1 ✓ | 3.29:1 ✓ |
action.primary.text on action.primary.bg | Primary button | 4.5:1 | 12.43:1 ✓ | 9.84:1 ✓ |
action.primary.text on action.primary.bg-hover | Primary button hover | 4.5:1 | 10.24:1 ✓ | 11.03:1 ✓ |
action.primary.text on action.primary.bg-active | Primary button active | 4.5:1 | 17.25:1 ✓ | 8.12:1 ✓ |
action.secondary.text on action.secondary.bg | Secondary button | 4.5:1 | 12.43:1 ✓ | 8.26:1 ✓ |
action.secondary.text on action.secondary.bg-hover | Secondary button hover | 4.5:1 | 11.83:1 ✓ | 6.73:1 ✓ |
action.secondary.text on action.secondary.bg-active | Secondary button active | 4.5:1 | 11.16:1 ✓ | 9.66:1 ✓ |
action.tertiary.text on action.tertiary.bg | Tertiary button | 4.5:1 | 11.03:1 ✓ | 5.82:1 ✓ |
action.tertiary.text on action.tertiary.bg-hover | Tertiary button hover | 4.5:1 | 9.84:1 ✓ | 4.98:1 ✓ |
action.tertiary.text on action.tertiary.bg-active | Tertiary button active | 4.5:1 | 8.78:1 ✓ | 8.14:1 ✓ |
action.ghost.text on action.ghost.bg-hover | Ghost button hover | 4.5:1 | 11.16:1 ✓ | 8.26:1 ✓ |
action.ghost.text on action.ghost.bg-active | Ghost button active | 4.5:1 | 10.49:1 ✓ | 6.73:1 ✓ |
action.ghost.text on canvas | Ghost button on page | 4.5:1 | 11.83:1 ✓ | 11.72:1 ✓ |
action.ghost.text on surface | Ghost button on cards | 4.5:1 | 12.43:1 ✓ | 9.66:1 ✓ |
action.destructive.text on action.destructive.bg | Destructive button | 4.5:1 | 6.57:1 ✓ | 9.52:1 ✓ |
action.destructive.text on action.destructive.bg-hover | Destructive button hover | 4.5:1 | 8.92:1 ✓ | 10.92:1 ✓ |
action.destructive.text on action.destructive.bg-active | Destructive button active | 4.5:1 | 10.68:1 ✓ | 9.79:1 ✓ |
action.accent.text on action.accent.bg | Accent button | 4.5:1 | 4.86:1 ✓ | 7.07:1 ✓ |
action.accent.text on action.accent.bg-hover | Accent button hover | 4.5:1 | 5.30:1 ✓ | 8.00:1 ✓ |
action.accent.text on action.accent.bg-active | Accent button active | 4.5:1 | 4.50:1 ✓ | 5.73:1 ✓ |
action.disabled.text on action.disabled.bg | Disabled button (WCAG-exempt; informative floor of 2:1) | 2:1 | 2.77:1 ✓ | 5.23:1 ✓ |
status.success.text on status.success.bg | Success label on tint | 4.5:1 | 7.46:1 ✓ | 7.23:1 ✓ |
status.success.text on surface | Success text on card | 4.5:1 | 8.41:1 ✓ | 5.83:1 ✓ |
status.success.solid on status.success.bg | Success icon on tint | 3:1 | 5.05:1 ✓ | 7.23:1 ✓ |
status.success.solid on surface | Success dot on card | 3:1 | 5.69:1 ✓ | 5.83:1 ✓ |
status.warning.text on status.warning.bg | Warning label on tint | 4.5:1 | 7.43:1 ✓ | 7.79:1 ✓ |
status.warning.text on surface | Warning text on card | 4.5:1 | 8.13:1 ✓ | 6.18:1 ✓ |
status.warning.solid on status.warning.bg | Warning icon on tint | 3:1 | 4.59:1 ✓ | 7.79:1 ✓ |
status.warning.solid on surface | Warning dot on card | 3:1 | 5.02:1 ✓ | 6.18:1 ✓ |
status.danger.text on status.danger.bg | Danger label on tint | 4.5:1 | 6.44:1 ✓ | 8.08:1 ✓ |
status.danger.text on surface | Danger text on card / error message | 4.5:1 | 7.37:1 ✓ | 5.82:1 ✓ |
status.danger.solid on status.danger.bg | Danger icon on tint | 3:1 | 4.95:1 ✓ | 8.08:1 ✓ |
status.danger.solid on surface | Invalid input border on card | 3:1 | 5.66:1 ✓ | 5.82:1 ✓ |
status.info.text on status.info.bg | Info label on tint | 4.5:1 | 7.13:1 ✓ | 7.72:1 ✓ |
status.info.text on surface | Info text on card | 4.5:1 | 8.20:1 ✓ | 6.07:1 ✓ |
status.info.solid on status.info.bg | Info icon on tint | 3:1 | 5.33:1 ✓ | 7.72:1 ✓ |
status.info.solid on surface | Info dot on card | 3:1 | 6.12:1 ✓ | 6.07:1 ✓ |
status.neutral.text on status.neutral.bg | Neutral label on tint | 4.5:1 | 7.55:1 ✓ | 9.35:1 ✓ |
status.neutral.solid on status.neutral.bg | Neutral icon on tint | 3:1 | 3.59:1 ✓ | 5.23:1 ✓ |
env.production.text on env.production.bg | Production badge | 4.5:1 | 12.43:1 ✓ | 9.84:1 ✓ |
env.staging.text on env.staging.bg | Staging badge | 4.5:1 | 7.43:1 ✓ | 7.79:1 ✓ |
env.development.text on env.development.bg | Development badge | 4.5:1 | 8.41:1 ✓ | 6.69:1 ✓ |
env.development.border on env.development.bg | Development badge dashed border | 3:1 | 4.00:1 ✓ | 3.74:1 ✓ |
env.staging.border on env.staging.bg | Staging badge border | 3:1 | 4.59:1 ✓ | 7.79:1 ✓ |
chart-other on surface | Chart 'Other' on card | 3:1 | 4.00:1 ✓ | 5.44:1 ✓ |
<span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Healthy</span>
<span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Degraded</span>
<span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>Failing</span>
<span class="af-badge af-badge--info"><span class="af-pip" aria-hidden="true"></span>Syncing</span>
<span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Paused</span>/* styles/components/data-display.css · badge */
/* Compact status or metadata. Pills are reserved for exactly this (brand.md). A badge ALWAYS has text; a status badge also
* has a pip or icon of a distinct shape. Tones: neutral · accent · success · warning · danger · info. */
.af-badge {
--_bg: var(--status-neutral-bg); --_text: var(--status-neutral-text); --_border: var(--status-neutral-border); --_pip: var(--status-neutral-solid);
display: inline-flex; align-items: center; gap: var(--badge-gap);
block-size: var(--badge-height); padding-inline: var(--badge-padding-x);
border: var(--border-width-hairline) solid var(--_border); border-radius: var(--badge-radius);
background: var(--_bg); color: var(--_text);
font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); white-space: nowrap; vertical-align: middle;
}
.af-badge > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }
.af-badge--success { --_bg: var(--status-success-bg); --_text: var(--status-success-text); --_border: var(--status-success-border); --_pip: var(--status-success-solid); }
.af-badge--warning { --_bg: var(--status-warning-bg); --_text: var(--status-warning-text); --_border: var(--status-warning-border); --_pip: var(--status-warning-solid); }
.af-badge--danger { --_bg: var(--status-danger-bg); --_text: var(--status-danger-text); --_border: var(--status-danger-border); --_pip: var(--status-danger-solid); }
.af-badge--info { --_bg: var(--status-info-bg); --_text: var(--status-info-text); --_border: var(--status-info-border); --_pip: var(--status-info-solid); }
.af-badge--accent { --_bg: var(--color-accent-bg); --_text: var(--color-accent-text); --_border: var(--color-accent-bg-strong); --_pip: var(--color-accent-solid); }
.af-badge--outline { --_bg: transparent; --_border: var(--color-border-strong); --_text: var(--color-muted-text); }
.af-badge--mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); }
/* styles/components/data-display.css · status-pip */
/* The status indicator. Each status has its own SHAPE so it is readable without colour:
* success ● filled circle warning ▲ triangle danger ◆ diamond info ○ ring neutral ▬ bar pending ◐ half circle
* Pair it with a visible label (or an aria-label if the label is truly absent). */
.af-pip { --_c: var(--status-neutral-solid); display: inline-block; flex: none; inline-size: var(--pip-size); block-size: var(--pip-size); background: var(--_c); border-radius: 50%; }
.af-badge .af-pip { --_c: var(--_pip); }
/* shape + colour per status; inside a badge the tone class selects the same shape automatically */
.af-pip[data-status="success"], .af-badge--success .af-pip { --_c: var(--status-success-solid); }
.af-pip[data-status="warning"], .af-badge--warning .af-pip { --_c: var(--status-warning-solid); border-radius: 0; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.af-pip[data-status="danger"], .af-badge--danger .af-pip { --_c: var(--status-danger-solid); border-radius: 0; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.af-pip[data-status="info"], .af-badge--info .af-pip { --_c: var(--status-info-solid); background: transparent; box-shadow: inset 0 0 0 2px var(--_c); }
.af-pip[data-status="neutral"], .af-badge:not([class*="--"]) .af-pip { --_c: var(--status-neutral-solid); border-radius: var(--radius-full); block-size: calc(var(--pip-size) / 3); }
.af-pip[data-status="pending"] { --_c: var(--status-info-solid); background: linear-gradient(90deg, var(--_c) 50%, transparent 50%); box-shadow: inset 0 0 0 1.5px var(--_c); }Design tokens this component reads. Override a token, not the component.
--badge-gap--badge-height--badge-padding-x--badge-radius--border-width-hairline--color-accent-bg--color-accent-bg-strong--color-accent-solid--color-accent-text--color-border-strong--color-muted-text--font-body--font-mono--font-weight-medium--icon-size-xs--status-danger-bg--status-danger-border--status-danger-solid--status-danger-text--status-info-bg--status-info-border--status-info-solid--status-info-text--status-neutral-bg--status-neutral-border--status-neutral-solid--status-neutral-text--status-success-bg--status-success-border--status-success-solid--status-success-text--status-warning-bg--status-warning-border--status-warning-solid--status-warning-text--type-caption-size--type-metadata-size--pip-size--radius-fullFoundations
Outfit sets headings and display text. Inter carries the interface. JetBrains Mono shows anything a developer might copy. All three are self-hosted variable fonts, so the system makes no third-party requests.
Outfit
Geometric, friendly, precise at large sizes. Weight 500–600.
Inter
Neutral and legible at small sizes, with tabular numerals.
JetBrains Mono
Names, IDs, keys, paths, code. Never for decoration.
Display and the larger headings are fluid between 360 px and 1280 px viewports, expressed in rem so users' text-size settings still apply.
| Style | Specimen | Family | Size | Weight | Line |
|---|---|---|---|---|---|
display | Object storage | display | 40–72px (fluid) | semibold | 1.02 |
h1 | Object storage | display | 34–56px (fluid) | semibold | 1.08 |
h2 | Object storage | display | 28–40px (fluid) | semibold | 1.15 |
h3 | Object storage | display | 22–28px (fluid) | semibold | 1.25 |
h4 | Object storage | display | 18–20px (fluid) | semibold | 1.35 |
component-title | Object storage | body | 15px | semibold | 1.4 |
body-lg | Object storage | body | 18–20px (fluid) | regular | 1.6 |
body | Object storage | body | 15–16px (fluid) | regular | 1.6 |
body-sm | Object storage | body | 14px | regular | 1.5 |
label | Object storage | body | 13px | medium | 1.4 |
caption | Object storage | body | 12px | regular | 1.4 |
eyebrow | Object storage | body | 12px | semibold | 1.4 |
metadata | Object storage | mono | 12px | regular | 1.5 |
code | Object storage | mono | 13–14px (fluid) | regular | 1.6 |
numeric | Object storage | body | 14px | medium | 1.4 |
metric | Object storage | body | 28–32px (fluid) | semibold | 1.1 |
| Bucket | Objects | Stored |
|---|---|---|
acme-assets-prod | 1,204,331 | 482.10 GB |
acme-logs-prod | 98,550 | 9.07 GB |
acme-backups | 412 | 1,310.00 GB |
<div class="af-table-wrap"><table class="af-table af-table--compact" style="--table-min: 0">
<caption class="af-visually-hidden">Sample usage by bucket</caption>
<thead><tr><th scope="col">Bucket</th><th scope="col" class="af-table__num">Objects</th><th scope="col" class="af-table__num">Stored</th></tr></thead>
<tbody>
<tr><th scope="row"><code>acme-assets-prod</code></th><td class="af-table__num">1,204,331</td><td class="af-table__num">482.10 GB</td></tr>
<tr><th scope="row"><code>acme-logs-prod</code></th><td class="af-table__num">98,550</td><td class="af-table__num">9.07 GB</td></tr>
<tr><th scope="row"><code>acme-backups</code></th><td class="af-table__num">412</td><td class="af-table__num">1,310.00 GB</td></tr>
</tbody></table></div>/* styles/components/data-display.css · key-value */
/* <dl> of terms and values. --inline puts the term beside the value (≥640px); default stacks them. Values that are
* identifiers use .af-kv__mono. A copy button may follow a value. */
.af-kv { display: grid; gap: var(--space-3) var(--space-6); margin: 0; grid-template-columns: minmax(0, 1fr); }
.af-kv__item { display: flex; flex-direction: column; gap: var(--space-0-5); min-inline-size: 0; }
.af-kv dt, .af-kv__term { font: var(--type-label-weight) var(--type-caption-size)/1.3 var(--type-label-family); color: var(--color-muted-text); }
.af-kv dd, .af-kv__value { margin: 0; font-size: var(--type-body-sm-size); line-height: 1.4; color: var(--color-text); overflow-wrap: anywhere; display: flex; align-items: center; gap: var(--space-2); }
.af-kv__mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); font-variant-ligatures: none; }
@media (min-width: 640px) {
.af-kv--inline { grid-template-columns: minmax(8rem, max-content) minmax(0, 1fr); gap: var(--space-2) var(--space-6); }
.af-kv--inline .af-kv__item { display: contents; }
.af-kv--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.af-kv--ruled .af-kv__item { padding-block: var(--space-2); border-block-end: 1px solid var(--color-border); }
.af-dl { margin: 0; }
.af-dl dt { font-weight: var(--font-weight-semibold); }
.af-dl dd { margin: 0 0 var(--space-3); color: var(--color-muted-text); }Design tokens this component reads. Override a token, not the component.
--color-border--color-muted-text--color-text--font-mono--font-weight-semibold--space-0-5--space-2--space-3--space-6--type-body-sm-size--type-caption-size--type-label-family--type-label-weight--type-metadata-size.af-table__num (tabular figures). State the unit and the byte convention (this system formats with decimal SI units: 1 MB = 1,000,000 B).l, 1, O and 0 are unambiguous.Foundations
A 4 px scale. Every gap, padding and size is one of these steps. If a design seems to need a value that is not here, the design is wrong or the scale needs a documented addition.
| Token | Value | Scale |
|---|---|---|
--space-px | 1px | |
--space-0-5 | 2px | |
--space-1 | 4px | |
--space-1-5 | 6px | |
--space-2 | 8px | |
--space-3 | 12px | |
--space-4 | 16px | |
--space-5 | 20px | |
--space-6 | 24px | |
--space-8 | 32px | |
--space-10 | 40px | |
--space-12 | 48px | |
--space-16 | 64px | |
--space-20 | 80px | |
--space-24 | 96px |
.af-stack, .af-cluster, .af-grid) and its --gap, not from margins on children.--card-padding).--space-0-5, --space-1-5) exist for optical alignment inside controls. Do not use them for layout.<div class="af-stack af-stack--2"><span class="af-badge">gap 8px</span><span class="af-badge">af-stack--2</span></div>
<div class="af-stack af-stack--6"><span class="af-badge">gap 24px</span><span class="af-badge">af-stack--6</span></div>Small radii for controls, medium for cards and panels. Pills (--radius-full) are for status badges and compact metadata only.
--radius-xs4px--radius-none0px--radius-sm6px--radius-md10px--radius-lg16px--radius-xl24px--radius-full9999pxFoundations
Composition uses a few primitives: container, stack, cluster, grid and split, plus the application shell. Prefer aligned columns and structured sections over scattered floating cards.
| Token | Min width | Meaning |
|---|---|---|
--breakpoint-sm | 640px | |
--breakpoint-md | 768px | |
--breakpoint-lg | 1024px | |
--breakpoint-xl | 1280px | |
--breakpoint-2xl | 1536px |
CSS custom properties cannot be used inside media queries. Write the literal pixel value in @media and keep it equal to this table — npm run validate checks that every literal in the CSS is one of these.
af-stack: vertical rhythm
af-cluster: wrapping row of related items
af-grid--auto: as many columns as fit (min 12rem)
<div class="af-stack af-stack--3">
<p class="t-label">af-stack: vertical rhythm</p>
<div class="af-card"><div class="af-card__body">One</div></div>
<div class="af-card"><div class="af-card__body">Two</div></div>
</div>
<div class="af-stack af-stack--2"><p class="t-label">af-cluster: wrapping row of related items</p>
<div class="af-cluster af-cluster--2"><span class="af-badge">Alpha</span><span class="af-badge">Beta</span><span class="af-badge">Gamma</span><span class="af-badge">Delta</span></div></div>
<div class="af-stack af-stack--2"><p class="t-label">af-grid--auto: as many columns as fit (min 12rem)</p>
<div class="af-grid af-grid--auto af-grid--gap-3" style="--min: 12rem">
<div class="af-card"><div class="af-card__body">Card</div></div><div class="af-card"><div class="af-card__body">Card</div></div><div class="af-card"><div class="af-card__body">Card</div></div>
</div></div>.af-container: marketing and documentation content, 1200 px with a fluid gutter..af-container--app: application content, wider with tighter gutters..af-container--narrow and .af-prose: reading copy, about 44rem.Sidebar (248 px, 64 px collapsed), a 56 px header, main content and an optional 360 px inspector. Below 1024 px the sidebar becomes an off-canvas drawer opened from the header. This documentation site is itself built on the shell: resize the window to see it.
Foundations
Depth explains what a surface is. A static card is flat and bordered. A control is raised. A field is recessed. Only things that float above the page (menus, drawers, dialogs, toasts) cast a shadow.
Flat: card
Recessed: field
Raised: control
Floating: popover
<div class="af-stack af-stack--2"><p class="t-label">Flat: card</p><div class="af-card"><div class="af-card__body">Border only</div></div></div>
<div class="af-stack af-stack--2"><p class="t-label">Recessed: field</p><input class="af-input" aria-label="Recessed field example" value="Inset shadow"></div>
<div class="af-stack af-stack--2"><p class="t-label">Raised: control</p><div><button class="af-button af-button--secondary af-button--md" type="button">Key</button></div></div>
<div class="af-stack af-stack--2"><p class="t-label">Floating: popover</p><div class="af-popover" style="position: static; display: block">Shadow plus border</div></div>--shadow-raised-controlSecondary and ghost-hover tactile edge: light top highlight + hard 1px bottom edge.--shadow-raised-primaryPrimary/accent/destructive solid buttons.--shadow-pressedActive/pressed state: the key is pushed in.--shadow-inset-controlText inputs, selects, search fields.--shadow-inset-wellMeter tracks, segmented-control tracks, code wells, inset panels.--shadow-card-hoverInteractive card hover only. Static cards have no shadow.--shadow-popoverPopovers, dropdown menus, tooltips, command-palette results.--shadow-drawerSide drawers, bottom sheets.--shadow-modalModal dialogs and command palette.--shadow-toast| Token | Value | Use |
|---|---|---|
--z-base | 0 | |
--z-sticky | 100 | |
--z-dropdown | 200 | |
--z-popover | 300 | |
--z-scrim | 400 | |
--z-modal | 500 | |
--z-toast | 600 | |
--z-tooltip | 700 |
Do
Draw a card's boundary with its 1px border and a tonal change.
Don't
Use a drop shadow in place of a visible border, or float a static card.
Foundations
Motion clarifies state and relationships. It is quick, small, and never ambient: opacity and short position changes for menus, tabs, drawers and status changes.
| Token | Value | Use |
|---|---|---|
--motion-instant | 80ms | Pressed feedback, tooltip fade. |
--motion-fast | 120ms | Hover, focus, small state changes. |
--motion-standard | 200ms | Menus, tabs, accordion, toast. |
--motion-emphasis | 280ms | Modals, drawers, sheets. |
--ease-standard | cubic-bezier(0.2, 0, 0, 1) | Default. State changes and movement within the page. |
--ease-enter | cubic-bezier(0.05, 0.7, 0.1, 1) | Elements entering: overlays, toasts. |
--ease-exit | cubic-bezier(0.3, 0, 0.8, 0.15) | Elements leaving. |
--motion-fast for hover, press and small state changes; --motion-standard for menus and tabs; --motion-emphasis for drawers and dialogs.--ease-enter, leave with --ease-exit. Entering surfaces fade and rise a few pixels; they do not bounce or scale dramatically.prefers-reduced-motion: reduce every transition and animation collapses to an instant change, and the system supplies a static alternative where motion carried meaning.Foundations
One outline family (Lucide, ISC licence), curated to a small functional set. Icons are an inline SVG sprite, so they render without JavaScript and inherit text colour. Select an icon to copy its markup.
<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-hard-drive"/></svg>
<button class="af-button af-button--secondary af-button--md" type="button"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload</button>
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" aria-label="Delete object"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-trash-2"/></svg></button>aria-hidden="true". An icon that is the only content of a control needs an aria-label on the control.--icon-size-xs/sm/md/lg (12, 16, 20, 24 px) with a consistent 1.75 px stroke.Components
Every component here is real: the markup, the CSS and the behaviour are the shipped implementation. Choose from this catalogue before creating anything new.
| You need to… | Use | Not |
|---|---|---|
| Trigger the main action of a view | Primary button (one per region) | A second primary, or a link styled as a button |
| Navigate to another page | Link, nav item, breadcrumb, tab-style <nav> | A button with an href-like handler |
| Show a short, optional action list | Menu (popover) | A modal |
| Collect one or two values, then continue | Modal | A drawer |
| Edit or inspect something while keeping context | Drawer or inspector | A full page |
| Confirm a destructive action | Alert dialog, with typed confirmation if irreversible | A toast with Undo for something that cannot be undone |
| Tell the user something happened | Toast (transient) or alert (persistent) | A modal |
| Show records to compare | Table | A grid of cards |
| Show one figure | Metric card | A chart |
| Show usage against a limit | Usage meter | A plain progress bar |
| Show progress of a known task | <progress> (.af-progress) | A spinner |
Buttons in 7 variants and 4 sizes, icon, toggle, loading, split, group, link.
Sidebar, top navigation, breadcrumbs, tabs, pagination, switchers, command palette.
Text, select, combobox, multi-select, date, file, dropzone, check, radio, switch, slider.
Tables, badges, meters, charts, code, JSON, timelines, cards and surfaces.
Modal, alert dialog, drawer, inspector, sheet, popover, menu, tooltip, toast.
Alerts, banners, validation, spinner, skeleton, empty, error and offline states.
20 storage-specific components and 7 patterns: buckets, objects, keys, uploads, usage.
Full API (classes, attributes, events, states and keyboard behaviour for each component) is in COMPONENT_API.md and components/*.md.
Components
A button is a physical key. Solid variants have a light top edge and a hard lower edge; pressing pushes the key in. Hierarchy comes from fill, border and weight, never from glow or movement.
Change the variant, size and state; the preview is the real stylesheet and the code below it is exactly what you would paste.
<button class="af-button af-button--primary af-button--md" type="button">Primary</button>
<button class="af-button af-button--secondary af-button--md" type="button">Secondary</button>
<button class="af-button af-button--tertiary af-button--md" type="button">Tertiary</button>
<button class="af-button af-button--ghost af-button--md" type="button">Ghost</button>
<button class="af-button af-button--destructive af-button--md" type="button">Destructive</button>
<button class="af-button af-button--accent af-button--md" type="button">Accent</button>
<button class="af-button af-button--link af-button--md" type="button">Link style</button>/* styles/components/actions.css · button */
.af-button {
/* Variant-controlled properties (set by the --variant classes below). Do not override outside this file. */
--_bg: var(--action-secondary-bg);
--_bg-hover: var(--action-secondary-bg-hover);
--_bg-active: var(--action-secondary-bg-active);
--_text: var(--action-secondary-text);
--_border: var(--action-secondary-border);
--_shadow: var(--shadow-raised-control);
/* Size-controlled properties (set by the --size classes below). */
--_h: var(--button-height-md);
--_px: var(--button-padding-x-md);
--_fs: var(--button-font-size-md);
--_icon: var(--icon-size-sm);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--_gap, var(--button-gap));
block-size: var(--_h);
min-inline-size: var(--_h);
padding-inline: calc(var(--_px) - var(--border-width-hairline));
border: var(--border-width-hairline) solid var(--_border);
border-radius: var(--button-radius);
background: var(--_bg);
color: var(--_text);
box-shadow: var(--_shadow);
font-family: var(--font-body);
font-size: var(--_fs);
font-weight: var(--button-font-weight);
line-height: 1;
text-decoration: none;
white-space: nowrap;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard),
color var(--motion-fast) var(--ease-standard);
}
.af-button > .af-icon { inline-size: var(--_icon); block-size: var(--_icon); flex: none; }
.af-button:active:not(:disabled):not([aria-disabled="true"]) { background: var(--_bg-active); box-shadow: var(--shadow-pressed); }
@media (hover: hover) {
.af-button:hover:not(:disabled):not([aria-disabled="true"]):not(:active) { background: var(--_bg-hover); }
}
/* Variants ──────────────────────────────────────────────────────────────── */
.af-button--primary {
--_bg: var(--action-primary-bg); --_bg-hover: var(--action-primary-bg-hover); --_bg-active: var(--action-primary-bg-active);
--_text: var(--action-primary-text); --_border: var(--action-primary-border); --_shadow: var(--shadow-raised-primary);
}
.af-button--secondary { /* the default; class is optional but allowed for explicitness */ }
.af-button--tertiary {
--_bg: var(--action-tertiary-bg); --_bg-hover: var(--action-tertiary-bg-hover); --_bg-active: var(--action-tertiary-bg-active);
--_text: var(--action-tertiary-text); --_border: transparent; --_shadow: none;
}
.af-button--ghost {
--_bg: transparent; --_bg-hover: var(--action-ghost-bg-hover); --_bg-active: var(--action-ghost-bg-active);
--_text: var(--action-ghost-text); --_border: transparent; --_shadow: none;
}
.af-button--destructive {
--_bg: var(--action-destructive-bg); --_bg-hover: var(--action-destructive-bg-hover); --_bg-active: var(--action-destructive-bg-active);
--_text: var(--action-destructive-text); --_border: var(--action-destructive-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--accent {
--_bg: var(--action-accent-bg); --_bg-hover: var(--action-accent-bg-hover); --_bg-active: var(--action-accent-bg-active);
--_text: var(--action-accent-text); --_border: var(--action-accent-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--link {
--_bg: transparent; --_bg-hover: transparent; --_bg-active: transparent;
--_text: var(--color-link); --_border: transparent; --_shadow: none;
block-size: auto; min-inline-size: 0; padding-inline: 0; border-radius: var(--radius-sm);
font-weight: var(--font-weight-medium);
text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em;
}
.af-button--link:active:not(:disabled):not([aria-disabled="true"]) { box-shadow: none; }
@media (hover: hover) {
.af-button--link:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--color-link-hover); text-decoration-thickness: 2px; }
}
/* Sizes ─────────────────────────────────────────────────────────────────── */
.af-button--xs { --_h: var(--button-height-xs); --_px: var(--button-padding-x-xs); --_fs: var(--button-font-size-xs); --_icon: var(--icon-size-xs); --_gap: var(--space-1-5); }
.af-button--sm { --_h: var(--button-height-sm); --_px: var(--button-padding-x-sm); --_fs: var(--button-font-size-sm); --_icon: var(--icon-size-sm); }
.af-button--md { /* default */ }
.af-button--lg { --_h: var(--button-height-lg); --_px: var(--button-padding-x-lg); --_fs: var(--button-font-size-lg); --_icon: var(--icon-size-md); }
@media (pointer: coarse) {
.af-button--xs { --_h: var(--button-height-touch-xs); }
.af-button--sm { --_h: var(--button-height-touch-sm); }
}
.af-button--block { display: flex; inline-size: 100%; }
/* Icon button: square, no label. Always give it an accessible name (aria-label) and, where the meaning is not obvious,
* a tooltip. */
.af-button--icon { padding-inline: 0; inline-size: var(--_h); }
/* States ────────────────────────────────────────────────────────────────── */
/* Toggle button: aria-pressed on a <button>. Pressed = the key stays pushed in, with an accent edge. */
.af-button[aria-pressed="true"] {
--_bg: var(--color-accent-bg-strong); --_bg-hover: var(--color-accent-bg-strong); --_bg-active: var(--color-accent-bg-strong);
--_text: var(--color-text); --_border: var(--color-accent-solid); --_shadow: var(--shadow-pressed);
}
.af-button--ghost[aria-pressed="true"], .af-button--tertiary[aria-pressed="true"] { --_border: var(--color-accent-solid); }
/* Loading: keep the label and the button's size; show a spinner; block activation (JS also ignores clicks). The button
* stays focusable. Announce progress in a nearby live region, not on the button. */
.af-button[aria-busy="true"] { cursor: progress; }
.af-button[aria-busy="true"]:active { box-shadow: var(--_shadow); background: var(--_bg); }
/* Disabled: native :disabled, or aria-disabled="true" when the control must stay focusable (e.g. to expose a tooltip that
* explains why). */
.af-button:disabled, .af-button[aria-disabled="true"] {
background: var(--action-disabled-bg);
border-color: var(--action-disabled-border);
color: var(--action-disabled-text);
box-shadow: none;
}
.af-button--link:disabled, .af-button--link[aria-disabled="true"],
.af-button--ghost:disabled, .af-button--ghost[aria-disabled="true"] { background: transparent; border-color: transparent; }Design tokens this component reads. Override a token, not the component.
--action-accent-bg--action-accent-bg-active--action-accent-bg-hover--action-accent-text--action-destructive-bg--action-destructive-bg-active--action-destructive-bg-hover--action-destructive-text--action-disabled-bg--action-disabled-border--action-disabled-text--action-ghost-bg-active--action-ghost-bg-hover--action-ghost-text--action-primary-bg--action-primary-bg-active--action-primary-bg-hover--action-primary-border--action-primary-text--action-secondary-bg--action-secondary-bg-active--action-secondary-bg-hover--action-secondary-border--action-secondary-text--action-tertiary-bg--action-tertiary-bg-active--action-tertiary-bg-hover--action-tertiary-text--border-width-hairline--button-font-size-lg--button-font-size-md--button-font-size-sm--button-font-size-xs--button-font-weight--button-gap--button-height-lg--button-height-md--button-height-sm--button-height-touch-sm--button-height-touch-xs--button-height-xs--button-padding-x-lg--button-padding-x-md--button-padding-x-sm--button-padding-x-xs--button-radius--color-accent-bg-strong--color-accent-solid--color-link--color-link-hover--color-text--ease-standard--font-body--font-weight-medium--icon-size-md--icon-size-sm--icon-size-xs--motion-fast--radius-sm--shadow-pressed--shadow-raised-control--shadow-raised-primary--space-1-5| Variant | Use for | Per view |
|---|---|---|
| Primary | The single main action: Create bucket, Save changes, Upload | One per region |
| Secondary | Alternative or supporting actions | Several |
| Tertiary | Low-emphasis tinted actions inside dense surfaces | Several |
| Ghost | Toolbar and row actions, icon buttons, dismiss | Many |
| Destructive | Delete, revoke, remove. Always confirmed in an alert dialog | Rare |
| Accent | Marketing and documentation calls to action only | One |
| Link style | An action that should read as text inside prose | Occasional |
<button class="af-button af-button--primary af-button--xs" type="button">Extra small</button>
<button class="af-button af-button--primary af-button--sm" type="button">Small</button>
<button class="af-button af-button--primary af-button--md" type="button">Medium</button>
<button class="af-button af-button--primary af-button--lg" type="button">Large</button>/* styles/components/actions.css · button */
.af-button {
/* Variant-controlled properties (set by the --variant classes below). Do not override outside this file. */
--_bg: var(--action-secondary-bg);
--_bg-hover: var(--action-secondary-bg-hover);
--_bg-active: var(--action-secondary-bg-active);
--_text: var(--action-secondary-text);
--_border: var(--action-secondary-border);
--_shadow: var(--shadow-raised-control);
/* Size-controlled properties (set by the --size classes below). */
--_h: var(--button-height-md);
--_px: var(--button-padding-x-md);
--_fs: var(--button-font-size-md);
--_icon: var(--icon-size-sm);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--_gap, var(--button-gap));
block-size: var(--_h);
min-inline-size: var(--_h);
padding-inline: calc(var(--_px) - var(--border-width-hairline));
border: var(--border-width-hairline) solid var(--_border);
border-radius: var(--button-radius);
background: var(--_bg);
color: var(--_text);
box-shadow: var(--_shadow);
font-family: var(--font-body);
font-size: var(--_fs);
font-weight: var(--button-font-weight);
line-height: 1;
text-decoration: none;
white-space: nowrap;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard),
color var(--motion-fast) var(--ease-standard);
}
.af-button > .af-icon { inline-size: var(--_icon); block-size: var(--_icon); flex: none; }
.af-button:active:not(:disabled):not([aria-disabled="true"]) { background: var(--_bg-active); box-shadow: var(--shadow-pressed); }
@media (hover: hover) {
.af-button:hover:not(:disabled):not([aria-disabled="true"]):not(:active) { background: var(--_bg-hover); }
}
/* Variants ──────────────────────────────────────────────────────────────── */
.af-button--primary {
--_bg: var(--action-primary-bg); --_bg-hover: var(--action-primary-bg-hover); --_bg-active: var(--action-primary-bg-active);
--_text: var(--action-primary-text); --_border: var(--action-primary-border); --_shadow: var(--shadow-raised-primary);
}
.af-button--secondary { /* the default; class is optional but allowed for explicitness */ }
.af-button--tertiary {
--_bg: var(--action-tertiary-bg); --_bg-hover: var(--action-tertiary-bg-hover); --_bg-active: var(--action-tertiary-bg-active);
--_text: var(--action-tertiary-text); --_border: transparent; --_shadow: none;
}
.af-button--ghost {
--_bg: transparent; --_bg-hover: var(--action-ghost-bg-hover); --_bg-active: var(--action-ghost-bg-active);
--_text: var(--action-ghost-text); --_border: transparent; --_shadow: none;
}
.af-button--destructive {
--_bg: var(--action-destructive-bg); --_bg-hover: var(--action-destructive-bg-hover); --_bg-active: var(--action-destructive-bg-active);
--_text: var(--action-destructive-text); --_border: var(--action-destructive-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--accent {
--_bg: var(--action-accent-bg); --_bg-hover: var(--action-accent-bg-hover); --_bg-active: var(--action-accent-bg-active);
--_text: var(--action-accent-text); --_border: var(--action-accent-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--link {
--_bg: transparent; --_bg-hover: transparent; --_bg-active: transparent;
--_text: var(--color-link); --_border: transparent; --_shadow: none;
block-size: auto; min-inline-size: 0; padding-inline: 0; border-radius: var(--radius-sm);
font-weight: var(--font-weight-medium);
text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em;
}
.af-button--link:active:not(:disabled):not([aria-disabled="true"]) { box-shadow: none; }
@media (hover: hover) {
.af-button--link:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--color-link-hover); text-decoration-thickness: 2px; }
}
/* Sizes ─────────────────────────────────────────────────────────────────── */
.af-button--xs { --_h: var(--button-height-xs); --_px: var(--button-padding-x-xs); --_fs: var(--button-font-size-xs); --_icon: var(--icon-size-xs); --_gap: var(--space-1-5); }
.af-button--sm { --_h: var(--button-height-sm); --_px: var(--button-padding-x-sm); --_fs: var(--button-font-size-sm); --_icon: var(--icon-size-sm); }
.af-button--md { /* default */ }
.af-button--lg { --_h: var(--button-height-lg); --_px: var(--button-padding-x-lg); --_fs: var(--button-font-size-lg); --_icon: var(--icon-size-md); }
@media (pointer: coarse) {
.af-button--xs { --_h: var(--button-height-touch-xs); }
.af-button--sm { --_h: var(--button-height-touch-sm); }
}
.af-button--block { display: flex; inline-size: 100%; }
/* Icon button: square, no label. Always give it an accessible name (aria-label) and, where the meaning is not obvious,
* a tooltip. */
.af-button--icon { padding-inline: 0; inline-size: var(--_h); }
/* States ────────────────────────────────────────────────────────────────── */
/* Toggle button: aria-pressed on a <button>. Pressed = the key stays pushed in, with an accent edge. */
.af-button[aria-pressed="true"] {
--_bg: var(--color-accent-bg-strong); --_bg-hover: var(--color-accent-bg-strong); --_bg-active: var(--color-accent-bg-strong);
--_text: var(--color-text); --_border: var(--color-accent-solid); --_shadow: var(--shadow-pressed);
}
.af-button--ghost[aria-pressed="true"], .af-button--tertiary[aria-pressed="true"] { --_border: var(--color-accent-solid); }
/* Loading: keep the label and the button's size; show a spinner; block activation (JS also ignores clicks). The button
* stays focusable. Announce progress in a nearby live region, not on the button. */
.af-button[aria-busy="true"] { cursor: progress; }
.af-button[aria-busy="true"]:active { box-shadow: var(--_shadow); background: var(--_bg); }
/* Disabled: native :disabled, or aria-disabled="true" when the control must stay focusable (e.g. to expose a tooltip that
* explains why). */
.af-button:disabled, .af-button[aria-disabled="true"] {
background: var(--action-disabled-bg);
border-color: var(--action-disabled-border);
color: var(--action-disabled-text);
box-shadow: none;
}
.af-button--link:disabled, .af-button--link[aria-disabled="true"],
.af-button--ghost:disabled, .af-button--ghost[aria-disabled="true"] { background: transparent; border-color: transparent; }Design tokens this component reads. Override a token, not the component.
--action-accent-bg--action-accent-bg-active--action-accent-bg-hover--action-accent-text--action-destructive-bg--action-destructive-bg-active--action-destructive-bg-hover--action-destructive-text--action-disabled-bg--action-disabled-border--action-disabled-text--action-ghost-bg-active--action-ghost-bg-hover--action-ghost-text--action-primary-bg--action-primary-bg-active--action-primary-bg-hover--action-primary-border--action-primary-text--action-secondary-bg--action-secondary-bg-active--action-secondary-bg-hover--action-secondary-border--action-secondary-text--action-tertiary-bg--action-tertiary-bg-active--action-tertiary-bg-hover--action-tertiary-text--border-width-hairline--button-font-size-lg--button-font-size-md--button-font-size-sm--button-font-size-xs--button-font-weight--button-gap--button-height-lg--button-height-md--button-height-sm--button-height-touch-sm--button-height-touch-xs--button-height-xs--button-padding-x-lg--button-padding-x-md--button-padding-x-sm--button-padding-x-xs--button-radius--color-accent-bg-strong--color-accent-solid--color-link--color-link-hover--color-text--ease-standard--font-body--font-weight-medium--icon-size-md--icon-size-sm--icon-size-xs--motion-fast--radius-sm--shadow-pressed--shadow-raised-control--shadow-raised-primary--space-1-5Medium is the default. Use xs and sm in dense tables and toolbars, lg for marketing and empty-state calls to action. Touch targets stay at least 24 × 24 px (WCAG 2.5.8); on touch, prefer md and above.
Hover, active and focus are shown here with the documentation-only data-force attribute, generated from the real :hover, :active and :focus-visible rules. Press Tab and click the live buttons on the right of each row to see the true states.
<button class="af-button af-button--ghost af-button--icon af-button--md" type="button" aria-label="Copy endpoint" data-tooltip="Copy endpoint"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button>
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" aria-label="Refresh" data-tooltip="Refresh"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-refresh-cw"/></svg></button>
<button class="af-button af-button--primary af-button--md" type="button"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload</button>
<button class="af-button af-button--secondary af-button--md" type="button">Next<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-arrow-right"/></svg></button>/* styles/components/actions.css · button */
.af-button {
/* Variant-controlled properties (set by the --variant classes below). Do not override outside this file. */
--_bg: var(--action-secondary-bg);
--_bg-hover: var(--action-secondary-bg-hover);
--_bg-active: var(--action-secondary-bg-active);
--_text: var(--action-secondary-text);
--_border: var(--action-secondary-border);
--_shadow: var(--shadow-raised-control);
/* Size-controlled properties (set by the --size classes below). */
--_h: var(--button-height-md);
--_px: var(--button-padding-x-md);
--_fs: var(--button-font-size-md);
--_icon: var(--icon-size-sm);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--_gap, var(--button-gap));
block-size: var(--_h);
min-inline-size: var(--_h);
padding-inline: calc(var(--_px) - var(--border-width-hairline));
border: var(--border-width-hairline) solid var(--_border);
border-radius: var(--button-radius);
background: var(--_bg);
color: var(--_text);
box-shadow: var(--_shadow);
font-family: var(--font-body);
font-size: var(--_fs);
font-weight: var(--button-font-weight);
line-height: 1;
text-decoration: none;
white-space: nowrap;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard),
color var(--motion-fast) var(--ease-standard);
}
.af-button > .af-icon { inline-size: var(--_icon); block-size: var(--_icon); flex: none; }
.af-button:active:not(:disabled):not([aria-disabled="true"]) { background: var(--_bg-active); box-shadow: var(--shadow-pressed); }
@media (hover: hover) {
.af-button:hover:not(:disabled):not([aria-disabled="true"]):not(:active) { background: var(--_bg-hover); }
}
/* Variants ──────────────────────────────────────────────────────────────── */
.af-button--primary {
--_bg: var(--action-primary-bg); --_bg-hover: var(--action-primary-bg-hover); --_bg-active: var(--action-primary-bg-active);
--_text: var(--action-primary-text); --_border: var(--action-primary-border); --_shadow: var(--shadow-raised-primary);
}
.af-button--secondary { /* the default; class is optional but allowed for explicitness */ }
.af-button--tertiary {
--_bg: var(--action-tertiary-bg); --_bg-hover: var(--action-tertiary-bg-hover); --_bg-active: var(--action-tertiary-bg-active);
--_text: var(--action-tertiary-text); --_border: transparent; --_shadow: none;
}
.af-button--ghost {
--_bg: transparent; --_bg-hover: var(--action-ghost-bg-hover); --_bg-active: var(--action-ghost-bg-active);
--_text: var(--action-ghost-text); --_border: transparent; --_shadow: none;
}
.af-button--destructive {
--_bg: var(--action-destructive-bg); --_bg-hover: var(--action-destructive-bg-hover); --_bg-active: var(--action-destructive-bg-active);
--_text: var(--action-destructive-text); --_border: var(--action-destructive-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--accent {
--_bg: var(--action-accent-bg); --_bg-hover: var(--action-accent-bg-hover); --_bg-active: var(--action-accent-bg-active);
--_text: var(--action-accent-text); --_border: var(--action-accent-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--link {
--_bg: transparent; --_bg-hover: transparent; --_bg-active: transparent;
--_text: var(--color-link); --_border: transparent; --_shadow: none;
block-size: auto; min-inline-size: 0; padding-inline: 0; border-radius: var(--radius-sm);
font-weight: var(--font-weight-medium);
text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em;
}
.af-button--link:active:not(:disabled):not([aria-disabled="true"]) { box-shadow: none; }
@media (hover: hover) {
.af-button--link:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--color-link-hover); text-decoration-thickness: 2px; }
}
/* Sizes ─────────────────────────────────────────────────────────────────── */
.af-button--xs { --_h: var(--button-height-xs); --_px: var(--button-padding-x-xs); --_fs: var(--button-font-size-xs); --_icon: var(--icon-size-xs); --_gap: var(--space-1-5); }
.af-button--sm { --_h: var(--button-height-sm); --_px: var(--button-padding-x-sm); --_fs: var(--button-font-size-sm); --_icon: var(--icon-size-sm); }
.af-button--md { /* default */ }
.af-button--lg { --_h: var(--button-height-lg); --_px: var(--button-padding-x-lg); --_fs: var(--button-font-size-lg); --_icon: var(--icon-size-md); }
@media (pointer: coarse) {
.af-button--xs { --_h: var(--button-height-touch-xs); }
.af-button--sm { --_h: var(--button-height-touch-sm); }
}
.af-button--block { display: flex; inline-size: 100%; }
/* Icon button: square, no label. Always give it an accessible name (aria-label) and, where the meaning is not obvious,
* a tooltip. */
.af-button--icon { padding-inline: 0; inline-size: var(--_h); }
/* States ────────────────────────────────────────────────────────────────── */
/* Toggle button: aria-pressed on a <button>. Pressed = the key stays pushed in, with an accent edge. */
.af-button[aria-pressed="true"] {
--_bg: var(--color-accent-bg-strong); --_bg-hover: var(--color-accent-bg-strong); --_bg-active: var(--color-accent-bg-strong);
--_text: var(--color-text); --_border: var(--color-accent-solid); --_shadow: var(--shadow-pressed);
}
.af-button--ghost[aria-pressed="true"], .af-button--tertiary[aria-pressed="true"] { --_border: var(--color-accent-solid); }
/* Loading: keep the label and the button's size; show a spinner; block activation (JS also ignores clicks). The button
* stays focusable. Announce progress in a nearby live region, not on the button. */
.af-button[aria-busy="true"] { cursor: progress; }
.af-button[aria-busy="true"]:active { box-shadow: var(--_shadow); background: var(--_bg); }
/* Disabled: native :disabled, or aria-disabled="true" when the control must stay focusable (e.g. to expose a tooltip that
* explains why). */
.af-button:disabled, .af-button[aria-disabled="true"] {
background: var(--action-disabled-bg);
border-color: var(--action-disabled-border);
color: var(--action-disabled-text);
box-shadow: none;
}
.af-button--link:disabled, .af-button--link[aria-disabled="true"],
.af-button--ghost:disabled, .af-button--ghost[aria-disabled="true"] { background: transparent; border-color: transparent; }Design tokens this component reads. Override a token, not the component.
--action-accent-bg--action-accent-bg-active--action-accent-bg-hover--action-accent-text--action-destructive-bg--action-destructive-bg-active--action-destructive-bg-hover--action-destructive-text--action-disabled-bg--action-disabled-border--action-disabled-text--action-ghost-bg-active--action-ghost-bg-hover--action-ghost-text--action-primary-bg--action-primary-bg-active--action-primary-bg-hover--action-primary-border--action-primary-text--action-secondary-bg--action-secondary-bg-active--action-secondary-bg-hover--action-secondary-border--action-secondary-text--action-tertiary-bg--action-tertiary-bg-active--action-tertiary-bg-hover--action-tertiary-text--border-width-hairline--button-font-size-lg--button-font-size-md--button-font-size-sm--button-font-size-xs--button-font-weight--button-gap--button-height-lg--button-height-md--button-height-sm--button-height-touch-sm--button-height-touch-xs--button-height-xs--button-padding-x-lg--button-padding-x-md--button-padding-x-sm--button-padding-x-xs--button-radius--color-accent-bg-strong--color-accent-solid--color-link--color-link-hover--color-text--ease-standard--font-body--font-weight-medium--icon-size-md--icon-size-sm--icon-size-xs--motion-fast--radius-sm--shadow-pressed--shadow-raised-control--shadow-raised-primary--space-1-5<button class="af-button af-button--secondary af-button--md" type="button" data-toggle aria-pressed="false"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-pin"/></svg>Pin bucket</button>/* styles/components/actions.css · button */
.af-button {
/* Variant-controlled properties (set by the --variant classes below). Do not override outside this file. */
--_bg: var(--action-secondary-bg);
--_bg-hover: var(--action-secondary-bg-hover);
--_bg-active: var(--action-secondary-bg-active);
--_text: var(--action-secondary-text);
--_border: var(--action-secondary-border);
--_shadow: var(--shadow-raised-control);
/* Size-controlled properties (set by the --size classes below). */
--_h: var(--button-height-md);
--_px: var(--button-padding-x-md);
--_fs: var(--button-font-size-md);
--_icon: var(--icon-size-sm);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--_gap, var(--button-gap));
block-size: var(--_h);
min-inline-size: var(--_h);
padding-inline: calc(var(--_px) - var(--border-width-hairline));
border: var(--border-width-hairline) solid var(--_border);
border-radius: var(--button-radius);
background: var(--_bg);
color: var(--_text);
box-shadow: var(--_shadow);
font-family: var(--font-body);
font-size: var(--_fs);
font-weight: var(--button-font-weight);
line-height: 1;
text-decoration: none;
white-space: nowrap;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard),
color var(--motion-fast) var(--ease-standard);
}
.af-button > .af-icon { inline-size: var(--_icon); block-size: var(--_icon); flex: none; }
.af-button:active:not(:disabled):not([aria-disabled="true"]) { background: var(--_bg-active); box-shadow: var(--shadow-pressed); }
@media (hover: hover) {
.af-button:hover:not(:disabled):not([aria-disabled="true"]):not(:active) { background: var(--_bg-hover); }
}
/* Variants ──────────────────────────────────────────────────────────────── */
.af-button--primary {
--_bg: var(--action-primary-bg); --_bg-hover: var(--action-primary-bg-hover); --_bg-active: var(--action-primary-bg-active);
--_text: var(--action-primary-text); --_border: var(--action-primary-border); --_shadow: var(--shadow-raised-primary);
}
.af-button--secondary { /* the default; class is optional but allowed for explicitness */ }
.af-button--tertiary {
--_bg: var(--action-tertiary-bg); --_bg-hover: var(--action-tertiary-bg-hover); --_bg-active: var(--action-tertiary-bg-active);
--_text: var(--action-tertiary-text); --_border: transparent; --_shadow: none;
}
.af-button--ghost {
--_bg: transparent; --_bg-hover: var(--action-ghost-bg-hover); --_bg-active: var(--action-ghost-bg-active);
--_text: var(--action-ghost-text); --_border: transparent; --_shadow: none;
}
.af-button--destructive {
--_bg: var(--action-destructive-bg); --_bg-hover: var(--action-destructive-bg-hover); --_bg-active: var(--action-destructive-bg-active);
--_text: var(--action-destructive-text); --_border: var(--action-destructive-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--accent {
--_bg: var(--action-accent-bg); --_bg-hover: var(--action-accent-bg-hover); --_bg-active: var(--action-accent-bg-active);
--_text: var(--action-accent-text); --_border: var(--action-accent-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--link {
--_bg: transparent; --_bg-hover: transparent; --_bg-active: transparent;
--_text: var(--color-link); --_border: transparent; --_shadow: none;
block-size: auto; min-inline-size: 0; padding-inline: 0; border-radius: var(--radius-sm);
font-weight: var(--font-weight-medium);
text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em;
}
.af-button--link:active:not(:disabled):not([aria-disabled="true"]) { box-shadow: none; }
@media (hover: hover) {
.af-button--link:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--color-link-hover); text-decoration-thickness: 2px; }
}
/* Sizes ─────────────────────────────────────────────────────────────────── */
.af-button--xs { --_h: var(--button-height-xs); --_px: var(--button-padding-x-xs); --_fs: var(--button-font-size-xs); --_icon: var(--icon-size-xs); --_gap: var(--space-1-5); }
.af-button--sm { --_h: var(--button-height-sm); --_px: var(--button-padding-x-sm); --_fs: var(--button-font-size-sm); --_icon: var(--icon-size-sm); }
.af-button--md { /* default */ }
.af-button--lg { --_h: var(--button-height-lg); --_px: var(--button-padding-x-lg); --_fs: var(--button-font-size-lg); --_icon: var(--icon-size-md); }
@media (pointer: coarse) {
.af-button--xs { --_h: var(--button-height-touch-xs); }
.af-button--sm { --_h: var(--button-height-touch-sm); }
}
.af-button--block { display: flex; inline-size: 100%; }
/* Icon button: square, no label. Always give it an accessible name (aria-label) and, where the meaning is not obvious,
* a tooltip. */
.af-button--icon { padding-inline: 0; inline-size: var(--_h); }
/* States ────────────────────────────────────────────────────────────────── */
/* Toggle button: aria-pressed on a <button>. Pressed = the key stays pushed in, with an accent edge. */
.af-button[aria-pressed="true"] {
--_bg: var(--color-accent-bg-strong); --_bg-hover: var(--color-accent-bg-strong); --_bg-active: var(--color-accent-bg-strong);
--_text: var(--color-text); --_border: var(--color-accent-solid); --_shadow: var(--shadow-pressed);
}
.af-button--ghost[aria-pressed="true"], .af-button--tertiary[aria-pressed="true"] { --_border: var(--color-accent-solid); }
/* Loading: keep the label and the button's size; show a spinner; block activation (JS also ignores clicks). The button
* stays focusable. Announce progress in a nearby live region, not on the button. */
.af-button[aria-busy="true"] { cursor: progress; }
.af-button[aria-busy="true"]:active { box-shadow: var(--_shadow); background: var(--_bg); }
/* Disabled: native :disabled, or aria-disabled="true" when the control must stay focusable (e.g. to expose a tooltip that
* explains why). */
.af-button:disabled, .af-button[aria-disabled="true"] {
background: var(--action-disabled-bg);
border-color: var(--action-disabled-border);
color: var(--action-disabled-text);
box-shadow: none;
}
.af-button--link:disabled, .af-button--link[aria-disabled="true"],
.af-button--ghost:disabled, .af-button--ghost[aria-disabled="true"] { background: transparent; border-color: transparent; }Design tokens this component reads. Override a token, not the component.
--action-accent-bg--action-accent-bg-active--action-accent-bg-hover--action-accent-text--action-destructive-bg--action-destructive-bg-active--action-destructive-bg-hover--action-destructive-text--action-disabled-bg--action-disabled-border--action-disabled-text--action-ghost-bg-active--action-ghost-bg-hover--action-ghost-text--action-primary-bg--action-primary-bg-active--action-primary-bg-hover--action-primary-border--action-primary-text--action-secondary-bg--action-secondary-bg-active--action-secondary-bg-hover--action-secondary-border--action-secondary-text--action-tertiary-bg--action-tertiary-bg-active--action-tertiary-bg-hover--action-tertiary-text--border-width-hairline--button-font-size-lg--button-font-size-md--button-font-size-sm--button-font-size-xs--button-font-weight--button-gap--button-height-lg--button-height-md--button-height-sm--button-height-touch-sm--button-height-touch-xs--button-height-xs--button-padding-x-lg--button-padding-x-md--button-padding-x-sm--button-padding-x-xs--button-radius--color-accent-bg-strong--color-accent-solid--color-link--color-link-hover--color-text--ease-standard--font-body--font-weight-medium--icon-size-md--icon-size-sm--icon-size-xs--motion-fast--radius-sm--shadow-pressed--shadow-raised-control--shadow-raised-primary--space-1-5<button class="af-button af-button--primary af-button--md" type="button" data-demo-busy="Saving"><span data-label>Save changes</span></button>/* styles/components/actions.css · button */
.af-button {
/* Variant-controlled properties (set by the --variant classes below). Do not override outside this file. */
--_bg: var(--action-secondary-bg);
--_bg-hover: var(--action-secondary-bg-hover);
--_bg-active: var(--action-secondary-bg-active);
--_text: var(--action-secondary-text);
--_border: var(--action-secondary-border);
--_shadow: var(--shadow-raised-control);
/* Size-controlled properties (set by the --size classes below). */
--_h: var(--button-height-md);
--_px: var(--button-padding-x-md);
--_fs: var(--button-font-size-md);
--_icon: var(--icon-size-sm);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--_gap, var(--button-gap));
block-size: var(--_h);
min-inline-size: var(--_h);
padding-inline: calc(var(--_px) - var(--border-width-hairline));
border: var(--border-width-hairline) solid var(--_border);
border-radius: var(--button-radius);
background: var(--_bg);
color: var(--_text);
box-shadow: var(--_shadow);
font-family: var(--font-body);
font-size: var(--_fs);
font-weight: var(--button-font-weight);
line-height: 1;
text-decoration: none;
white-space: nowrap;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard),
color var(--motion-fast) var(--ease-standard);
}
.af-button > .af-icon { inline-size: var(--_icon); block-size: var(--_icon); flex: none; }
.af-button:active:not(:disabled):not([aria-disabled="true"]) { background: var(--_bg-active); box-shadow: var(--shadow-pressed); }
@media (hover: hover) {
.af-button:hover:not(:disabled):not([aria-disabled="true"]):not(:active) { background: var(--_bg-hover); }
}
/* Variants ──────────────────────────────────────────────────────────────── */
.af-button--primary {
--_bg: var(--action-primary-bg); --_bg-hover: var(--action-primary-bg-hover); --_bg-active: var(--action-primary-bg-active);
--_text: var(--action-primary-text); --_border: var(--action-primary-border); --_shadow: var(--shadow-raised-primary);
}
.af-button--secondary { /* the default; class is optional but allowed for explicitness */ }
.af-button--tertiary {
--_bg: var(--action-tertiary-bg); --_bg-hover: var(--action-tertiary-bg-hover); --_bg-active: var(--action-tertiary-bg-active);
--_text: var(--action-tertiary-text); --_border: transparent; --_shadow: none;
}
.af-button--ghost {
--_bg: transparent; --_bg-hover: var(--action-ghost-bg-hover); --_bg-active: var(--action-ghost-bg-active);
--_text: var(--action-ghost-text); --_border: transparent; --_shadow: none;
}
.af-button--destructive {
--_bg: var(--action-destructive-bg); --_bg-hover: var(--action-destructive-bg-hover); --_bg-active: var(--action-destructive-bg-active);
--_text: var(--action-destructive-text); --_border: var(--action-destructive-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--accent {
--_bg: var(--action-accent-bg); --_bg-hover: var(--action-accent-bg-hover); --_bg-active: var(--action-accent-bg-active);
--_text: var(--action-accent-text); --_border: var(--action-accent-bg-active); --_shadow: var(--shadow-raised-primary);
}
.af-button--link {
--_bg: transparent; --_bg-hover: transparent; --_bg-active: transparent;
--_text: var(--color-link); --_border: transparent; --_shadow: none;
block-size: auto; min-inline-size: 0; padding-inline: 0; border-radius: var(--radius-sm);
font-weight: var(--font-weight-medium);
text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em;
}
.af-button--link:active:not(:disabled):not([aria-disabled="true"]) { box-shadow: none; }
@media (hover: hover) {
.af-button--link:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--color-link-hover); text-decoration-thickness: 2px; }
}
/* Sizes ─────────────────────────────────────────────────────────────────── */
.af-button--xs { --_h: var(--button-height-xs); --_px: var(--button-padding-x-xs); --_fs: var(--button-font-size-xs); --_icon: var(--icon-size-xs); --_gap: var(--space-1-5); }
.af-button--sm { --_h: var(--button-height-sm); --_px: var(--button-padding-x-sm); --_fs: var(--button-font-size-sm); --_icon: var(--icon-size-sm); }
.af-button--md { /* default */ }
.af-button--lg { --_h: var(--button-height-lg); --_px: var(--button-padding-x-lg); --_fs: var(--button-font-size-lg); --_icon: var(--icon-size-md); }
@media (pointer: coarse) {
.af-button--xs { --_h: var(--button-height-touch-xs); }
.af-button--sm { --_h: var(--button-height-touch-sm); }
}
.af-button--block { display: flex; inline-size: 100%; }
/* Icon button: square, no label. Always give it an accessible name (aria-label) and, where the meaning is not obvious,
* a tooltip. */
.af-button--icon { padding-inline: 0; inline-size: var(--_h); }
/* States ────────────────────────────────────────────────────────────────── */
/* Toggle button: aria-pressed on a <button>. Pressed = the key stays pushed in, with an accent edge. */
.af-button[aria-pressed="true"] {
--_bg: var(--color-accent-bg-strong); --_bg-hover: var(--color-accent-bg-strong); --_bg-active: var(--color-accent-bg-strong);
--_text: var(--color-text); --_border: var(--color-accent-solid); --_shadow: var(--shadow-pressed);
}
.af-button--ghost[aria-pressed="true"], .af-button--tertiary[aria-pressed="true"] { --_border: var(--color-accent-solid); }
/* Loading: keep the label and the button's size; show a spinner; block activation (JS also ignores clicks). The button
* stays focusable. Announce progress in a nearby live region, not on the button. */
.af-button[aria-busy="true"] { cursor: progress; }
.af-button[aria-busy="true"]:active { box-shadow: var(--_shadow); background: var(--_bg); }
/* Disabled: native :disabled, or aria-disabled="true" when the control must stay focusable (e.g. to expose a tooltip that
* explains why). */
.af-button:disabled, .af-button[aria-disabled="true"] {
background: var(--action-disabled-bg);
border-color: var(--action-disabled-border);
color: var(--action-disabled-text);
box-shadow: none;
}
.af-button--link:disabled, .af-button--link[aria-disabled="true"],
.af-button--ghost:disabled, .af-button--ghost[aria-disabled="true"] { background: transparent; border-color: transparent; }
/* styles/components/feedback.css · spinner */
/* Indeterminate activity for operations without measurable progress, shown with text that says what is happening. Prefer
* <progress> when you know the fraction. Under reduced motion the ring is static (a three-quarter arc) — the label still says
* what is happening. Decorative: aria-hidden="true"; announce busy state in a live region or via aria-busy on the region. */
.af-spinner { inline-size: var(--icon-size-md); block-size: var(--icon-size-md); flex: none; border: 2px solid color-mix(in srgb, currentColor 25%, transparent); border-block-start-color: currentColor; border-radius: 50%; animation: af-spin 0.8s linear infinite; }
.af-spinner--sm { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); border-width: 1.5px; }
.af-spinner--lg { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); border-width: 2.5px; }
.af-button > .af-spinner { inline-size: var(--_icon); block-size: var(--_icon); }
@keyframes af-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .af-spinner { animation: none; border-color: currentColor; border-inline-end-color: transparent; } }
.af-loading { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--color-muted-text); font-size: var(--type-body-sm-size); }Design tokens this component reads. Override a token, not the component.
--action-accent-bg--action-accent-bg-active--action-accent-bg-hover--action-accent-text--action-destructive-bg--action-destructive-bg-active--action-destructive-bg-hover--action-destructive-text--action-disabled-bg--action-disabled-border--action-disabled-text--action-ghost-bg-active--action-ghost-bg-hover--action-ghost-text--action-primary-bg--action-primary-bg-active--action-primary-bg-hover--action-primary-border--action-primary-text--action-secondary-bg--action-secondary-bg-active--action-secondary-bg-hover--action-secondary-border--action-secondary-text--action-tertiary-bg--action-tertiary-bg-active--action-tertiary-bg-hover--action-tertiary-text--border-width-hairline--button-font-size-lg--button-font-size-md--button-font-size-sm--button-font-size-xs--button-font-weight--button-gap--button-height-lg--button-height-md--button-height-sm--button-height-touch-sm--button-height-touch-xs--button-height-xs--button-padding-x-lg--button-padding-x-md--button-padding-x-sm--button-padding-x-xs--button-radius--color-accent-bg-strong--color-accent-solid--color-link--color-link-hover--color-text--ease-standard--font-body--font-weight-medium--icon-size-md--icon-size-sm--icon-size-xs--motion-fast--radius-sm--shadow-pressed--shadow-raised-control--shadow-raised-primary--space-1-5--color-muted-text--icon-size-lg--space-2--type-body-sm-size<div class="af-split-button">
<button class="af-button af-button--primary af-button--md" type="button"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload files</button>
<button class="af-button af-button--primary af-button--md" type="button" popovertarget="act-split-menu" aria-haspopup="menu" aria-expanded="false" aria-label="More upload options"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></button>
</div>
<div class="af-menu" id="act-split-menu" popover role="menu" aria-label="Upload options" data-placement="bottom-end">
<button class="af-menu__item" role="menuitem" type="button" data-action="upload-folder"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-folder-plus"/></svg>Upload folder</button>
<button class="af-menu__item" role="menuitem" type="button" data-action="from-url"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-link"/></svg>Import from URL</button>
</div>/* styles/components/actions.css · split-button */
/* A primary action plus a menu of related actions. The chevron button is a real <button> with aria-haspopup="menu". */
.af-split-button { display: inline-flex; vertical-align: middle; position: relative; }
.af-split-button > .af-button:first-child { border-start-end-radius: 0; border-end-end-radius: 0; }
.af-split-button > .af-button:last-child { border-start-start-radius: 0; border-end-start-radius: 0; min-inline-size: var(--_h); padding-inline: 0; border-inline-start-color: color-mix(in srgb, var(--_text) 32%, transparent); }
.af-split-button > .af-button:focus-visible { z-index: 1; }
/* styles/components/overlays.css · menu */
/* Dropdown menu and context menu share one panel: role="menu" > role="menuitem" (or menuitemcheckbox / menuitemradio).
* Keyboard: ArrowDown/Up move, Home/End jump, type-ahead, Enter/Space activate, Escape closes and returns focus to the
* trigger, Tab closes. The trigger has aria-haspopup="menu" and aria-expanded. Destructive items are last, after a separator,
* and open a confirmation — a menu item never deletes directly. */
.af-menu { position: fixed; margin: 0; inset: auto; min-inline-size: var(--popover-min-width); max-inline-size: min(20rem, calc(100vw - 2 * var(--space-4))); max-block-size: min(24rem, calc(100dvh - 2 * var(--space-4))); overflow-y: auto; overscroll-behavior: contain; padding: var(--space-1); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--popover-radius); background: var(--color-surface-raised); color: var(--color-text); box-shadow: var(--popover-shadow); }
.af-menu:not(:popover-open) { display: none; }
.af-menu__item { display: flex; align-items: center; gap: var(--space-3); inline-size: 100%; min-block-size: var(--menu-item-height); padding: var(--space-1-5) var(--space-3); border-radius: var(--menu-item-radius); color: var(--color-text); font: var(--font-weight-regular) var(--type-body-sm-size)/1.3 var(--font-body); text-align: start; text-decoration: none; cursor: pointer; }
.af-menu__item > .af-icon { inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); color: var(--color-muted-text); }
.af-menu__item:hover, .af-menu__item:focus { background: var(--color-surface-hover); }
.af-menu__item:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: calc(-1 * var(--focus-ring-width)); }
.af-menu__item[aria-disabled="true"] { color: var(--color-disabled-text); cursor: not-allowed; }
.af-menu__item[aria-disabled="true"] > .af-icon { color: inherit; }
.af-menu__item[aria-disabled="true"]:hover { background: none; }
.af-menu__item--danger { color: var(--status-danger-text); }
.af-menu__item--danger > .af-icon { color: var(--status-danger-solid); }
.af-menu__item--danger:hover, .af-menu__item--danger:focus { background: var(--status-danger-bg); }
.af-menu__shortcut { margin-inline-start: auto; padding-inline-start: var(--space-4); font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }
.af-menu__item[aria-checked="true"]::after { content: ""; margin-inline-start: auto; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); background: var(--color-accent-text); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat; }
.af-menu__label { padding: var(--space-1-5) var(--space-3) var(--space-1); font: var(--type-eyebrow-weight) 0.6875rem/1.3 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); }
.af-menu__sep { block-size: 0; margin: var(--space-1) calc(-1 * var(--space-1)); border: 0; border-block-start: 1px solid var(--color-border); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-accent-text--color-border--color-border-strong--color-disabled-text--color-muted-text--color-surface-hover--color-surface-raised--color-text--focus-ring-color--focus-ring-width--font-body--font-weight-regular--icon-size-sm--menu-item-height--menu-item-radius--popover-min-width--popover-radius--popover-shadow--space-1--space-1-5--space-3--space-4--status-danger-bg--status-danger-solid--status-danger-text--type-body-sm-size--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-weight--type-metadata-family--type-metadata-size--type-metadata-weight<div class="af-button-group" role="group" aria-label="View">
<button class="af-button af-button--secondary af-button--md" type="button" aria-pressed="true"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-list"/></svg>List</button>
<button class="af-button af-button--secondary af-button--md" type="button" aria-pressed="false" data-toggle><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-columns-3"/></svg>Columns</button>
</div>/* styles/components/actions.css · button-group */
.af-button-group { display: inline-flex; vertical-align: middle; }
.af-button-group > .af-button { border-radius: 0; }
.af-button-group > .af-button + .af-button { margin-inline-start: calc(-1 * var(--border-width-hairline)); }
.af-button-group > .af-button:first-child { border-start-start-radius: var(--button-radius); border-end-start-radius: var(--button-radius); }
.af-button-group > .af-button:last-child { border-start-end-radius: var(--button-radius); border-end-end-radius: var(--button-radius); }
.af-button-group > .af-button:focus-visible { z-index: 1; }
.af-button-group > .af-button[aria-pressed="true"] { z-index: 1; }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--button-radiusRead the accessibility notes or open the WCAG 2.2 specification.
<p>Read the <a class="af-link" href="#accessibility">accessibility notes</a> or open the <a class="af-link" href="https://www.w3.org/TR/WCAG22/" rel="noopener">WCAG 2.2 specification<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-external-link"/></svg></a>.</p>/* styles/components/actions.css · link */
/* A styled anchor for navigation. Use .af-button--link for an action that looks like a link, <a class="af-link"> for
* navigation. Always underlined: colour alone never identifies a link (WCAG 1.4.1). */
.af-link {
color: var(--color-link);
font-weight: var(--font-weight-medium);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
border-radius: var(--radius-sm);
}
.af-link:hover { color: var(--color-link-hover); text-decoration-thickness: 2px; }
.af-link > .af-icon { display: inline; inline-size: 1em; block-size: 1em; vertical-align: -0.125em; margin-inline-start: var(--space-1); }
.af-link--muted { color: var(--color-muted-text); }
.af-link--plain { text-decoration: none; }
.af-link--plain:hover { text-decoration: underline; }Design tokens this component reads. Override a token, not the component.
--color-link--color-link-hover--color-muted-text--font-weight-medium--radius-sm--space-1Do
Label buttons with the verb and the object: “Create bucket”, “Revoke key”, “Delete 3 objects”.
Use one primary button per region and put it last (or first on small screens) in the action row.
Don't
Write “OK”, “Submit” or “Click here”. Invent a new button style when a variant fits. Use a destructive button without a confirmation step for anything irreversible.
Components
Every control is a recessed well: you are pressing into a field, not onto one. Labels are always visible, errors say what to do, and validity is never shown by colour alone.
A field is label, optional hint, control, then message. The hint and the error are linked to the control with aria-describedby.
Lowercase letters, digits and hyphens. Cannot be changed later.
Use lowercase letters, digits or hyphens only.
Name is available (sample check).
<div class="af-field"><label class="af-field__label" for="fm-a">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fm-a" placeholder="acme-assets" aria-describedby="fm-a-h"><p class="af-field__hint" id="fm-a-h">Lowercase letters, digits and hyphens. Cannot be changed later.</p></div>
<div class="af-field"><label class="af-field__label" for="fm-b">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fm-b" value="Acme_Assets" aria-invalid="true" aria-describedby="fm-b-e"><p class="af-field__error" id="fm-b-e"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-alert"/></svg><span>Use lowercase letters, digits or hyphens only.</span></p></div>
<div class="af-field"><label class="af-field__label" for="fm-c">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fm-c" value="acme-assets" data-valid="true" aria-describedby="fm-c-s"><p class="af-field__success" id="fm-c-s"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg><span>Name is available (sample check).</span></p></div>
<div class="af-field"><label class="af-field__label" for="fm-d">Region <span class="af-field__optional">optional</span></label><input class="af-input" id="fm-d" value="Set at creation" disabled></div>
<div class="af-field"><label class="af-field__label" for="fm-e">Bucket ID</label><input class="af-input af-input--mono" id="fm-e" value="bkt_SAMPLE_0001" readonly></div>/* styles/components/forms.css · field */
.af-field { display: flex; flex-direction: column; gap: var(--space-1-5); min-inline-size: 0; }
.af-field__label {
display: flex; align-items: baseline; gap: var(--space-1);
font: var(--type-label-weight) var(--type-label-size)/var(--type-label-line-height) var(--type-label-family);
color: var(--color-text);
}
.af-field__required { color: var(--status-danger-text); font-weight: var(--font-weight-semibold); }
.af-field__optional { color: var(--color-muted-text); font-weight: var(--font-weight-regular); }
.af-field__description, .af-field__hint {
margin: 0;
font: var(--type-caption-weight) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family);
color: var(--color-muted-text);
}
.af-field__error, .af-field__success {
display: flex; align-items: flex-start; gap: var(--space-1-5); margin: 0;
font: var(--font-weight-medium) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family);
}
.af-field__error { color: var(--status-danger-text); }
.af-field__success { color: var(--status-success-text); }
.af-field__error > .af-icon, .af-field__success > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); margin-block-start: 1px; }
.af-field__counter { margin-inline-start: auto; font-variant-numeric: tabular-nums; color: var(--color-muted-text); font-size: var(--type-caption-size); }
.af-field--inline { flex-direction: row; align-items: center; gap: var(--space-3); }
/* styles/components/forms.css · input */
.af-input, .af-textarea, .af-select__control {
--_h: var(--input-height-md);
display: block;
inline-size: 100%;
min-inline-size: 0;
block-size: var(--_h);
padding-inline: var(--input-padding-x);
border: var(--border-width-hairline) solid var(--input-border);
border-radius: var(--input-radius);
background: var(--input-bg);
color: var(--color-text);
box-shadow: var(--input-shadow);
font-family: var(--font-body);
font-size: var(--type-body-size);
line-height: 1.25;
transition: border-color var(--motion-fast) var(--ease-standard), background-color var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard);
}
.af-input::placeholder, .af-textarea::placeholder { color: var(--input-placeholder); opacity: 1; }
@media (hover: hover) {
.af-input:hover:not(:disabled):not([readonly]), .af-textarea:hover:not(:disabled):not([readonly]), .af-select__control:hover:not(:disabled) { border-color: var(--input-border-hover); }
}
.af-input:focus-visible, .af-textarea:focus-visible, .af-select__control:focus-visible { border-color: var(--input-border-focus); outline-offset: 0; }
.af-input--sm { --_h: var(--input-height-sm); }
.af-input--lg { --_h: var(--input-height-lg); }
.af-input--mono, .af-textarea--mono { font-family: var(--font-mono); font-size: var(--type-code-size); font-variant-ligatures: none; }
.af-input[type="number"] { font-variant-numeric: tabular-nums; }
/* Validation */
.af-input[aria-invalid="true"], .af-textarea[aria-invalid="true"], .af-select__control[aria-invalid="true"] {
border-color: var(--input-border-invalid);
box-shadow: var(--input-shadow), inset 0 0 0 var(--border-width-hairline) var(--input-border-invalid);
}
.af-input[data-valid="true"], .af-textarea[data-valid="true"], .af-select__control[data-valid="true"] { border-color: var(--input-border-valid); }
/* Disabled: not focusable, not submitted, may fall below text contrast (WCAG exempt). Prefer explaining why in the
* description rather than hiding the reason. */
.af-input:disabled, .af-textarea:disabled, .af-select__control:disabled {
background: var(--color-surface-disabled);
border-color: var(--action-disabled-border);
color: var(--color-disabled-text);
box-shadow: none;
cursor: not-allowed;
}
.af-input:disabled::placeholder, .af-textarea:disabled::placeholder { color: var(--color-disabled-text); }
/* Read-only: focusable, selectable, full-contrast text — the value is real, it just cannot be edited here. A dashed boundary
* (still ≥3:1) and a flat inset surface distinguish it from an editable field without making it look disabled. */
.af-input[readonly], .af-textarea[readonly] {
background: var(--color-surface-inset);
border-style: dashed;
box-shadow: none;
}
.af-textarea { block-size: auto; min-block-size: calc(var(--input-height-md) * 2); padding-block: var(--space-3); line-height: 1.5; }
/* Select: native <select> wrapped for the icon. Native keeps keyboard, mobile pickers and screen-reader behaviour. */
.af-select { position: relative; display: block; }
.af-select__control { appearance: none; -webkit-appearance: none; padding-inline-end: var(--space-10); cursor: pointer; text-overflow: ellipsis; }
.af-select__icon { position: absolute; inset-block: 0; inset-inline-end: var(--space-3); margin-block: auto; color: var(--color-muted-text); pointer-events: none; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }
.af-select__control:disabled + .af-select__icon { color: var(--color-disabled-text); }
.af-select__control option { background: var(--color-surface); color: var(--color-text); }Design tokens this component reads. Override a token, not the component.
--color-muted-text--color-text--font-weight-medium--font-weight-regular--font-weight-semibold--icon-size-xs--space-1--space-1-5--space-3--status-danger-text--status-success-text--type-caption-family--type-caption-line-height--type-caption-size--type-caption-weight--type-label-family--type-label-line-height--type-label-size--type-label-weight--action-disabled-border--border-width-hairline--color-disabled-text--color-surface--color-surface-disabled--color-surface-inset--ease-standard--font-body--font-mono--icon-size-sm--input-bg--input-border--input-border-focus--input-border-hover--input-border-invalid--input-border-valid--input-height-lg--input-height-md--input-height-sm--input-padding-x--input-placeholder--input-radius--input-shadow--motion-fast--space-10--type-body-size--type-code-size<input class="af-input af-input--sm" aria-label="Small input" placeholder="Small">
<input class="af-input" aria-label="Medium input" placeholder="Medium (default)">
<input class="af-input af-input--lg" aria-label="Large input" placeholder="Large">/* styles/components/forms.css · input */
.af-input, .af-textarea, .af-select__control {
--_h: var(--input-height-md);
display: block;
inline-size: 100%;
min-inline-size: 0;
block-size: var(--_h);
padding-inline: var(--input-padding-x);
border: var(--border-width-hairline) solid var(--input-border);
border-radius: var(--input-radius);
background: var(--input-bg);
color: var(--color-text);
box-shadow: var(--input-shadow);
font-family: var(--font-body);
font-size: var(--type-body-size);
line-height: 1.25;
transition: border-color var(--motion-fast) var(--ease-standard), background-color var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard);
}
.af-input::placeholder, .af-textarea::placeholder { color: var(--input-placeholder); opacity: 1; }
@media (hover: hover) {
.af-input:hover:not(:disabled):not([readonly]), .af-textarea:hover:not(:disabled):not([readonly]), .af-select__control:hover:not(:disabled) { border-color: var(--input-border-hover); }
}
.af-input:focus-visible, .af-textarea:focus-visible, .af-select__control:focus-visible { border-color: var(--input-border-focus); outline-offset: 0; }
.af-input--sm { --_h: var(--input-height-sm); }
.af-input--lg { --_h: var(--input-height-lg); }
.af-input--mono, .af-textarea--mono { font-family: var(--font-mono); font-size: var(--type-code-size); font-variant-ligatures: none; }
.af-input[type="number"] { font-variant-numeric: tabular-nums; }
/* Validation */
.af-input[aria-invalid="true"], .af-textarea[aria-invalid="true"], .af-select__control[aria-invalid="true"] {
border-color: var(--input-border-invalid);
box-shadow: var(--input-shadow), inset 0 0 0 var(--border-width-hairline) var(--input-border-invalid);
}
.af-input[data-valid="true"], .af-textarea[data-valid="true"], .af-select__control[data-valid="true"] { border-color: var(--input-border-valid); }
/* Disabled: not focusable, not submitted, may fall below text contrast (WCAG exempt). Prefer explaining why in the
* description rather than hiding the reason. */
.af-input:disabled, .af-textarea:disabled, .af-select__control:disabled {
background: var(--color-surface-disabled);
border-color: var(--action-disabled-border);
color: var(--color-disabled-text);
box-shadow: none;
cursor: not-allowed;
}
.af-input:disabled::placeholder, .af-textarea:disabled::placeholder { color: var(--color-disabled-text); }
/* Read-only: focusable, selectable, full-contrast text — the value is real, it just cannot be edited here. A dashed boundary
* (still ≥3:1) and a flat inset surface distinguish it from an editable field without making it look disabled. */
.af-input[readonly], .af-textarea[readonly] {
background: var(--color-surface-inset);
border-style: dashed;
box-shadow: none;
}
.af-textarea { block-size: auto; min-block-size: calc(var(--input-height-md) * 2); padding-block: var(--space-3); line-height: 1.5; }
/* Select: native <select> wrapped for the icon. Native keeps keyboard, mobile pickers and screen-reader behaviour. */
.af-select { position: relative; display: block; }
.af-select__control { appearance: none; -webkit-appearance: none; padding-inline-end: var(--space-10); cursor: pointer; text-overflow: ellipsis; }
.af-select__icon { position: absolute; inset-block: 0; inset-inline-end: var(--space-3); margin-block: auto; color: var(--color-muted-text); pointer-events: none; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }
.af-select__control:disabled + .af-select__icon { color: var(--color-disabled-text); }
.af-select__control option { background: var(--color-surface); color: var(--color-text); }Design tokens this component reads. Override a token, not the component.
--action-disabled-border--border-width-hairline--color-disabled-text--color-muted-text--color-surface--color-surface-disabled--color-surface-inset--color-text--ease-standard--font-body--font-mono--icon-size-sm--input-bg--input-border--input-border-focus--input-border-hover--input-border-invalid--input-border-valid--input-height-lg--input-height-md--input-height-sm--input-padding-x--input-placeholder--input-radius--input-shadow--motion-fast--space-10--space-3--type-body-size--type-code-size<div class="af-field"><label class="af-field__label" for="fm-ta">Description <span class="af-field__optional">optional</span></label><textarea class="af-textarea" id="fm-ta" rows="3" maxlength="200" data-counter="#fm-ta-c" aria-describedby="fm-ta-c"></textarea><span class="af-field__counter" id="fm-ta-c">0 / 200</span></div>
<div class="af-field"><label class="af-field__label" for="fm-sel">Storage class</label><div class="af-select"><select class="af-select__control" id="fm-sel"><option>Standard</option><option>Infrequent access</option><option>Archive</option></select><svg class="af-icon af-select__icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></div></div>/* styles/components/forms.css · field */
.af-field { display: flex; flex-direction: column; gap: var(--space-1-5); min-inline-size: 0; }
.af-field__label {
display: flex; align-items: baseline; gap: var(--space-1);
font: var(--type-label-weight) var(--type-label-size)/var(--type-label-line-height) var(--type-label-family);
color: var(--color-text);
}
.af-field__required { color: var(--status-danger-text); font-weight: var(--font-weight-semibold); }
.af-field__optional { color: var(--color-muted-text); font-weight: var(--font-weight-regular); }
.af-field__description, .af-field__hint {
margin: 0;
font: var(--type-caption-weight) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family);
color: var(--color-muted-text);
}
.af-field__error, .af-field__success {
display: flex; align-items: flex-start; gap: var(--space-1-5); margin: 0;
font: var(--font-weight-medium) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family);
}
.af-field__error { color: var(--status-danger-text); }
.af-field__success { color: var(--status-success-text); }
.af-field__error > .af-icon, .af-field__success > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); margin-block-start: 1px; }
.af-field__counter { margin-inline-start: auto; font-variant-numeric: tabular-nums; color: var(--color-muted-text); font-size: var(--type-caption-size); }
.af-field--inline { flex-direction: row; align-items: center; gap: var(--space-3); }
/* styles/components/forms.css · input */
.af-input, .af-textarea, .af-select__control {
--_h: var(--input-height-md);
display: block;
inline-size: 100%;
min-inline-size: 0;
block-size: var(--_h);
padding-inline: var(--input-padding-x);
border: var(--border-width-hairline) solid var(--input-border);
border-radius: var(--input-radius);
background: var(--input-bg);
color: var(--color-text);
box-shadow: var(--input-shadow);
font-family: var(--font-body);
font-size: var(--type-body-size);
line-height: 1.25;
transition: border-color var(--motion-fast) var(--ease-standard), background-color var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard);
}
.af-input::placeholder, .af-textarea::placeholder { color: var(--input-placeholder); opacity: 1; }
@media (hover: hover) {
.af-input:hover:not(:disabled):not([readonly]), .af-textarea:hover:not(:disabled):not([readonly]), .af-select__control:hover:not(:disabled) { border-color: var(--input-border-hover); }
}
.af-input:focus-visible, .af-textarea:focus-visible, .af-select__control:focus-visible { border-color: var(--input-border-focus); outline-offset: 0; }
.af-input--sm { --_h: var(--input-height-sm); }
.af-input--lg { --_h: var(--input-height-lg); }
.af-input--mono, .af-textarea--mono { font-family: var(--font-mono); font-size: var(--type-code-size); font-variant-ligatures: none; }
.af-input[type="number"] { font-variant-numeric: tabular-nums; }
/* Validation */
.af-input[aria-invalid="true"], .af-textarea[aria-invalid="true"], .af-select__control[aria-invalid="true"] {
border-color: var(--input-border-invalid);
box-shadow: var(--input-shadow), inset 0 0 0 var(--border-width-hairline) var(--input-border-invalid);
}
.af-input[data-valid="true"], .af-textarea[data-valid="true"], .af-select__control[data-valid="true"] { border-color: var(--input-border-valid); }
/* Disabled: not focusable, not submitted, may fall below text contrast (WCAG exempt). Prefer explaining why in the
* description rather than hiding the reason. */
.af-input:disabled, .af-textarea:disabled, .af-select__control:disabled {
background: var(--color-surface-disabled);
border-color: var(--action-disabled-border);
color: var(--color-disabled-text);
box-shadow: none;
cursor: not-allowed;
}
.af-input:disabled::placeholder, .af-textarea:disabled::placeholder { color: var(--color-disabled-text); }
/* Read-only: focusable, selectable, full-contrast text — the value is real, it just cannot be edited here. A dashed boundary
* (still ≥3:1) and a flat inset surface distinguish it from an editable field without making it look disabled. */
.af-input[readonly], .af-textarea[readonly] {
background: var(--color-surface-inset);
border-style: dashed;
box-shadow: none;
}
.af-textarea { block-size: auto; min-block-size: calc(var(--input-height-md) * 2); padding-block: var(--space-3); line-height: 1.5; }
/* Select: native <select> wrapped for the icon. Native keeps keyboard, mobile pickers and screen-reader behaviour. */
.af-select { position: relative; display: block; }
.af-select__control { appearance: none; -webkit-appearance: none; padding-inline-end: var(--space-10); cursor: pointer; text-overflow: ellipsis; }
.af-select__icon { position: absolute; inset-block: 0; inset-inline-end: var(--space-3); margin-block: auto; color: var(--color-muted-text); pointer-events: none; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }
.af-select__control:disabled + .af-select__icon { color: var(--color-disabled-text); }
.af-select__control option { background: var(--color-surface); color: var(--color-text); }Design tokens this component reads. Override a token, not the component.
--color-muted-text--color-text--font-weight-medium--font-weight-regular--font-weight-semibold--icon-size-xs--space-1--space-1-5--space-3--status-danger-text--status-success-text--type-caption-family--type-caption-line-height--type-caption-size--type-caption-weight--type-label-family--type-label-line-height--type-label-size--type-label-weight--action-disabled-border--border-width-hairline--color-disabled-text--color-surface--color-surface-disabled--color-surface-inset--ease-standard--font-body--font-mono--icon-size-sm--input-bg--input-border--input-border-focus--input-border-hover--input-border-invalid--input-border-valid--input-height-lg--input-height-md--input-height-sm--input-padding-x--input-placeholder--input-radius--input-shadow--motion-fast--space-10--type-body-size--type-code-sizeSelects are the native element, so the platform supplies keyboard behaviour and mobile pickers. Reach for a combobox only when the list is long enough to need filtering.
<fieldset class="af-fieldset"><legend class="af-fieldset__legend">Bucket features</legend>
<label class="af-check"><input class="af-check__input" type="checkbox" checked><span><span class="af-check__label">Versioning</span><span class="af-check__desc">Keep earlier versions of overwritten objects.</span></span></label>
<label class="af-check"><input class="af-check__input" type="checkbox"><span><span class="af-check__label">Object lock</span><span class="af-check__desc">Block deletes for a retention period.</span></span></label>
<label class="af-check"><input class="af-check__input" type="checkbox" disabled><span><span class="af-check__label">Replication</span><span class="af-check__desc">Unavailable in this sample.</span></span></label>
</fieldset>
<fieldset class="af-fieldset"><legend class="af-fieldset__legend">Access</legend>
<label class="af-check"><input class="af-check__input af-radio__input" type="radio" name="fm-acc" checked><span class="af-check__label">Private</span></label>
<label class="af-check"><input class="af-check__input af-radio__input" type="radio" name="fm-acc"><span class="af-check__label">Signed URLs only</span></label>
<label class="af-check"><input class="af-check__input af-radio__input" type="radio" name="fm-acc"><span class="af-check__label">Public read</span></label>
</fieldset>
<div class="af-stack">
<label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch" checked><span class="af-switch-field__text">Request logging</span></label>
<label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch"><span class="af-switch-field__text">Public access</span></label>
<label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch" disabled><span class="af-switch-field__text">Disabled switch</span></label>
</div>/* styles/components/forms.css · checkbox */
.af-check { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: var(--space-3); row-gap: 0; align-items: start; cursor: pointer; min-block-size: var(--space-6); }
.af-check--disabled, .af-check:has(input:disabled) { cursor: not-allowed; color: var(--color-disabled-text); }
.af-check__input {
appearance: none; -webkit-appearance: none;
display: grid; place-content: center;
inline-size: var(--check-size); block-size: var(--check-size);
margin: 3px 0 0; /* optically centres the box on the first 24px line */
border: var(--border-width-hairline) solid var(--input-border);
border-radius: var(--check-radius);
background: var(--input-bg);
box-shadow: var(--input-shadow);
cursor: inherit;
transition: background-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard);
}
.af-check__input::after {
content: ""; inline-size: var(--check-size); block-size: var(--check-size);
background: var(--action-primary-text);
-webkit-mask: var(--_mark) center / 72% no-repeat; mask: var(--_mark) center / 72% no-repeat;
--_mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
transform: scale(0);
transition: transform var(--motion-fast) var(--ease-standard);
}
.af-check__input:checked, .af-check__input:indeterminate { background: var(--action-primary-bg); border-color: var(--action-primary-border); box-shadow: var(--shadow-raised-primary); }
.af-check__input:checked::after, .af-check__input:indeterminate::after { transform: scale(1); }
.af-check__input:indeterminate::after { --_mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E"); }
.af-check__input[aria-invalid="true"] { border-color: var(--input-border-invalid); }
.af-check__input:disabled { background: var(--color-surface-disabled); border-color: var(--action-disabled-border); box-shadow: none; }
.af-check__input:disabled:checked { background: var(--action-disabled-text); border-color: var(--action-disabled-text); }
.af-check__label { font-size: var(--type-body-size); line-height: 1.5; }
.af-check__desc { display: block; font: var(--type-caption-weight) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family); color: var(--color-muted-text); }
.af-check:has(input:disabled) .af-check__desc { color: var(--color-disabled-text); }
@media (hover: hover) { .af-check__input:not(:disabled):not(:checked):hover { border-color: var(--input-border-hover); } }
/* styles/components/forms.css · radio */
.af-radio__input { border-radius: var(--radius-full); }
.af-radio__input::after { -webkit-mask: none; mask: none; background: var(--action-primary-text); inline-size: 6px; block-size: 6px; border-radius: 50%; }
.af-radio__input:checked { box-shadow: var(--shadow-raised-primary); }
/* styles/components/forms.css · switch */
/* A switch applies immediately (no Save step). If a change needs confirming, use a checkbox in a form instead. State is
* conveyed by thumb position as well as colour. Markup: <input type="checkbox" role="switch" class="af-switch">. */
.af-switch {
--_inset: calc((var(--switch-height) - var(--switch-thumb)) / 2 - var(--border-width-hairline));
appearance: none; -webkit-appearance: none;
position: relative; flex: none;
inline-size: var(--switch-width); block-size: var(--switch-height);
margin: 0;
border: var(--border-width-hairline) solid var(--input-border);
border-radius: var(--radius-full);
background: var(--color-surface-inset);
box-shadow: var(--shadow-inset-well);
cursor: pointer;
transition: background-color var(--motion-standard) var(--ease-standard), border-color var(--motion-standard) var(--ease-standard);
}
.af-switch::before {
content: ""; position: absolute;
inset-block-start: var(--_inset); inset-inline-start: var(--_inset);
inline-size: var(--switch-thumb); block-size: var(--switch-thumb);
border-radius: 50%;
background: var(--color-border-control);
transition: inset-inline-start var(--motion-standard) var(--ease-standard), background-color var(--motion-standard) var(--ease-standard);
}
.af-switch:checked { background: var(--color-accent-solid); border-color: var(--color-accent-solid); }
.af-switch:checked::before { inset-inline-start: calc(100% - var(--switch-thumb) - var(--_inset)); background: var(--color-surface); box-shadow: var(--shadow-raised-control); }
.af-switch:disabled { background: var(--color-surface-disabled); border-color: var(--action-disabled-border); box-shadow: none; cursor: not-allowed; }
.af-switch:disabled::before { background: var(--action-disabled-text); }
.af-switch:disabled:checked { background: var(--action-disabled-text); }
.af-switch:disabled:checked::before { background: var(--color-surface-disabled); box-shadow: none; }
.af-switch-field { display: flex; align-items: flex-start; gap: var(--space-3); }
.af-switch-field > .af-switch { margin-block-start: 2px; }
.af-switch-field__text { display: flex; flex-direction: column; min-inline-size: 0; }Design tokens this component reads. Override a token, not the component.
--action-disabled-border--action-disabled-text--action-primary-bg--action-primary-border--action-primary-text--border-width-hairline--check-radius--check-size--color-disabled-text--color-muted-text--color-surface-disabled--ease-standard--input-bg--input-border--input-border-hover--input-border-invalid--input-shadow--motion-fast--shadow-raised-primary--space-3--space-6--type-body-size--type-caption-family--type-caption-line-height--type-caption-size--type-caption-weight--radius-full--color-accent-solid--color-border-control--color-surface--color-surface-inset--motion-standard--shadow-inset-well--shadow-raised-control--switch-height--switch-thumb--switch-widthA switch applies immediately and takes effect on its own. A checkbox waits for the form to be submitted. Do not use a switch inside a form that has a Save button.
<div class="af-field"><label class="af-field__label" for="fm-rng">Retention</label><input class="af-slider" type="range" id="fm-rng" min="1" max="90" value="30" data-output="#fm-rng-o" data-unit=" days" aria-describedby="fm-rng-o"><output id="fm-rng-o" class="af-field__hint">30 days</output></div>
<div class="af-field"><label class="af-field__label" for="fm-num">Max keys</label><div class="af-number">
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-number-step="-1" data-number-target="#fm-num" aria-label="Decrease max keys"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-minus"/></svg></button>
<input class="af-input" id="fm-num" type="number" inputmode="numeric" value="5" min="1" max="10">
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-number-step="1" data-number-target="#fm-num" aria-label="Increase max keys"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-plus"/></svg></button></div></div>
<fieldset class="af-fieldset"><legend class="af-fieldset__legend">Time range</legend><div class="af-segmented">
<span class="af-segmented__option"><input class="af-segmented__input" type="radio" name="fm-seg" id="fm-s1" checked><label class="af-segmented__label" for="fm-s1">24 h</label></span>
<span class="af-segmented__option"><input class="af-segmented__input" type="radio" name="fm-seg" id="fm-s2"><label class="af-segmented__label" for="fm-s2">7 d</label></span>
<span class="af-segmented__option"><input class="af-segmented__input" type="radio" name="fm-seg" id="fm-s3"><label class="af-segmented__label" for="fm-s3">30 d</label></span></div></fieldset>/* styles/components/forms.css · slider */
.af-slider { --_fill: 0%; appearance: none; -webkit-appearance: none; inline-size: 100%; block-size: var(--slider-thumb); margin: 0; background: transparent; cursor: pointer; }
.af-slider::-webkit-slider-runnable-track {
block-size: var(--slider-track); border: var(--border-width-hairline) solid var(--input-border); border-radius: var(--radius-full);
background: linear-gradient(to right, var(--color-accent-solid) var(--_fill), var(--color-surface-inset) var(--_fill));
box-shadow: var(--shadow-inset-well);
}
.af-slider::-moz-range-track { block-size: var(--slider-track); border: var(--border-width-hairline) solid var(--input-border); border-radius: var(--radius-full); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); }
.af-slider::-moz-range-progress { block-size: var(--slider-track); border-radius: var(--radius-full); background: var(--color-accent-solid); }
.af-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
inline-size: var(--slider-thumb); block-size: var(--slider-thumb);
margin-block-start: calc((var(--slider-track) - var(--slider-thumb)) / 2 - var(--border-width-hairline));
border: var(--border-width-hairline) solid var(--input-border); border-radius: 50%;
background: var(--color-surface-raised); box-shadow: var(--shadow-raised-control);
}
.af-slider::-moz-range-thumb { inline-size: var(--slider-thumb); block-size: var(--slider-thumb); box-sizing: border-box; border: var(--border-width-hairline) solid var(--input-border); border-radius: 50%; background: var(--color-surface-raised); box-shadow: var(--shadow-raised-control); }
.af-slider:focus-visible { outline-offset: var(--space-1); border-radius: var(--radius-full); }
.af-slider:disabled { cursor: not-allowed; opacity: 0.5; }
.af-slider-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); }
.af-slider-field__value { min-inline-size: 4ch; text-align: end; font: var(--type-numeric-weight) var(--type-numeric-size)/1 var(--type-numeric-family); font-variant-numeric: tabular-nums; }
/* styles/components/forms.css · number-input */
.af-number { display: flex; align-items: stretch; }
.af-number > .af-input { text-align: center; border-radius: 0; margin-inline: calc(-1 * var(--border-width-hairline)); flex: 1; -moz-appearance: textfield; appearance: textfield; }
.af-number > .af-input::-webkit-outer-spin-button, .af-number > .af-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.af-number > .af-button { --_h: var(--input-height-md); }
.af-number > .af-button:first-child { border-start-end-radius: 0; border-end-end-radius: 0; }
.af-number > .af-button:last-child { border-start-start-radius: 0; border-end-start-radius: 0; }
.af-number > .af-input:focus-visible { position: relative; z-index: 1; }
/* styles/components/forms.css · segmented-control */
/* A small set of mutually exclusive views or modes. Native radio inputs inside a <fieldset>; arrow keys move selection. */
.af-segmented { display: inline-flex; padding: var(--space-0-5); border: var(--border-width-hairline) solid var(--input-border); border-radius: calc(var(--radius-sm) + var(--space-0-5)); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); }
.af-segmented__option { position: relative; display: inline-flex; }
.af-segmented__input { position: absolute; inline-size: 100%; block-size: 100%; margin: 0; opacity: 0; cursor: pointer; }
.af-segmented__label {
display: inline-flex; align-items: center; gap: var(--space-1-5);
block-size: calc(var(--button-height-sm) - 2 * var(--space-0-5) - 2 * var(--border-width-hairline)); padding-inline: var(--space-3);
border: var(--border-width-hairline) solid transparent; border-radius: var(--radius-sm);
font: var(--type-label-weight) var(--type-label-size)/1 var(--type-label-family);
color: var(--color-muted-text); white-space: nowrap; pointer-events: none;
transition: background-color var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);
}
.af-segmented__input:checked + .af-segmented__label { background: var(--color-surface-raised); border-color: var(--color-border-strong); box-shadow: var(--shadow-raised-control); color: var(--color-text); }
.af-segmented__input:focus-visible + .af-segmented__label { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); }
.af-segmented__input:disabled + .af-segmented__label { color: var(--color-disabled-text); }
@media (hover: hover) { .af-segmented__input:not(:checked):not(:disabled):hover + .af-segmented__label { color: var(--color-text); } }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-accent-solid--color-surface-inset--color-surface-raised--input-border--radius-full--shadow-inset-well--shadow-raised-control--slider-thumb--slider-track--space-1--space-3--type-numeric-family--type-numeric-size--type-numeric-weight--input-height-md--button-height-sm--color-border-strong--color-disabled-text--color-muted-text--color-text--ease-standard--focus-ring-color--focus-ring-offset--focus-ring-width--motion-fast--radius-sm--space-0-5--space-1-5--type-label-family--type-label-size--type-label-weightRe-masks after 30 seconds. Never revealed automatically.
<div class="af-search"><svg class="af-icon af-search__icon" aria-hidden="true" focusable="false"><use href="#i-search"/></svg><input class="af-input" type="search" placeholder="Search objects" aria-label="Search objects" data-search-shortcut><kbd class="af-search__kbd">/</kbd></div>
<div class="af-field"><label class="af-field__label" for="fm-ig">Custom domain</label><div class="af-input-group"><span class="af-input-group__addon af-input-group__addon--mono">https://</span><input class="af-input af-input--mono" id="fm-ig" value="assets.example.test"></div></div>
<div class="af-field"><label class="af-field__label" for="fm-cp">Endpoint (sample value)</label><div class="af-input-group af-copyable"><input class="af-input" id="fm-cp" value="https://storage.example.test/v1" readonly><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-copy-target="#fm-cp" data-copy-label="Endpoint" aria-label="Copy endpoint"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button></div></div>
<div class="af-field"><label class="af-field__label" for="fm-sc">Secret key (sample value)</label><div class="af-input-group af-secret"><input class="af-input" id="fm-sc" type="password" value="sample-secret-0000-0000" readonly autocomplete="off"><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-secret-toggle="#fm-sc" aria-pressed="false" aria-label="Show secret"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-eye"/></svg></button></div><p class="af-field__hint">Re-masks after 30 seconds. Never revealed automatically.</p></div>/* styles/components/forms.css · search-input */
.af-search { position: relative; display: block; min-inline-size: 0; }
.af-search > .af-input { padding-inline-start: var(--space-10); padding-inline-end: var(--space-10); }
.af-search__icon { position: absolute; inset-block: 0; inset-inline-start: var(--space-3); margin-block: auto; color: var(--color-muted-text); pointer-events: none; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }
.af-search__clear { position: absolute; inset-block: 0; inset-inline-end: var(--space-1); margin-block: auto; }
.af-search__kbd { position: absolute; inset-block: 0; inset-inline-end: var(--space-3); margin-block: auto; block-size: fit-content; pointer-events: none; }
.af-search > .af-input::-webkit-search-cancel-button, .af-search > .af-input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
/* styles/components/forms.css · input-group */
/* Joins an input with addons and/or buttons into one control with a single boundary. Size with --sm / --lg on the group. */
.af-input-group { --_gh: var(--input-height-md); display: flex; align-items: stretch; min-inline-size: 0; }
.af-input-group--sm { --_gh: var(--input-height-sm); }
.af-input-group--lg { --_gh: var(--input-height-lg); }
.af-input-group > * { border-radius: 0; flex: none; }
.af-input-group > .af-input, .af-input-group > .af-search, .af-input-group > .af-select { flex: 1 1 auto; }
.af-input-group > .af-input, .af-input-group > .af-select .af-select__control { --_h: var(--_gh); }
.af-input-group > .af-button { --_h: var(--_gh); }
.af-input-group > * + * { margin-inline-start: calc(-1 * var(--border-width-hairline)); }
.af-input-group > :first-child, .af-input-group > :first-child .af-input, .af-input-group > :first-child .af-select__control { border-start-start-radius: var(--input-radius); border-end-start-radius: var(--input-radius); }
.af-input-group > :last-child, .af-input-group > :last-child .af-input, .af-input-group > :last-child .af-select__control { border-start-end-radius: var(--input-radius); border-end-end-radius: var(--input-radius); }
.af-input-group > :focus-visible, .af-input-group > .af-input:focus-visible { position: relative; z-index: 1; }
.af-input-group__addon {
display: inline-flex; align-items: center; gap: var(--space-1-5);
block-size: var(--_gh); padding-inline: var(--input-padding-x);
border: var(--border-width-hairline) solid var(--input-border);
background: var(--color-surface-inset);
color: var(--color-muted-text);
font-size: var(--type-body-sm-size); white-space: nowrap;
}
.af-input-group__addon--mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); }
/* styles/components/forms.css · copyable-input */
/* A read-only value with a copy button. Markup: <div class="af-input-group af-copyable">. The button's label changes to
* "Copied" and a polite live region announces it (scripts/component-interactions.js). Used for endpoints, bucket names, IDs
* and public URLs. */
.af-copyable > .af-input { font-family: var(--font-mono); font-size: var(--type-code-size); font-variant-ligatures: none; text-overflow: ellipsis; }
.af-copyable [data-copied="true"] { --_text: var(--status-success-text); }
/* styles/components/forms.css · secret-field */
/* A masked value that can be revealed (toggle button with aria-pressed) and copied. Never auto-reveal. Secrets that cannot be
* recovered (API keys after creation) use the Secret reveal panel in patterns/infrastructure.css instead. */
.af-secret > .af-input { font-family: var(--font-mono); letter-spacing: 0.06em; }
.af-secret > .af-input[type="text"] { letter-spacing: 0; }Design tokens this component reads. Override a token, not the component.
--color-muted-text--icon-size-sm--space-1--space-10--space-3--border-width-hairline--color-surface-inset--font-mono--input-border--input-height-lg--input-height-md--input-height-sm--input-padding-x--input-radius--space-1-5--type-body-sm-size--type-metadata-size--status-success-text--type-code-sizeA combobox follows the ARIA 1.2 pattern: a text input that owns a listbox, with focus staying in the input and aria-activedescendant pointing at the active option.
<div class="af-field"><label class="af-field__label" for="fm-cb">Region (sample)</label>
<div class="af-combobox" data-combobox>
<input class="af-input af-combobox__input" id="fm-cb" role="combobox" aria-expanded="false" aria-controls="fm-cb-l" aria-autocomplete="list" autocomplete="off" placeholder="Search regions">
<button type="button" class="af-button af-button--ghost af-button--icon af-button--sm af-combobox__toggle" tabindex="-1" aria-label="Show regions"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></button>
<ul class="af-listbox" id="fm-cb-l" role="listbox" aria-label="Regions" hidden>
<li class="af-option" role="option" data-value="region-a">Sample region A</li><li class="af-option" role="option" data-value="region-b">Sample region B</li><li class="af-option" role="option" data-value="region-c">Sample region C</li><li class="af-option" role="option" data-value="region-d">Sample region D</li>
</ul></div></div>
<div class="af-field"><label class="af-field__label" id="fm-ms-l" for="fm-ms">Labels</label>
<div class="af-multiselect" data-multiselect data-name="labels">
<div class="af-multiselect__control"><input class="af-multiselect__input" id="fm-ms" role="combobox" aria-expanded="false" aria-controls="fm-ms-list" aria-autocomplete="list" autocomplete="off" placeholder="Add labels"></div>
<ul class="af-listbox" id="fm-ms-list" role="listbox" aria-labelledby="fm-ms-l" aria-multiselectable="true" hidden>
<li class="af-option" role="option" data-value="web">web</li><li class="af-option" role="option" data-value="backup">backup</li><li class="af-option" role="option" data-value="logs">logs</li><li class="af-option" role="option" data-value="media">media</li>
</ul></div></div>/* styles/components/forms.css · combobox */
/* Editable combobox with a listbox popup (WAI-ARIA APG "Combobox with listbox popup"). Focus stays on the input; the visually
* active option is exposed with aria-activedescendant and styled with [data-active]. */
.af-combobox { position: relative; }
.af-combobox__input { padding-inline-end: var(--space-10); }
.af-combobox__toggle { position: absolute; inset-block: 0; inset-inline-end: var(--space-1); margin-block: auto; }
.af-listbox {
position: absolute; z-index: var(--z-dropdown);
inset-inline: 0; inset-block-start: calc(100% + var(--space-1));
max-block-size: 16rem; overflow-y: auto; overscroll-behavior: contain;
margin: 0; padding: var(--space-1); list-style: none;
border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--popover-radius);
background: var(--color-surface-raised); box-shadow: var(--popover-shadow);
}
.af-option {
display: flex; align-items: center; gap: var(--space-2);
min-block-size: var(--menu-item-height); padding: var(--space-1-5) var(--space-3);
border-radius: var(--radius-sm); cursor: pointer;
font-size: var(--type-body-sm-size); line-height: 1.3; color: var(--color-text);
}
.af-option[data-active="true"], .af-option:hover { background: var(--color-surface-hover); }
.af-option[data-active="true"] { outline: var(--border-width-hairline) solid var(--color-border-strong); outline-offset: -1px; }
.af-option[aria-selected="true"] { background: var(--color-surface-selected); font-weight: var(--font-weight-medium); }
.af-option[aria-selected="true"]::after { content: ""; margin-inline-start: auto; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); background: var(--color-accent-text); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat; }
.af-option[aria-disabled="true"] { color: var(--color-disabled-text); cursor: not-allowed; }
.af-option__meta { margin-inline-start: auto; font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }
.af-listbox__empty { padding: var(--space-3); font-size: var(--type-body-sm-size); color: var(--color-muted-text); }
/* styles/components/forms.css · multi-select */
/* Chips inside the same recessed boundary as an input, followed by a combobox input. Backspace on an empty input removes the
* last chip; each chip has its own remove button with an accessible name. */
.af-multiselect { position: relative; }
.af-multiselect__control {
display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1-5);
min-block-size: var(--input-height-md); padding: var(--space-1) var(--space-2);
border: var(--border-width-hairline) solid var(--input-border); border-radius: var(--input-radius);
background: var(--input-bg); box-shadow: var(--input-shadow); cursor: text;
}
.af-multiselect__control:focus-within { border-color: var(--input-border-focus); outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: 0; }
.af-multiselect__control[aria-invalid="true"], .af-multiselect[data-invalid="true"] .af-multiselect__control { border-color: var(--input-border-invalid); }
.af-multiselect__input { flex: 1 1 8rem; min-inline-size: 6rem; block-size: calc(var(--input-height-md) - 2 * var(--space-1) - 2 * var(--border-width-hairline)); padding: 0 var(--space-1); border: 0; background: transparent; color: var(--color-text); outline: none; font-size: var(--type-body-size); }
.af-multiselect__input::placeholder { color: var(--input-placeholder); opacity: 1; }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-accent-text--color-border-strong--color-disabled-text--color-muted-text--color-surface-hover--color-surface-raised--color-surface-selected--color-text--font-weight-medium--icon-size-sm--menu-item-height--popover-radius--popover-shadow--radius-sm--space-1--space-1-5--space-10--space-2--space-3--type-body-sm-size--type-metadata-family--type-metadata-size--type-metadata-weight--z-dropdown--focus-ring-color--focus-ring-width--input-bg--input-border--input-border-focus--input-border-invalid--input-height-md--input-placeholder--input-radius--input-shadow--type-body-sizeFormat YYYY-MM-DD. Arrow keys move by day and week, Page Up and Page Down by month.
<div class="af-field"><label class="af-field__label" for="fm-dp">Expiry date</label>
<div class="af-datepicker" data-datepicker data-min="2026-01-01" data-max="2027-12-31">
<div class="af-input-group"><input class="af-input" id="fm-dp" inputmode="numeric" placeholder="YYYY-MM-DD" autocomplete="off" aria-describedby="fm-dp-h"><button type="button" class="af-button af-button--secondary af-button--icon af-button--md" data-datepicker-toggle aria-label="Choose date"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-calendar"/></svg></button></div>
</div><p class="af-field__hint" id="fm-dp-h">Format YYYY-MM-DD. Arrow keys move by day and week, Page Up and Page Down by month.</p></div>/* styles/components/forms.css · date-picker */
/* Text input (ISO yyyy-mm-dd, typed or picked) + a calendar button that opens a grid in a popover. Keyboard: arrows move by
* day/week, PageUp/PageDown by month, Shift+PageUp/Down by year, Home/End to the start/end of the week, Enter/Space selects,
* Escape closes and returns focus to the button. */
.af-datepicker { position: relative; }
.af-datepicker__popover { position: absolute; z-index: var(--z-popover); inset-block-start: calc(100% + var(--space-1)); inset-inline-start: 0; }
.af-calendar { inline-size: 18.5rem; padding: var(--space-3); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--popover-radius); background: var(--color-surface-raised); box-shadow: var(--popover-shadow); }
.af-calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-block-end: var(--space-2); }
.af-calendar__title { font: var(--font-weight-semibold) var(--type-body-sm-size)/1.2 var(--font-body); }
.af-calendar__grid { inline-size: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0; }
.af-calendar__grid th { padding-block: var(--space-1-5); font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); color: var(--color-muted-text); text-align: center; }
.af-calendar__grid td { padding: var(--space-0-5); text-align: center; }
.af-calendar__day {
inline-size: 100%; aspect-ratio: 1; display: inline-grid; place-items: center;
border: var(--border-width-hairline) solid transparent; border-radius: var(--radius-sm);
font: var(--font-weight-regular) var(--type-body-sm-size)/1 var(--font-body); font-variant-numeric: tabular-nums;
color: var(--color-text);
}
.af-calendar__day[data-outside="true"] { color: var(--color-subtle-text); }
.af-calendar__day[aria-current="date"] { border-color: var(--color-accent-solid); font-weight: var(--font-weight-semibold); }
.af-calendar__day[aria-selected="true"], .af-calendar__day[aria-pressed="true"] { background: var(--action-primary-bg); border-color: var(--action-primary-border); color: var(--action-primary-text); box-shadow: var(--shadow-raised-primary); }
.af-calendar__day:disabled { color: var(--color-disabled-text); text-decoration: line-through; cursor: not-allowed; }
@media (hover: hover) { .af-calendar__day:hover:not(:disabled):not([aria-selected="true"]):not([aria-pressed="true"]) { background: var(--color-surface-hover); border-color: var(--color-border-strong); } }Design tokens this component reads. Override a token, not the component.
--action-primary-bg--action-primary-border--action-primary-text--border-width-hairline--color-accent-solid--color-border-strong--color-disabled-text--color-muted-text--color-subtle-text--color-surface-hover--color-surface-raised--color-text--font-body--font-weight-medium--font-weight-regular--font-weight-semibold--popover-radius--popover-shadow--radius-sm--shadow-raised-primary--space-0-5--space-1--space-1-5--space-2--space-3--type-body-sm-size--type-caption-size--z-popoverThe dropzone is a labelled file input first, so it works without drag and drop. The queue below shows each file’s state in text and icon. The transport here is simulated: nothing leaves your browser, and a filename containing “fail” fails once so you can try Retry.
No uploads yet.
No uploads yet.
<div class="af-dropzone" data-dropzone data-queue="#fm-q" data-accept=".txt,.png,.zip,.json" data-max-size="5000000">
<input class="af-dropzone__input" id="fm-dz" type="file" multiple>
<label class="af-dropzone__label" for="fm-dz"><svg class="af-icon af-dropzone__icon" aria-hidden="true" focusable="false"><use href="#i-cloud-upload"/></svg><span class="af-dropzone__title">Drop files or <span class="af-dropzone__action">browse</span></span><span class="af-dropzone__hint">.txt, .png, .zip or .json up to 5 MB. Simulated upload.</span></label>
<div data-dropzone-message role="status" hidden></div>
</div>
<section class="af-queue" id="fm-q" data-upload-queue aria-label="Upload queue">
<div class="af-queue__head"><h4 class="af-queue__title">Uploads</h4><span data-queue-summary role="status"></span>
<span class="af-cluster"><button class="af-button af-button--ghost af-button--sm" type="button" data-queue-cancel-all>Cancel all</button><button class="af-button af-button--ghost af-button--sm" type="button" data-queue-clear>Clear finished</button></span></div>
<ul class="af-queue__list"></ul><p class="af-queue__empty">No uploads yet.</p>
</section>/* styles/components/forms.css · dropzone */
/* Drag-and-drop target. The real file input stays in the tab order (visually hidden); the whole zone is its label. */
.af-dropzone { position: relative; border: var(--border-width-thick) dashed var(--input-border); border-radius: var(--radius-md); background: var(--color-surface); transition: background-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard); }
.af-dropzone__input { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; overflow: hidden; }
.af-dropzone__label { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); padding: var(--space-8) var(--space-6); text-align: center; cursor: pointer; }
.af-dropzone__icon { color: var(--color-muted-text); inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); }
.af-dropzone__title { font: var(--type-component-title-weight) var(--type-component-title-size)/var(--type-component-title-line-height) var(--type-component-title-family); }
.af-dropzone__action { color: var(--color-link); text-decoration: underline; text-underline-offset: 0.2em; }
.af-dropzone__hint { font: var(--type-caption-weight) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family); color: var(--color-muted-text); }
.af-dropzone:has(.af-dropzone__input:focus-visible) { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); }
.af-dropzone[data-state="dragover"] { border-style: solid; border-color: var(--color-accent-solid); background: var(--color-accent-bg); }
.af-dropzone[data-state="dragover"] .af-dropzone__icon { color: var(--color-accent-text); }
.af-dropzone[data-state="error"] { border-color: var(--status-danger-solid); background: var(--status-danger-bg); }
.af-dropzone[data-state="disabled"], .af-dropzone:has(.af-dropzone__input:disabled) { border-color: var(--action-disabled-border); background: var(--color-surface-disabled); color: var(--color-disabled-text); }
.af-dropzone[data-state="disabled"] .af-dropzone__label { cursor: not-allowed; }
.af-dropzone--compact .af-dropzone__label { flex-direction: row; justify-content: center; padding: var(--space-4); }
/* styles/patterns/infrastructure.css · upload-queue */
/* A panel listing every upload in the current session. Header: overall summary + Cancel all / Clear completed. Rows are
* .af-upload-row. The summary text lives in a polite live region updated on state CHANGES (start, complete, fail), not on every
* progress tick. */
.af-queue { display: flex; flex-direction: column; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }
.af-queue__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); padding: var(--space-3) var(--space-4); border-block-end: 1px solid var(--color-border); background: var(--color-surface-inset); }
.af-queue__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); }
.af-queue__list { margin: 0; padding: 0; list-style: none; max-block-size: 22rem; overflow-y: auto; overscroll-behavior: contain; }
.af-queue__empty { padding: var(--space-6) var(--space-4); text-align: center; color: var(--color-muted-text); font-size: var(--type-body-sm-size); }Design tokens this component reads. Override a token, not the component.
--action-disabled-border--border-width-thick--color-accent-bg--color-accent-solid--color-accent-text--color-disabled-text--color-link--color-muted-text--color-surface--color-surface-disabled--ease-standard--focus-ring-color--focus-ring-offset--focus-ring-width--icon-size-lg--input-border--motion-fast--radius-md--space-2--space-4--space-6--space-8--status-danger-bg--status-danger-solid--type-caption-family--type-caption-line-height--type-caption-size--type-caption-weight--type-component-title-family--type-component-title-line-height--type-component-title-size--type-component-title-weight--border-width-hairline--card-border--card-radius--color-border--color-surface-inset--space-3--type-body-sm-sizeValidate on submit, then on blur once a field has been touched. On failure, an error summary receives focus and lists every problem as a link to its field; each field also shows its own message. Native constraints supply the rules, and data-error-* attributes supply the wording.
<form class="af-form" data-validate novalidate data-demo-submit="Saved (sample, nothing was sent)">
<div class="af-error-summary" data-error-summary hidden tabindex="-1"><h4 class="af-error-summary__title">There is a problem</h4><ul></ul></div>
<div class="af-field"><label class="af-field__label" for="fv-name">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fv-name" name="name" required pattern="[a-z0-9\-]{3,63}" aria-describedby="fv-name-h" data-error-value-missing="Enter a bucket name." data-error-pattern-mismatch="Use 3 to 63 lowercase letters, digits or hyphens."><p class="af-field__hint" id="fv-name-h">3 to 63 characters. Lowercase letters, digits and hyphens.</p></div>
<div class="af-field"><label class="af-field__label" for="fv-mail">Alert email <span class="af-field__required">required</span></label><input class="af-input" id="fv-mail" name="mail" type="email" required data-error-value-missing="Enter an email address." data-error-type-mismatch="Enter an email address like name@example.test."></div>
<label class="af-check"><input class="af-check__input" type="checkbox" required data-error-value-missing="Confirm that you understand."><span class="af-check__label">I understand the bucket name cannot be changed</span></label>
<div class="af-form__actions"><button class="af-button af-button--secondary af-button--md" type="reset">Reset</button><button class="af-button af-button--primary af-button--md" type="submit">Create bucket</button></div>
</form>/* styles/components/forms.css · form-layout */
.af-form { display: flex; flex-direction: column; gap: var(--space-6); }
.af-form__row { display: grid; gap: var(--space-4); grid-template-columns: minmax(0, 1fr); }
.af-form__actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-3); padding-block-start: var(--space-4); border-block-start: 1px solid var(--color-border); }
@media (min-width: 640px) { .af-form__row--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* styles/components/feedback.css · error-summary */
/* At the top of a form after a failed submit: lists every error with a link to its field. It receives focus when shown (tabindex
* -1) so screen-reader and keyboard users land on it. */
.af-error-summary { padding: var(--space-4); border: var(--border-width-thick) solid var(--status-danger-solid); border-radius: var(--card-radius); background: var(--status-danger-bg); }
.af-error-summary:focus-visible { outline-offset: var(--space-1); }
.af-error-summary__title { margin: 0 0 var(--space-2); font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); color: var(--status-danger-text); display: flex; align-items: center; gap: var(--space-2); }
.af-error-summary ul { margin: 0; padding-inline-start: var(--space-5); font-size: var(--type-body-sm-size); }
.af-error-summary a { color: var(--status-danger-text); font-weight: var(--font-weight-medium); }
/* styles/components/forms.css · field */
.af-field { display: flex; flex-direction: column; gap: var(--space-1-5); min-inline-size: 0; }
.af-field__label {
display: flex; align-items: baseline; gap: var(--space-1);
font: var(--type-label-weight) var(--type-label-size)/var(--type-label-line-height) var(--type-label-family);
color: var(--color-text);
}
.af-field__required { color: var(--status-danger-text); font-weight: var(--font-weight-semibold); }
.af-field__optional { color: var(--color-muted-text); font-weight: var(--font-weight-regular); }
.af-field__description, .af-field__hint {
margin: 0;
font: var(--type-caption-weight) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family);
color: var(--color-muted-text);
}
.af-field__error, .af-field__success {
display: flex; align-items: flex-start; gap: var(--space-1-5); margin: 0;
font: var(--font-weight-medium) var(--type-caption-size)/var(--type-caption-line-height) var(--type-caption-family);
}
.af-field__error { color: var(--status-danger-text); }
.af-field__success { color: var(--status-success-text); }
.af-field__error > .af-icon, .af-field__success > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); margin-block-start: 1px; }
.af-field__counter { margin-inline-start: auto; font-variant-numeric: tabular-nums; color: var(--color-muted-text); font-size: var(--type-caption-size); }
.af-field--inline { flex-direction: row; align-items: center; gap: var(--space-3); }Design tokens this component reads. Override a token, not the component.
--color-border--space-3--space-4--space-6--border-width-thick--card-radius--font-weight-medium--space-1--space-2--space-5--status-danger-bg--status-danger-solid--status-danger-text--type-body-sm-size--type-component-title-family--type-component-title-size--type-component-title-weight--color-muted-text--color-text--font-weight-regular--font-weight-semibold--icon-size-xs--space-1-5--status-success-text--type-caption-family--type-caption-line-height--type-caption-size--type-caption-weight--type-label-family--type-label-line-height--type-label-size--type-label-weightComponents
Data is the product, so these components are quiet and exact. Numbers are tabular, identifiers are monospaced and never truncated where a person may need to quote them, and status always has a shape and a word.
There is one card. It is flat, hairline-bordered and square-cornered to the system radius. Variants change the role, not the decoration. Reach for --elevated only for something that floats, never to make a card look nicer.
Flat with a hairline border.
Use for a self-contained group of content.
Recessed, for content that sits inside another surface.
An empty slot waiting to be filled, such as “Add a webhook”.
A filled tint and an accent border mark the choice, together with a visible “Selected” label.
SelectedThe whole card is one link. Nested buttons stay independently focusable.
<div class="af-card"><div class="af-card__header"><div><h4 class="af-card__title">Default card</h4><p class="af-card__subtitle">Flat with a hairline border.</p></div></div><div class="af-card__body"><p>Use for a self-contained group of content.</p></div><div class="af-card__footer"><button class="af-button af-button--secondary af-button--sm" type="button">Action</button></div></div>
<div class="af-card af-card--inset"><div class="af-card__body"><h4 class="af-card__title">Inset card</h4><p>Recessed, for content that sits inside another surface.</p></div></div>
<div class="af-card af-card--dashed"><div class="af-card__body"><h4 class="af-card__title">Dashed card</h4><p>An empty slot waiting to be filled, such as “Add a webhook”.</p></div></div>
<div class="af-card af-card--selected"><div class="af-card__body"><h4 class="af-card__title">Selected card</h4><p>A filled tint and an accent border mark the choice, together with a visible “Selected” label.</p><span class="af-badge af-badge--accent">Selected</span></div></div>
<div class="af-card af-card--interactive"><div class="af-card__body"><h4 class="af-card__title"><a class="af-card__link" href="#data-display">Interactive card</a></h4><p>The whole card is one link. Nested buttons stay independently focusable.</p></div></div>/* styles/components/surfaces.css · card */
.af-card {
--_pad: var(--card-padding-md);
position: relative;
display: flex; flex-direction: column;
min-inline-size: 0;
background: var(--color-surface);
border: var(--border-width-hairline) solid var(--card-border);
border-radius: var(--card-radius);
color: var(--color-text);
}
.af-card--sm { --_pad: var(--card-padding-sm); }
.af-card--lg { --_pad: var(--card-padding-lg); }
.af-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); padding: var(--_pad) var(--_pad) 0; }
.af-card__header--divided { padding-block-end: var(--_pad); border-block-end: 1px solid var(--color-border); }
.af-card__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/var(--type-component-title-line-height) var(--type-component-title-family); letter-spacing: var(--type-component-title-letter-spacing); }
.af-card__subtitle { margin: 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); color: var(--color-muted-text); }
.af-card__actions { display: flex; align-items: center; gap: var(--space-2); flex: none; margin-inline-start: auto; }
.af-card__body { flex: 1; padding: var(--_pad); display: flex; flex-direction: column; gap: var(--card-gap); min-inline-size: 0; }
.af-card__header + .af-card__body { padding-block-start: var(--space-3); }
.af-card__header--divided + .af-card__body { padding-block-start: var(--_pad); }
.af-card__body--flush { padding: 0; }
.af-card__footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--_pad); border-block-start: 1px solid var(--color-border); background: var(--color-surface-inset); border-end-start-radius: calc(var(--card-radius) - var(--border-width-hairline)); border-end-end-radius: calc(var(--card-radius) - var(--border-width-hairline)); }
/* Variants */
.af-card--inset { background: var(--color-surface-inset); border-color: transparent; box-shadow: var(--shadow-inset-well); }
.af-card--elevated { border-color: var(--color-border-strong); box-shadow: var(--popover-shadow); background: var(--color-surface-raised); }
.af-card--dashed { border-style: dashed; border-color: var(--color-border-strong); background: transparent; }
.af-card--selected { border-color: var(--color-accent-solid); background: var(--color-surface-selected); box-shadow: inset 0 0 0 var(--border-width-hairline) var(--color-accent-solid); }
/* Interactive card: the whole card is one target via a stretched link/button inside the title (.af-card__link). Only ONE
* interactive element per card carries the stretch; secondary actions sit above it with position: relative. */
.af-card--interactive { transition: border-color var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard); }
.af-card__link { color: inherit; text-decoration: none; }
.af-card__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.af-card__link:focus-visible { outline: none; }
.af-card--interactive:has(.af-card__link:focus-visible) { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); }
@media (hover: hover) { .af-card--interactive:hover { border-color: var(--card-border-hover); box-shadow: var(--shadow-card-hover); } }
.af-card--interactive:active { box-shadow: none; }
.af-card--interactive .af-card__actions, .af-card--interactive .af-button:not(.af-card__link) { position: relative; z-index: 1; }
/* styles/components/surfaces.css · surface */
.af-surface { background: var(--color-surface); border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-border-hover--card-gap--card-padding-lg--card-padding-md--card-padding-sm--card-radius--color-accent-solid--color-border--color-border-strong--color-muted-text--color-surface--color-surface-inset--color-surface-raised--color-surface-selected--color-text--ease-standard--focus-ring-color--focus-ring-offset--focus-ring-width--motion-fast--popover-shadow--shadow-card-hover--shadow-inset-well--space-2--space-3--type-body-sm-line-height--type-body-sm-size--type-component-title-family--type-component-title-letter-spacing--type-component-title-line-height--type-component-title-size--type-component-title-weightSample figure. Previous period: 1,155,800.
Sample figure. Decimal units: 1 GB = 1,000,000,000 B.
All sample objects are in sync.
Storage is above the 80% warning line.
<div class="af-card"><div class="af-card__body"><div class="af-metric"><span class="af-metric__label">Objects stored</span><span class="af-metric__value">1,204,331</span><span class="af-metric__delta" data-trend="up"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chart-line"/></svg>Up 4.2% <span class="af-visually-hidden">compared with the previous period</span></span><p class="af-metric__helper">Sample figure. Previous period: 1,155,800.</p></div></div></div>
<div class="af-card"><div class="af-card__body"><div class="af-metric"><span class="af-metric__label">Stored</span><span class="af-metric__value">62.4<span class="af-metric__unit">GB</span></span><span class="af-metric__delta" data-trend="down"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chart-line"/></svg>Down 1.1 GB</span><p class="af-metric__helper">Sample figure. Decimal units: 1 GB = 1,000,000,000 B.</p></div></div></div>
<div class="af-status-card" data-status="success"><svg class="af-icon af-status-card__icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg><div><h4 class="af-status-card__title">Replication healthy</h4><p class="af-status-card__text">All sample objects are in sync.</p><p class="af-status-card__meta">Sample status, not live</p></div></div>
<div class="af-status-card" data-status="warning"><svg class="af-icon af-status-card__icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg><div><h4 class="af-status-card__title">Approaching quota</h4><p class="af-status-card__text">Storage is above the 80% warning line.</p><p class="af-status-card__meta">Sample status, not live</p></div><div class="af-status-card__actions"><button class="af-button af-button--secondary af-button--sm" type="button">Review usage</button></div></div>/* styles/components/surfaces.css · metric-card */
/* A single comparable figure. Label (what), value (how much, tabular), unit (mono), optional delta and helper. The delta ALWAYS
* has an arrow icon and a word or sign — never colour alone — and states its comparison period. */
.af-metric { display: flex; flex-direction: column; gap: var(--space-1-5); }
.af-metric__label { display: flex; align-items: center; gap: var(--space-1-5); font: var(--type-label-weight) var(--type-label-size)/var(--type-label-line-height) var(--type-label-family); color: var(--color-muted-text); }
.af-metric__value { display: flex; align-items: baseline; gap: var(--space-1-5); font: var(--type-metric-weight) var(--type-metric-size)/var(--type-metric-line-height) var(--type-metric-family); letter-spacing: var(--type-metric-letter-spacing); font-variant-numeric: tabular-nums; color: var(--color-text); }
.af-metric__unit { font: var(--font-weight-medium) var(--type-body-sm-size)/1 var(--font-mono); color: var(--color-muted-text); letter-spacing: 0; }
.af-metric__delta { display: inline-flex; align-items: center; gap: var(--space-1); font: var(--font-weight-medium) var(--type-caption-size)/1.3 var(--font-body); font-variant-numeric: tabular-nums; color: var(--color-muted-text); }
.af-metric__delta[data-trend="up"] { color: var(--status-success-text); }
.af-metric__delta[data-trend="down"] { color: var(--status-danger-text); }
.af-metric__delta > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }
.af-metric__helper { margin: 0; font-size: var(--type-caption-size); line-height: var(--type-caption-line-height); color: var(--color-muted-text); }
.af-metric__spark { block-size: 2.5rem; inline-size: 100%; color: var(--color-chart-1); }
/* styles/components/surfaces.css · status-card */
/* Reports the state of one thing and what to do about it. Colour comes from data-status; the icon shape and the headline words
* carry the meaning independently of colour. */
.af-status-card { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-3) var(--space-4); align-items: start; padding: var(--card-padding-md); border: var(--border-width-hairline) solid var(--status-neutral-border); border-radius: var(--card-radius); background: var(--status-neutral-bg); color: var(--color-text); }
.af-status-card[data-status="success"] { background: var(--status-success-bg); border-color: var(--status-success-border); }
.af-status-card[data-status="warning"] { background: var(--status-warning-bg); border-color: var(--status-warning-border); }
.af-status-card[data-status="danger"] { background: var(--status-danger-bg); border-color: var(--status-danger-border); }
.af-status-card[data-status="info"] { background: var(--status-info-bg); border-color: var(--status-info-border); }
.af-status-card__icon { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); color: var(--status-neutral-solid); }
.af-status-card[data-status="success"] .af-status-card__icon { color: var(--status-success-solid); }
.af-status-card[data-status="warning"] .af-status-card__icon { color: var(--status-warning-solid); }
.af-status-card[data-status="danger"] .af-status-card__icon { color: var(--status-danger-solid); }
.af-status-card[data-status="info"] .af-status-card__icon { color: var(--status-info-solid); }
.af-status-card__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/var(--type-component-title-line-height) var(--type-component-title-family); }
.af-status-card__text { margin: var(--space-1) 0 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); color: var(--color-text); }
.af-status-card__meta { margin: var(--space-1-5) 0 0; font: var(--type-metadata-weight) var(--type-metadata-size)/1.4 var(--type-metadata-family); color: var(--color-muted-text); }
.af-status-card__actions { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: var(--space-2); }
@media (min-width: 640px) { .af-status-card__actions { grid-column: 3; grid-row: 1; justify-content: flex-end; } }Design tokens this component reads. Override a token, not the component.
--color-chart-1--color-muted-text--color-text--font-body--font-mono--font-weight-medium--icon-size-xs--space-1--space-1-5--status-danger-text--status-success-text--type-body-sm-size--type-caption-line-height--type-caption-size--type-label-family--type-label-line-height--type-label-size--type-label-weight--type-metric-family--type-metric-letter-spacing--type-metric-line-height--type-metric-size--type-metric-weight--border-width-hairline--card-padding-md--card-radius--icon-size-lg--space-2--space-3--space-4--status-danger-bg--status-danger-border--status-danger-solid--status-info-bg--status-info-border--status-info-solid--status-neutral-bg--status-neutral-border--status-neutral-solid--status-success-bg--status-success-border--status-success-solid--status-warning-bg--status-warning-border--status-warning-solid--type-body-sm-line-height--type-component-title-family--type-component-title-line-height--type-component-title-size--type-component-title-weight--type-metadata-family--type-metadata-size--type-metadata-weightKeys are shown once
Copy the secret when you create the key. You cannot view it again.
Details live here. The disclosure is a native <details> element.
<div class="af-panel af-panel--info"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div><p class="af-panel__title">Keys are shown once</p><p>Copy the secret when you create the key. You cannot view it again.</p></div></div>
<details class="af-collapsible"><summary>Advanced settings<span class="af-collapsible__hint">3 options</span></summary><div class="af-collapsible__body"><p>Details live here. The disclosure is a native <code><details></code> element.</p></div></details>
<div class="af-card af-card--dashed"><div class="af-empty"><span class="af-empty__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-database"/></svg></span><h4 class="af-empty__title">No buckets yet</h4><p class="af-empty__text">A bucket holds your objects. Create one to start uploading.</p><div class="af-empty__actions"><button class="af-button af-button--primary af-button--md" type="button">Create bucket</button><a class="af-button af-button--ghost af-button--md" href="#data-display">Read the guide</a></div></div></div>/* styles/components/surfaces.css · panel */
/* Information panel: quiet grouped content that is not a card in a grid — an explanation, a configuration summary, a note.
* Tones are for meaning (info/success/warning/danger), never decoration. */
.af-panel { display: flex; gap: var(--space-3); padding: var(--space-4); border: var(--border-width-hairline) solid var(--color-border); border-radius: var(--card-radius); background: var(--color-surface-inset); font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); }
.af-panel > .af-icon { margin-block-start: 2px; color: var(--color-muted-text); }
.af-panel__title { margin: 0 0 var(--space-1); font: var(--font-weight-semibold) var(--type-body-sm-size)/1.35 var(--font-body); }
.af-panel p { margin: 0; color: var(--color-muted-text); }
.af-panel--info { background: var(--status-info-bg); border-color: var(--status-info-border); }
.af-panel--info > .af-icon { color: var(--status-info-solid); }
.af-panel--info p, .af-panel--info .af-panel__title { color: var(--color-text); }
/* styles/components/surfaces.css · collapsible */
/* Native <details>/<summary>: keyboard, state and announcement are built in. Use for optional or advanced configuration
* (progressive complexity), not to hide information a user needs to complete the task. */
.af-collapsible { border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); }
.af-collapsible > summary { display: flex; align-items: center; gap: var(--space-3); min-block-size: var(--button-height-lg); padding: var(--space-2) var(--space-4); list-style: none; border-radius: inherit; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); }
.af-collapsible > summary::-webkit-details-marker { display: none; }
.af-collapsible > summary::after { content: ""; margin-inline-start: auto; flex: none; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); background: currentColor; color: var(--color-muted-text); -webkit-mask: var(--_chev) center / contain no-repeat; mask: var(--_chev) center / contain no-repeat; --_chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); transition: transform var(--motion-standard) var(--ease-standard); }
.af-collapsible[open] > summary::after { transform: rotate(180deg); }
.af-collapsible[open] > summary { border-end-start-radius: 0; border-end-end-radius: 0; border-block-end: 1px solid var(--color-border); }
.af-collapsible > summary:focus-visible { outline-offset: var(--focus-ring-offset-inset); }
@media (hover: hover) { .af-collapsible > summary:hover { background: var(--color-surface-hover); } }
.af-collapsible__body { padding: var(--space-4); }
.af-collapsible__hint { font: var(--font-weight-regular) var(--type-body-sm-size)/1 var(--font-body); color: var(--color-muted-text); }
/* styles/components/surfaces.css · empty-state */
/* Says what is missing, why, and the next action. Never a bare "No data". */
.af-empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); max-inline-size: 28rem; margin-inline: auto; padding: var(--space-10) var(--space-6); text-align: center; }
.af-empty__icon { display: grid; place-items: center; inline-size: var(--avatar-size-lg); block-size: var(--avatar-size-lg); margin-block-end: var(--space-2); border: var(--border-width-hairline) dashed var(--color-border-control); border-radius: var(--radius-md); color: var(--color-muted-text); }
.af-empty__title { margin: 0; font: var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-line-height) var(--type-h4-family); letter-spacing: var(--type-h4-letter-spacing); }
.af-empty__text { margin: 0; color: var(--color-muted-text); font-size: var(--type-body-sm-size); line-height: var(--type-body-line-height); }
.af-empty__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin-block-start: var(--space-3); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-radius--color-border--color-muted-text--color-surface-inset--color-text--font-body--font-weight-semibold--space-1--space-3--space-4--status-info-bg--status-info-border--status-info-solid--type-body-sm-line-height--type-body-sm-size--button-height-lg--card-border--color-surface--color-surface-hover--ease-standard--focus-ring-offset-inset--font-weight-regular--icon-size-sm--motion-standard--space-2--type-component-title-family--type-component-title-size--type-component-title-weight--avatar-size-lg--color-border-control--radius-md--space-10--space-6--type-body-line-height--type-h4-family--type-h4-letter-spacing--type-h4-line-height--type-h4-size--type-h4-weightTables are the workhorse. Selection, sorting, sticky headers and a stacked small-screen layout are all built in. Sorting state is exposed as aria-sort, and a bulk bar appears when rows are selected.
| Status | ||||
|---|---|---|---|---|
| backup.zip | 48.2 MB | 2026-10-30 | Stored | |
| logo.png | 18.4 kB | 2026-11-02 | Stored | |
| report.json | 950 B | 2026-11-04 | Processing |
| Status | ||||
|---|---|---|---|---|
| backup.zip | 48.2 MB | 2026-10-30 | Stored | |
| logo.png | 18.4 kB | 2026-11-02 | Stored | |
| report.json | 950 B | 2026-11-04 | Processing |
<div class="af-table-bulkbar" data-bulkbar hidden role="region" aria-label="Bulk actions"><span data-bulk-count role="status"></span><span class="af-cluster af-cluster--2"><button class="af-button af-button--secondary af-button--sm" type="button">Download</button><button class="af-button af-button--destructive af-button--sm" type="button">Delete</button></span></div>
<div class="af-table-wrap" role="region" aria-label="Sample objects" tabindex="0">
<table class="af-table" data-table-select>
<caption class="af-visually-hidden">Sample objects. Sortable by name, size and modified date.</caption>
<thead><tr>
<th scope="col" class="af-table__select"><input class="af-check__input" type="checkbox" data-select-all aria-label="Select all rows"></th>
<th scope="col" aria-sort="ascending"><button class="af-table__sort" type="button">Name</button></th>
<th scope="col" aria-sort="none" class="af-table__num"><button class="af-table__sort" type="button">Size</button></th>
<th scope="col" aria-sort="none"><button class="af-table__sort" type="button">Modified</button></th>
<th scope="col">Status</th></tr></thead>
<tbody>
<tr><td class="af-table__select"><input class="af-check__input" type="checkbox" data-select-row aria-label="Select backup.zip"></td><th scope="row" class="af-table__name">backup.zip</th><td class="af-table__num" data-sort="48200000">48.2 MB</td><td data-sort="2026-10-30">2026-10-30</td><td><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Stored</span></td></tr>
<tr><td class="af-table__select"><input class="af-check__input" type="checkbox" data-select-row aria-label="Select logo.png"></td><th scope="row" class="af-table__name">logo.png</th><td class="af-table__num" data-sort="18400">18.4 kB</td><td data-sort="2026-11-02">2026-11-02</td><td><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Stored</span></td></tr>
<tr><td class="af-table__select"><input class="af-check__input" type="checkbox" data-select-row aria-label="Select report.json"></td><th scope="row" class="af-table__name">report.json</th><td class="af-table__num" data-sort="950">950 B</td><td data-sort="2026-11-04">2026-11-04</td><td><span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Processing</span></td></tr>
</tbody>
</table>
</div>/* styles/components/data-display.css · table */
/* A real <table>. The scroll wrapper is focusable (role="region" + aria-label + tabindex="0") so keyboard users can scroll it.
* Density: default 48px · --compact 40px · --dense 32px rows. Numeric columns: .af-table__num (right-aligned, tabular).
* Sorting: <th aria-sort> with a <button class="af-table__sort"> inside. Selection: a checkbox column; selected rows carry
* aria-selected="true" and are announced by the checkbox, not by colour. */
.af-table-wrap { position: relative; overflow-x: auto; border: var(--border-width-hairline) solid var(--table-border); border-radius: var(--card-radius); background: var(--color-surface); }
.af-table-wrap:focus-visible { outline-offset: 0; }
.af-table { inline-size: 100%; min-inline-size: var(--table-min, 40rem); border-collapse: separate; border-spacing: 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); text-align: start; --_row: var(--table-row-height-comfortable); }
.af-table--fit { min-inline-size: 0; }
.af-table--compact { --_row: var(--table-row-height-compact); }
.af-table--dense { --_row: var(--table-row-height-dense); font-size: var(--type-caption-size); }
.af-table caption { padding: var(--space-3) var(--table-cell-padding-x); text-align: start; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); caption-side: top; }
.af-table th, .af-table td { block-size: var(--_row); padding: 0 var(--table-cell-padding-x); border-block-end: 1px solid var(--table-border); text-align: start; vertical-align: middle; }
.af-table tbody tr:last-child > :is(td, th) { border-block-end: 0; }
.af-table thead th {
block-size: var(--table-header-height); background: var(--color-surface-inset);
font: var(--type-label-weight) var(--type-caption-size)/1.2 var(--type-label-family); letter-spacing: 0.02em; color: var(--color-muted-text); white-space: nowrap;
}
.af-table--sticky thead th { position: sticky; inset-block-start: 0; z-index: 1; }
.af-table tbody th[scope="row"] { font-weight: var(--font-weight-medium); }
.af-table tbody tr { transition: background-color var(--motion-fast) var(--ease-standard); }
@media (hover: hover) { .af-table tbody tr:hover { background: var(--color-surface-hover); } }
.af-table tbody tr[aria-selected="true"] { background: var(--color-surface-selected); }
.af-table tbody tr[aria-selected="true"] > :first-child { box-shadow: inset 3px 0 0 var(--color-accent-solid); }
.af-table__num { text-align: end !important; font-variant-numeric: tabular-nums; white-space: nowrap; }
.af-table__mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); font-variant-ligatures: none; }
.af-table__select { inline-size: var(--space-12); padding-inline-end: 0; }
.af-table__actions { inline-size: 1%; white-space: nowrap; text-align: end !important; }
.af-table__primary { display: flex; align-items: center; gap: var(--space-3); min-inline-size: 0; }
.af-table__name { font-weight: var(--font-weight-medium); color: var(--color-text); overflow-wrap: anywhere; }
.af-table__sub { display: block; font: var(--type-caption-weight) var(--type-caption-size)/1.3 var(--type-caption-family); color: var(--color-muted-text); }
.af-table__sort { display: inline-flex; align-items: center; gap: var(--space-1); margin-inline: calc(-1 * var(--space-1)); padding: var(--space-0-5) var(--space-1); border-radius: var(--radius-sm); font: inherit; letter-spacing: inherit; color: inherit; }
.af-table__sort > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); opacity: 0.55; }
@media (hover: hover) { .af-table__sort:hover { color: var(--color-text); background: var(--color-surface-hover); } }
th[aria-sort="ascending"] .af-table__sort, th[aria-sort="descending"] .af-table__sort { color: var(--color-text); }
th[aria-sort="ascending"] .af-table__sort > .af-icon, th[aria-sort="descending"] .af-table__sort > .af-icon { opacity: 1; color: var(--color-accent-text); }
.af-table-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--table-cell-padding-x); border-block-start: 1px solid var(--table-border); background: var(--color-surface); }
.af-table-bulkbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--table-cell-padding-x); background: var(--color-accent-bg-strong); border-block-end: 1px solid var(--color-border-strong); font-size: var(--type-body-sm-size); font-weight: var(--font-weight-medium); }
/* Stacked (mobile): below 640px a table with .af-table--stack becomes a list of labelled cards. Every <td> needs data-label. */
@media (max-width: 639.98px) {
.af-table--stack { min-inline-size: 0; }
.af-table--stack thead { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
.af-table--stack, .af-table--stack tbody, .af-table--stack tr, .af-table--stack td, .af-table--stack th[scope="row"] { display: block; }
.af-table--stack tr { padding: var(--space-3) var(--table-cell-padding-x); border-block-end: 1px solid var(--table-border); }
.af-table--stack tbody tr:last-child { border-block-end: 0; }
.af-table--stack td, .af-table--stack th[scope="row"] { block-size: auto; padding: var(--space-1) 0; border: 0; display: flex; justify-content: space-between; gap: var(--space-4); text-align: end; }
.af-table--stack td::before { content: attr(data-label); flex: none; color: var(--color-muted-text); font-weight: var(--font-weight-medium); text-align: start; }
.af-table--stack td.af-table__select, .af-table--stack td.af-table__actions { justify-content: flex-end; inline-size: auto; }
.af-table--stack td.af-table__select::before, .af-table--stack td.af-table__actions::before { content: none; }
}Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-radius--color-accent-bg-strong--color-accent-solid--color-accent-text--color-border-strong--color-muted-text--color-surface--color-surface-hover--color-surface-inset--color-surface-selected--color-text--ease-standard--font-mono--font-weight-medium--icon-size-xs--motion-fast--radius-sm--space-0-5--space-1--space-12--space-2--space-3--space-4--table-border--table-cell-padding-x--table-header-height--table-min--table-row-height-comfortable--table-row-height-compact--table-row-height-dense--type-body-sm-line-height--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--type-component-title-family--type-component-title-size--type-component-title-weight--type-label-family--type-label-weight--type-metadata-sizeEach status has its own shape: ● success, ▲ warning, ◆ danger, ○ info, ▬ neutral, ◐ pending. Colour reinforces; it never carries the meaning alone. Environments differ by fill and border style as well as label.
<span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Healthy</span>
<span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Degraded</span>
<span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>Failed</span>
<span class="af-badge af-badge--info"><span class="af-pip" aria-hidden="true"></span>Syncing</span>
<span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Inactive</span>
<span class="af-badge af-badge--outline">Outline</span>
<span class="af-badge af-badge--mono">v1.0.0</span>
<span class="af-cluster af-cluster--1"><span class="af-pip" data-status="pending" aria-hidden="true"></span>Pending</span>
<span class="af-env" data-env="production"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-server"/></svg>Production</span>
<span class="af-env" data-env="staging"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-layers"/></svg>Staging</span>
<span class="af-env" data-env="development"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-flask-conical"/></svg>Development</span>/* styles/components/data-display.css · badge */
/* Compact status or metadata. Pills are reserved for exactly this (brand.md). A badge ALWAYS has text; a status badge also
* has a pip or icon of a distinct shape. Tones: neutral · accent · success · warning · danger · info. */
.af-badge {
--_bg: var(--status-neutral-bg); --_text: var(--status-neutral-text); --_border: var(--status-neutral-border); --_pip: var(--status-neutral-solid);
display: inline-flex; align-items: center; gap: var(--badge-gap);
block-size: var(--badge-height); padding-inline: var(--badge-padding-x);
border: var(--border-width-hairline) solid var(--_border); border-radius: var(--badge-radius);
background: var(--_bg); color: var(--_text);
font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); white-space: nowrap; vertical-align: middle;
}
.af-badge > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }
.af-badge--success { --_bg: var(--status-success-bg); --_text: var(--status-success-text); --_border: var(--status-success-border); --_pip: var(--status-success-solid); }
.af-badge--warning { --_bg: var(--status-warning-bg); --_text: var(--status-warning-text); --_border: var(--status-warning-border); --_pip: var(--status-warning-solid); }
.af-badge--danger { --_bg: var(--status-danger-bg); --_text: var(--status-danger-text); --_border: var(--status-danger-border); --_pip: var(--status-danger-solid); }
.af-badge--info { --_bg: var(--status-info-bg); --_text: var(--status-info-text); --_border: var(--status-info-border); --_pip: var(--status-info-solid); }
.af-badge--accent { --_bg: var(--color-accent-bg); --_text: var(--color-accent-text); --_border: var(--color-accent-bg-strong); --_pip: var(--color-accent-solid); }
.af-badge--outline { --_bg: transparent; --_border: var(--color-border-strong); --_text: var(--color-muted-text); }
.af-badge--mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); }
/* styles/components/data-display.css · status-pip */
/* The status indicator. Each status has its own SHAPE so it is readable without colour:
* success ● filled circle warning ▲ triangle danger ◆ diamond info ○ ring neutral ▬ bar pending ◐ half circle
* Pair it with a visible label (or an aria-label if the label is truly absent). */
.af-pip { --_c: var(--status-neutral-solid); display: inline-block; flex: none; inline-size: var(--pip-size); block-size: var(--pip-size); background: var(--_c); border-radius: 50%; }
.af-badge .af-pip { --_c: var(--_pip); }
/* shape + colour per status; inside a badge the tone class selects the same shape automatically */
.af-pip[data-status="success"], .af-badge--success .af-pip { --_c: var(--status-success-solid); }
.af-pip[data-status="warning"], .af-badge--warning .af-pip { --_c: var(--status-warning-solid); border-radius: 0; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.af-pip[data-status="danger"], .af-badge--danger .af-pip { --_c: var(--status-danger-solid); border-radius: 0; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.af-pip[data-status="info"], .af-badge--info .af-pip { --_c: var(--status-info-solid); background: transparent; box-shadow: inset 0 0 0 2px var(--_c); }
.af-pip[data-status="neutral"], .af-badge:not([class*="--"]) .af-pip { --_c: var(--status-neutral-solid); border-radius: var(--radius-full); block-size: calc(var(--pip-size) / 3); }
.af-pip[data-status="pending"] { --_c: var(--status-info-solid); background: linear-gradient(90deg, var(--_c) 50%, transparent 50%); box-shadow: inset 0 0 0 1.5px var(--_c); }
/* styles/components/data-display.css · env-badge */
/* Environment identity — distinguished by FILL, BORDER STYLE and LABEL, never colour alone:
* production solid dark fill · staging tinted fill, solid border · development outline, dashed border */
.af-env { display: inline-flex; align-items: center; gap: var(--space-1-5); block-size: var(--badge-height); padding-inline: var(--badge-padding-x); border: var(--border-width-hairline) solid var(--env-development-border); border-radius: var(--radius-sm); background: var(--env-development-bg); color: var(--env-development-text); font: var(--font-weight-semibold) var(--type-eyebrow-size)/1 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; white-space: nowrap; border-style: dashed; }
.af-env[data-env="production"] { background: var(--env-production-bg); color: var(--env-production-text); border-color: var(--env-production-border); border-style: solid; }
.af-env[data-env="staging"] { background: var(--env-staging-bg); color: var(--env-staging-text); border-color: var(--env-staging-border); border-style: solid; }
.af-env > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }Design tokens this component reads. Override a token, not the component.
--badge-gap--badge-height--badge-padding-x--badge-radius--border-width-hairline--color-accent-bg--color-accent-bg-strong--color-accent-solid--color-accent-text--color-border-strong--color-muted-text--font-body--font-mono--font-weight-medium--icon-size-xs--status-danger-bg--status-danger-border--status-danger-solid--status-danger-text--status-info-bg--status-info-border--status-info-solid--status-info-text--status-neutral-bg--status-neutral-border--status-neutral-solid--status-neutral-text--status-success-bg--status-success-border--status-success-solid--status-success-text--status-warning-bg--status-warning-border--status-warning-solid--status-warning-text--type-caption-size--type-metadata-size--pip-size--radius-full--env-development-bg--env-development-border--env-development-text--env-production-bg--env-production-border--env-production-text--env-staging-bg--env-staging-border--env-staging-text--font-weight-semibold--radius-sm--space-1-5--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-size<span class="af-chip">backup<button class="af-chip__remove" type="button" aria-label="Remove backup"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></span>
<span class="af-chip af-chip--static">read-only</span>
<span class="af-avatar af-avatar--sm" aria-hidden="true">AK</span><span class="af-avatar" aria-hidden="true">AK</span><span class="af-avatar af-avatar--lg" aria-hidden="true">AK</span>/* styles/components/data-display.css · chip */
/* An applied filter, tag or selected value, optionally removable. Remove button needs aria-label="Remove <value>". */
.af-chip { display: inline-flex; align-items: center; gap: var(--space-1); block-size: calc(var(--button-height-xs) + var(--space-0-5)); padding-inline: var(--space-2) var(--space-0-5); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--radius-sm); background: var(--color-surface-raised); color: var(--color-text); font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); }
.af-chip--static { padding-inline: var(--space-2); }
.af-chip__remove { display: inline-grid; place-items: center; inline-size: var(--button-height-xs); block-size: var(--button-height-xs); border-radius: var(--radius-xs); color: var(--color-muted-text); }
.af-chip__remove > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }
@media (hover: hover) { .af-chip__remove:hover { background: var(--color-surface-hover); color: var(--color-text); } }
/* styles/components/data-display.css · avatar */
.af-avatar { display: inline-grid; place-items: center; flex: none; inline-size: var(--avatar-size-md); block-size: var(--avatar-size-md); border-radius: var(--radius-full); background: var(--color-accent-bg-strong); color: var(--color-text); font: var(--font-weight-semibold) var(--type-caption-size)/1 var(--font-body); text-transform: uppercase; overflow: hidden; }
.af-avatar--sm { inline-size: var(--avatar-size-sm); block-size: var(--avatar-size-sm); font-size: 0.6875rem; }
.af-avatar--lg { inline-size: var(--avatar-size-lg); block-size: var(--avatar-size-lg); font-size: var(--type-body-sm-size); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--button-height-xs--color-border-strong--color-muted-text--color-surface-hover--color-surface-raised--color-text--font-body--font-weight-medium--icon-size-xs--radius-sm--radius-xs--space-0-5--space-1--space-2--type-caption-size--avatar-size-lg--avatar-size-md--avatar-size-sm--color-accent-bg-strong--font-weight-semibold--radius-full--type-body-sm-size<ul class="af-list">
<li class="af-list__item"><svg class="af-icon af-list__lead" aria-hidden="true" focusable="false"><use href="#i-database"/></svg><div class="af-list__main"><span class="af-list__title">acme-assets-prod</span><span class="af-list__meta">Created 2026-09-12 (sample)</span></div><div class="af-list__trail"><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Active</span></div></li>
<li class="af-list__item"><svg class="af-icon af-list__lead" aria-hidden="true" focusable="false"><use href="#i-database"/></svg><div class="af-list__main"><span class="af-list__title">acme-logs</span><span class="af-list__meta">Created 2026-10-01 (sample)</span></div><div class="af-list__trail"><span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Inactive</span></div></li>
</ul>
<dl class="af-kv af-kv--ruled">
<div class="af-kv__item"><dt class="af-kv__term">Storage class</dt><dd class="af-kv__value">Standard</dd></div>
<div class="af-kv__item"><dt class="af-kv__term">Versioning</dt><dd class="af-kv__value">Enabled</dd></div>
<div class="af-kv__item"><dt class="af-kv__term">Bucket ID</dt><dd class="af-kv__value af-kv__mono">bkt_SAMPLE_0001</dd></div>
</dl>
<dl class="af-meta">
<div class="af-meta__row"><dt>Content type</dt><dd>image/png</dd></div>
<div class="af-meta__row"><dt>Size</dt><dd>18,400 B (18.4 kB)</dd></div>
<div class="af-meta__row"><dt>ETag</dt><dd>sample-etag-0000</dd></div>
</dl>/* styles/components/data-display.css · data-list */
/* A vertical list of records where a table is too heavy (mobile, side panels, short lists). */
.af-list { margin: 0; padding: 0; list-style: none; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); }
.af-list__item { display: flex; align-items: center; gap: var(--space-3); min-block-size: var(--table-row-height-comfortable); padding: var(--space-2) var(--space-4); }
.af-list__item + .af-list__item { border-block-start: 1px solid var(--color-border); }
.af-list__main { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; }
.af-list__title { font-weight: var(--font-weight-medium); font-size: var(--type-body-sm-size); line-height: 1.35; overflow-wrap: anywhere; }
.af-list__meta { font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-list__trail { flex: none; display: flex; align-items: center; gap: var(--space-2); }
.af-list__lead { flex: none; color: var(--color-muted-text); }
/* styles/components/data-display.css · key-value */
/* <dl> of terms and values. --inline puts the term beside the value (≥640px); default stacks them. Values that are
* identifiers use .af-kv__mono. A copy button may follow a value. */
.af-kv { display: grid; gap: var(--space-3) var(--space-6); margin: 0; grid-template-columns: minmax(0, 1fr); }
.af-kv__item { display: flex; flex-direction: column; gap: var(--space-0-5); min-inline-size: 0; }
.af-kv dt, .af-kv__term { font: var(--type-label-weight) var(--type-caption-size)/1.3 var(--type-label-family); color: var(--color-muted-text); }
.af-kv dd, .af-kv__value { margin: 0; font-size: var(--type-body-sm-size); line-height: 1.4; color: var(--color-text); overflow-wrap: anywhere; display: flex; align-items: center; gap: var(--space-2); }
.af-kv__mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); font-variant-ligatures: none; }
@media (min-width: 640px) {
.af-kv--inline { grid-template-columns: minmax(8rem, max-content) minmax(0, 1fr); gap: var(--space-2) var(--space-6); }
.af-kv--inline .af-kv__item { display: contents; }
.af-kv--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.af-kv--ruled .af-kv__item { padding-block: var(--space-2); border-block-end: 1px solid var(--color-border); }
.af-dl { margin: 0; }
.af-dl dt { font-weight: var(--font-weight-semibold); }
.af-dl dd { margin: 0 0 var(--space-3); color: var(--color-muted-text); }
/* styles/components/data-display.css · metadata-viewer */
/* Object metadata as ruled key/value rows in monospace — content type, size, ETag, checksum, headers. */
.af-meta { margin: 0; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }
.af-meta__row { display: grid; grid-template-columns: minmax(6rem, 9rem) minmax(0, 1fr); gap: var(--space-3); align-items: baseline; padding: var(--space-2) var(--space-4); }
.af-meta__row + .af-meta__row { border-block-start: 1px solid var(--color-border); }
.af-meta dt { font: var(--type-label-weight) var(--type-caption-size)/1.4 var(--type-label-family); color: var(--color-muted-text); }
.af-meta dd { margin: 0; font: var(--type-metadata-weight) var(--type-metadata-size)/1.5 var(--type-metadata-family); font-variant-ligatures: none; overflow-wrap: anywhere; display: flex; align-items: flex-start; gap: var(--space-2); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-border--color-muted-text--color-surface--font-weight-medium--space-2--space-3--space-4--table-row-height-comfortable--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--color-text--font-mono--font-weight-semibold--space-0-5--space-6--type-label-family--type-label-weight--type-metadata-size--type-metadata-family--type-metadata-weightA progress bar is a task moving to completion. A meter is a quantity against a limit; it has a threshold marker, ruler ticks, and a word for its level. An estimate is hatched so it cannot be mistaken for a measurement.
<div class="af-stack af-stack--2"><label for="dd-p1">Uploading 3 of 8 files, 42%</label><progress class="af-progress" id="dd-p1" max="100" value="42"></progress></div>
<div class="af-stack af-stack--2"><label for="dd-p2">Processing</label><progress class="af-progress" id="dd-p2" max="100"></progress></div>
<div class="af-stack af-stack--2"><label for="dd-p3">Import complete</label><progress class="af-progress" id="dd-p3" max="100" value="100" data-status="success"></progress></div>/* styles/components/data-display.css · progress */
/* Native <progress>: determinate (value + max) or indeterminate (no value). Pair with text that says WHAT is progressing and
* how far ("Uploading 3 of 8 — 42%"). Tone via data-status. Indeterminate shows a moving segment; under reduced motion it is
* a static half bar — the accompanying text still says what is happening. */
.af-progress { appearance: none; -webkit-appearance: none; inline-size: 100%; block-size: var(--progress-height); border: 0; border-radius: var(--meter-radius); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); overflow: hidden; vertical-align: middle; --_c: var(--color-accent-solid); }
.af-progress::-webkit-progress-bar { background: transparent; }
.af-progress::-webkit-progress-value { background: var(--_c); border-radius: var(--meter-radius); transition: inline-size var(--motion-standard) var(--ease-standard); }
.af-progress::-moz-progress-bar { background: var(--_c); border-radius: var(--meter-radius); }
.af-progress[data-status="success"] { --_c: var(--status-success-solid); }
.af-progress[data-status="warning"] { --_c: var(--status-warning-solid); }
.af-progress[data-status="danger"] { --_c: var(--status-danger-solid); }
.af-progress:indeterminate { background: linear-gradient(90deg, transparent 0 25%, var(--_c) 25% 55%, transparent 55% 100%) var(--color-surface-inset); background-size: 200% 100%; animation: af-indeterminate 1.4s var(--ease-standard) infinite; }
.af-progress:indeterminate::-webkit-progress-value { background: transparent; }
.af-progress:indeterminate::-moz-progress-bar { background: transparent; }
@keyframes af-indeterminate { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .af-progress:indeterminate { animation: none; background: linear-gradient(90deg, var(--_c) 0 50%, transparent 50%) var(--color-surface-inset); } }Design tokens this component reads. Override a token, not the component.
--color-accent-solid--color-surface-inset--ease-standard--meter-radius--motion-standard--progress-height--shadow-inset-well--status-danger-solid--status-success-solid--status-warning-solid<div class="af-meter" data-level="ok"><div class="af-meter__head"><span class="af-meter__label" id="dd-m1">Storage used</span><span class="af-meter__figures"><strong>42 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="dd-m1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="42" aria-valuetext="42 GB of 100 GB used, 42%"><div class="af-meter__fill" style="--value: 42%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg>Within limit</span><span>Warns at 80%</span></div></div>
<div class="af-meter" data-level="warning"><div class="af-meter__head"><span class="af-meter__label" id="dd-m2">Storage used</span><span class="af-meter__figures"><strong>86 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="dd-m2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86 GB of 100 GB used, 86%"><div class="af-meter__fill" style="--value: 86%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg>Approaching limit</span><span>Warns at 80%</span></div></div>
<div class="af-meter" data-level="danger" data-kind="estimate"><div class="af-meter__head"><span class="af-meter__label" id="dd-m3">Egress this month</span><span class="af-meter__figures"><strong>104 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="dd-m3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-valuetext="104 GB of 100 GB, over the limit, estimated"><div class="af-meter__fill" style="--value: 100%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-octagon-alert"/></svg>Over limit (estimate)</span><span>Warns at 80%</span></div></div>/* styles/components/data-display.css · usage-meter */
/* How much of a limit is used. A recessed track with ruler ticks every 10%, a fill whose colour follows data-level, a
* threshold marker (where warnings begin) and a LABEL that says the level in words. role="meter" with aria-valuetext gives
* screen readers "62.4 GB of 100 GB used, 62%". Custom properties --value and --threshold are the ONLY inline styles allowed
* (data, not design): --value: 62.4%; --threshold: 80%.
* data-level: ok (accent) · warning (≥ threshold) · danger (≥ limit) data-kind="estimate" hatches the fill. */
.af-meter { --_fill: var(--color-accent-solid); display: flex; flex-direction: column; gap: var(--space-2); min-inline-size: 0; }
.af-meter[data-level="warning"] { --_fill: var(--status-warning-solid); }
.af-meter[data-level="danger"] { --_fill: var(--status-danger-solid); }
.af-meter__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-1) var(--space-3); }
.af-meter__label { font: var(--type-label-weight) var(--type-label-size)/1.3 var(--type-label-family); }
.af-meter__figures { font: var(--font-weight-regular) var(--type-body-sm-size)/1.3 var(--font-body); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
.af-meter__figures strong { color: var(--color-text); font-weight: var(--font-weight-semibold); }
.af-meter__track { position: relative; block-size: var(--meter-height); border-radius: var(--meter-radius); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); overflow: hidden; }
.af-meter--lg .af-meter__track { block-size: var(--meter-height-lg); }
.af-meter__fill { position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: var(--value, 0%); max-inline-size: 100%; min-inline-size: var(--meter-height); border-radius: var(--meter-radius); background: var(--_fill); transition: inline-size var(--motion-emphasis) var(--ease-standard); }
.af-meter[data-kind="estimate"] .af-meter__fill { background: repeating-linear-gradient(135deg, var(--_fill) 0 4px, color-mix(in srgb, var(--_fill) 55%, var(--color-surface-inset)) 4px 8px); }
.af-meter__track::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(90deg, transparent 0 calc(10% - 1px), var(--color-surface) calc(10% - 1px) 10%); opacity: 0.85; }
.af-meter__threshold { position: absolute; inset-block: -2px; inset-inline-start: var(--threshold, 80%); inline-size: 2px; background: var(--color-text); z-index: 1; border-radius: 1px; }
.af-meter__foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-1) var(--space-3); font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-meter__state { display: inline-flex; align-items: center; gap: var(--space-1-5); font-weight: var(--font-weight-medium); color: var(--color-muted-text); }
.af-meter[data-level="warning"] .af-meter__state { color: var(--status-warning-text); }
.af-meter[data-level="danger"] .af-meter__state { color: var(--status-danger-text); }
/* Stacked usage breakdown (parts of one whole). Series are told apart by colour AND by texture (see data-series), and by the
* legend that lists names and values. 2px surface gaps separate segments; no borders. */
.af-breakdown { display: flex; gap: 2px; block-size: var(--meter-height-lg); border-radius: var(--meter-radius); overflow: hidden; background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); }
.af-breakdown__seg { flex: var(--share, 1) 1 0; min-inline-size: 2px; background: var(--color-chart-1); }
.af-breakdown__seg[data-series="2"] { background: var(--color-chart-2); }
.af-breakdown__seg[data-series="3"] { background: var(--color-chart-3); }
.af-breakdown__seg[data-series="other"] { background: var(--color-chart-other); }
.af-breakdown__seg[data-series="2"], .af-breakdown__seg[data-series="3"], .af-breakdown__seg[data-series="other"] { background-image: repeating-linear-gradient(45deg, transparent 0 4px, color-mix(in srgb, var(--color-surface) 38%, transparent) 4px 6px); }
.af-breakdown__seg[data-series="3"] { background-image: repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--color-surface) 38%, transparent) 4px 6px); }Design tokens this component reads. Override a token, not the component.
--color-accent-solid--color-chart-1--color-chart-2--color-chart-3--color-chart-other--color-muted-text--color-surface--color-surface-inset--color-text--ease-standard--font-body--font-weight-medium--font-weight-regular--font-weight-semibold--meter-height--meter-height-lg--meter-radius--motion-emphasis--shadow-inset-well--share--space-1--space-1-5--space-2--space-3--status-danger-solid--status-danger-text--status-warning-solid--status-warning-text--threshold--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--type-label-family--type-label-size--type-label-weight--value<div class="af-breakdown" role="img" aria-label="Storage by class: Standard 62.4 GB, Infrequent 18.1 GB, Archive 9.5 GB"><span class="af-breakdown__seg" data-series="1" style="--share: 62.4"></span><span class="af-breakdown__seg" data-series="2" style="--share: 18.1"></span><span class="af-breakdown__seg" data-series="3" style="--share: 9.5"></span></div>
<ul class="af-legend"><li class="af-legend__item"><span class="af-legend__swatch" data-series="1"></span>Standard <span class="af-legend__value">62.4 GB</span></li><li class="af-legend__item"><span class="af-legend__swatch" data-series="2"></span>Infrequent <span class="af-legend__value">18.1 GB</span></li><li class="af-legend__item"><span class="af-legend__swatch" data-series="3"></span>Archive <span class="af-legend__value">9.5 GB</span></li></ul>/* styles/components/data-display.css · legend */
.af-legend { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); margin: 0; padding: 0; list-style: none; font: var(--font-weight-regular) var(--type-caption-size)/1.4 var(--font-body); color: var(--color-muted-text); }
.af-legend__item { display: inline-flex; align-items: center; gap: var(--space-1-5); }
.af-legend__swatch { inline-size: var(--space-3); block-size: var(--space-3); flex: none; border-radius: var(--radius-xs); background: var(--color-chart-1); }
.af-legend__swatch[data-series="2"] { background: var(--color-chart-2); }
.af-legend__swatch[data-series="3"] { background: var(--color-chart-3); }
.af-legend__swatch[data-series="other"] { background: var(--color-chart-other); }
.af-chart[data-texture] .af-legend__swatch[data-series="2"], .af-chart[data-texture] .af-legend__swatch[data-series="other"], .af-legend[data-texture] .af-legend__swatch[data-series="2"], .af-legend[data-texture] .af-legend__swatch[data-series="other"] { background-image: repeating-linear-gradient(45deg, transparent 0 3px, color-mix(in srgb, var(--color-surface) 38%, transparent) 3px 5px); }
.af-chart[data-texture] .af-legend__swatch[data-series="3"], .af-legend[data-texture] .af-legend__swatch[data-series="3"] { background-image: repeating-linear-gradient(135deg, transparent 0 3px, color-mix(in srgb, var(--color-surface) 38%, transparent) 3px 5px); }
.af-legend__value { color: var(--color-text); font-variant-numeric: tabular-nums; font-weight: var(--font-weight-medium); }
.af-legend button.af-legend__item { border-radius: var(--radius-sm); padding: var(--space-0-5) var(--space-1); }
.af-legend button.af-legend__item[aria-pressed="false"] { opacity: 0.45; text-decoration: line-through; }
/* styles/components/data-display.css · usage-meter */
/* How much of a limit is used. A recessed track with ruler ticks every 10%, a fill whose colour follows data-level, a
* threshold marker (where warnings begin) and a LABEL that says the level in words. role="meter" with aria-valuetext gives
* screen readers "62.4 GB of 100 GB used, 62%". Custom properties --value and --threshold are the ONLY inline styles allowed
* (data, not design): --value: 62.4%; --threshold: 80%.
* data-level: ok (accent) · warning (≥ threshold) · danger (≥ limit) data-kind="estimate" hatches the fill. */
.af-meter { --_fill: var(--color-accent-solid); display: flex; flex-direction: column; gap: var(--space-2); min-inline-size: 0; }
.af-meter[data-level="warning"] { --_fill: var(--status-warning-solid); }
.af-meter[data-level="danger"] { --_fill: var(--status-danger-solid); }
.af-meter__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-1) var(--space-3); }
.af-meter__label { font: var(--type-label-weight) var(--type-label-size)/1.3 var(--type-label-family); }
.af-meter__figures { font: var(--font-weight-regular) var(--type-body-sm-size)/1.3 var(--font-body); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
.af-meter__figures strong { color: var(--color-text); font-weight: var(--font-weight-semibold); }
.af-meter__track { position: relative; block-size: var(--meter-height); border-radius: var(--meter-radius); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); overflow: hidden; }
.af-meter--lg .af-meter__track { block-size: var(--meter-height-lg); }
.af-meter__fill { position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: var(--value, 0%); max-inline-size: 100%; min-inline-size: var(--meter-height); border-radius: var(--meter-radius); background: var(--_fill); transition: inline-size var(--motion-emphasis) var(--ease-standard); }
.af-meter[data-kind="estimate"] .af-meter__fill { background: repeating-linear-gradient(135deg, var(--_fill) 0 4px, color-mix(in srgb, var(--_fill) 55%, var(--color-surface-inset)) 4px 8px); }
.af-meter__track::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(90deg, transparent 0 calc(10% - 1px), var(--color-surface) calc(10% - 1px) 10%); opacity: 0.85; }
.af-meter__threshold { position: absolute; inset-block: -2px; inset-inline-start: var(--threshold, 80%); inline-size: 2px; background: var(--color-text); z-index: 1; border-radius: 1px; }
.af-meter__foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-1) var(--space-3); font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-meter__state { display: inline-flex; align-items: center; gap: var(--space-1-5); font-weight: var(--font-weight-medium); color: var(--color-muted-text); }
.af-meter[data-level="warning"] .af-meter__state { color: var(--status-warning-text); }
.af-meter[data-level="danger"] .af-meter__state { color: var(--status-danger-text); }
/* Stacked usage breakdown (parts of one whole). Series are told apart by colour AND by texture (see data-series), and by the
* legend that lists names and values. 2px surface gaps separate segments; no borders. */
.af-breakdown { display: flex; gap: 2px; block-size: var(--meter-height-lg); border-radius: var(--meter-radius); overflow: hidden; background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); }
.af-breakdown__seg { flex: var(--share, 1) 1 0; min-inline-size: 2px; background: var(--color-chart-1); }
.af-breakdown__seg[data-series="2"] { background: var(--color-chart-2); }
.af-breakdown__seg[data-series="3"] { background: var(--color-chart-3); }
.af-breakdown__seg[data-series="other"] { background: var(--color-chart-other); }
.af-breakdown__seg[data-series="2"], .af-breakdown__seg[data-series="3"], .af-breakdown__seg[data-series="other"] { background-image: repeating-linear-gradient(45deg, transparent 0 4px, color-mix(in srgb, var(--color-surface) 38%, transparent) 4px 6px); }
.af-breakdown__seg[data-series="3"] { background-image: repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--color-surface) 38%, transparent) 4px 6px); }Design tokens this component reads. Override a token, not the component.
--color-chart-1--color-chart-2--color-chart-3--color-chart-other--color-muted-text--color-surface--color-text--font-body--font-weight-medium--font-weight-regular--radius-sm--radius-xs--space-0-5--space-1--space-1-5--space-3--space-4--type-caption-size--color-accent-solid--color-surface-inset--ease-standard--font-weight-semibold--meter-height--meter-height-lg--meter-radius--motion-emphasis--shadow-inset-well--share--space-2--status-danger-solid--status-danger-text--status-warning-solid--status-warning-text--threshold--type-body-sm-size--type-caption-family--type-caption-weight--type-label-family--type-label-size--type-label-weight--valueCharts are built from a data spec. Lines are 2 px, bars are at most 24 px wide with a rounded data end, grid lines are hairlines, and series are told apart by colour and by texture or marker. Every chart has a data table twin (“Show data”), keyboard arrows to step through points, and a tooltip on hover. Three series is the ceiling; the rest become “Other”.
<figure data-chart='{"type":"line","title":"Requests per hour","subtitle":"Last 8 hours. Sample data","categories":["00:00","01:00","02:00","03:00","04:00","05:00","06:00","07:00"],"series":[{"name":"Reads","values":[120,180,150,220,260,240,300,280]},{"name":"Writes","values":[40,60,55,90,80,110,95,120]}],"format":"number"}'></figure>/* styles/components/data-display.css · chart */
/* Chart container: a <figure> that owns title, units/period/aggregation, legend, the plot, and a table-view twin for
* accessibility. SVG text uses text tokens, never the series colour. Marks: 2px lines, ≤24px bars with a 4px rounded data end,
* ≥8px end-dots with a 2px surface ring, areas at ~10% opacity, hairline solid gridlines. Series colours are
* --color-chart-1..3 (+ -other), validated adjacent-pair for colour-vision deficiency in both themes; max 3 series.
* Texture (data-texture on the figure, print, forced-colors) adds hatching as a second channel. */
.af-chart { margin: 0; display: flex; flex-direction: column; gap: var(--space-3); min-inline-size: 0; }
.af-chart__head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--space-2) var(--space-4); }
.af-chart__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); }
.af-chart__subtitle { margin: var(--space-0-5) 0 0; font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-chart__plot { position: relative; inline-size: 100%; min-block-size: 11rem; }
.af-chart__svg { inline-size: 100%; block-size: auto; overflow: visible; }
.af-chart__grid { stroke: var(--color-chart-grid); stroke-width: 1; fill: none; shape-rendering: crispEdges; }
.af-chart__axis { stroke: var(--color-border-strong); stroke-width: 1; shape-rendering: crispEdges; }
.af-chart__tick { fill: var(--color-muted-text); font: var(--font-weight-regular) 11px/1 var(--font-body); font-variant-numeric: tabular-nums; }
.af-chart__label { fill: var(--color-text); font: var(--font-weight-medium) 11px/1 var(--font-body); font-variant-numeric: tabular-nums; }
.af-chart__line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.af-chart__area { stroke: none; opacity: 0.1; }
.af-chart__dot { stroke: var(--color-surface); stroke-width: 2; }
.af-chart__bar { stroke: none; }
.af-chart [data-series="1"] { --_s: var(--color-chart-1); }
.af-chart [data-series="2"] { --_s: var(--color-chart-2); }
.af-chart [data-series="3"] { --_s: var(--color-chart-3); }
.af-chart [data-series="other"] { --_s: var(--color-chart-other); }
.af-chart__line[data-series], .af-chart__area[data-series], .af-chart__bar[data-series], .af-chart__dot[data-series] { }
.af-chart__line[data-series] { stroke: var(--_s); }
.af-chart__area[data-series], .af-chart__bar[data-series], .af-chart__dot[data-series] { fill: var(--_s); }
.af-chart__crosshair { stroke: var(--color-border-control); stroke-width: 1; shape-rendering: crispEdges; pointer-events: none; }
.af-chart__hit { fill: transparent; }
.af-chart__hit:focus-visible { outline: none; }
.af-chart__hit:focus-visible + .af-chart__bar, .af-chart__bar[data-hover="true"] { filter: brightness(0.9); }
.af-chart__tip { position: absolute; z-index: var(--z-tooltip); min-inline-size: 9rem; padding: var(--space-2) var(--space-3); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--radius-sm); background: var(--color-surface-raised); box-shadow: var(--popover-shadow); font: var(--font-weight-regular) var(--type-caption-size)/1.5 var(--font-body); color: var(--color-text); pointer-events: none; transform: translate(-50%, calc(-100% - 12px)); white-space: nowrap; }
.af-chart__tip-title { display: block; font-weight: var(--font-weight-semibold); margin-block-end: var(--space-1); }
.af-chart__tip-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); font-variant-numeric: tabular-nums; }
.af-chart__tip-row > span:first-child { display: inline-flex; align-items: center; gap: var(--space-1-5); color: var(--color-muted-text); }
.af-chart__note { margin: 0; font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-chart__table { margin-block-start: var(--space-2); }
.af-chart[data-texture] .af-chart__bar[data-series="2"], .af-chart[data-texture] .af-chart__area[data-series="2"] { fill: url(#af-hatch-45); }
.af-chart[data-texture] .af-chart__bar[data-series="3"], .af-chart[data-texture] .af-chart__area[data-series="3"] { fill: url(#af-hatch-135); }
@media (forced-colors: active) {
.af-chart__line, .af-chart__bar, .af-chart__dot, .af-chart__area { stroke: CanvasText; fill: CanvasText; }
.af-chart__line { fill: none; }
}
@media print { .af-chart__bar[data-series="2"] { fill: url(#af-hatch-45); } .af-chart__bar[data-series="3"] { fill: url(#af-hatch-135); } }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-border-control--color-border-strong--color-chart-1--color-chart-2--color-chart-3--color-chart-grid--color-chart-other--color-muted-text--color-surface--color-surface-raised--color-text--font-body--font-weight-medium--font-weight-regular--font-weight-semibold--popover-shadow--radius-sm--space-0-5--space-1--space-1-5--space-2--space-3--space-4--type-caption-family--type-caption-size--type-caption-weight--type-component-title-family--type-component-title-size--type-component-title-weight--z-tooltip<figure data-chart='{"type":"bar","title":"Stored by class","subtitle":"Sample data","categories":["Mon","Tue","Wed","Thu"],"series":[{"name":"Standard","values":[1200000,2200000,1800000,2600000]},{"name":"Archive","values":[400000,600000,900000,700000]}],"format":"bytes","texture":true}'></figure>/* styles/components/data-display.css · chart */
/* Chart container: a <figure> that owns title, units/period/aggregation, legend, the plot, and a table-view twin for
* accessibility. SVG text uses text tokens, never the series colour. Marks: 2px lines, ≤24px bars with a 4px rounded data end,
* ≥8px end-dots with a 2px surface ring, areas at ~10% opacity, hairline solid gridlines. Series colours are
* --color-chart-1..3 (+ -other), validated adjacent-pair for colour-vision deficiency in both themes; max 3 series.
* Texture (data-texture on the figure, print, forced-colors) adds hatching as a second channel. */
.af-chart { margin: 0; display: flex; flex-direction: column; gap: var(--space-3); min-inline-size: 0; }
.af-chart__head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--space-2) var(--space-4); }
.af-chart__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); }
.af-chart__subtitle { margin: var(--space-0-5) 0 0; font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-chart__plot { position: relative; inline-size: 100%; min-block-size: 11rem; }
.af-chart__svg { inline-size: 100%; block-size: auto; overflow: visible; }
.af-chart__grid { stroke: var(--color-chart-grid); stroke-width: 1; fill: none; shape-rendering: crispEdges; }
.af-chart__axis { stroke: var(--color-border-strong); stroke-width: 1; shape-rendering: crispEdges; }
.af-chart__tick { fill: var(--color-muted-text); font: var(--font-weight-regular) 11px/1 var(--font-body); font-variant-numeric: tabular-nums; }
.af-chart__label { fill: var(--color-text); font: var(--font-weight-medium) 11px/1 var(--font-body); font-variant-numeric: tabular-nums; }
.af-chart__line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.af-chart__area { stroke: none; opacity: 0.1; }
.af-chart__dot { stroke: var(--color-surface); stroke-width: 2; }
.af-chart__bar { stroke: none; }
.af-chart [data-series="1"] { --_s: var(--color-chart-1); }
.af-chart [data-series="2"] { --_s: var(--color-chart-2); }
.af-chart [data-series="3"] { --_s: var(--color-chart-3); }
.af-chart [data-series="other"] { --_s: var(--color-chart-other); }
.af-chart__line[data-series], .af-chart__area[data-series], .af-chart__bar[data-series], .af-chart__dot[data-series] { }
.af-chart__line[data-series] { stroke: var(--_s); }
.af-chart__area[data-series], .af-chart__bar[data-series], .af-chart__dot[data-series] { fill: var(--_s); }
.af-chart__crosshair { stroke: var(--color-border-control); stroke-width: 1; shape-rendering: crispEdges; pointer-events: none; }
.af-chart__hit { fill: transparent; }
.af-chart__hit:focus-visible { outline: none; }
.af-chart__hit:focus-visible + .af-chart__bar, .af-chart__bar[data-hover="true"] { filter: brightness(0.9); }
.af-chart__tip { position: absolute; z-index: var(--z-tooltip); min-inline-size: 9rem; padding: var(--space-2) var(--space-3); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--radius-sm); background: var(--color-surface-raised); box-shadow: var(--popover-shadow); font: var(--font-weight-regular) var(--type-caption-size)/1.5 var(--font-body); color: var(--color-text); pointer-events: none; transform: translate(-50%, calc(-100% - 12px)); white-space: nowrap; }
.af-chart__tip-title { display: block; font-weight: var(--font-weight-semibold); margin-block-end: var(--space-1); }
.af-chart__tip-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); font-variant-numeric: tabular-nums; }
.af-chart__tip-row > span:first-child { display: inline-flex; align-items: center; gap: var(--space-1-5); color: var(--color-muted-text); }
.af-chart__note { margin: 0; font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-chart__table { margin-block-start: var(--space-2); }
.af-chart[data-texture] .af-chart__bar[data-series="2"], .af-chart[data-texture] .af-chart__area[data-series="2"] { fill: url(#af-hatch-45); }
.af-chart[data-texture] .af-chart__bar[data-series="3"], .af-chart[data-texture] .af-chart__area[data-series="3"] { fill: url(#af-hatch-135); }
@media (forced-colors: active) {
.af-chart__line, .af-chart__bar, .af-chart__dot, .af-chart__area { stroke: CanvasText; fill: CanvasText; }
.af-chart__line { fill: none; }
}
@media print { .af-chart__bar[data-series="2"] { fill: url(#af-hatch-45); } .af-chart__bar[data-series="3"] { fill: url(#af-hatch-135); } }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-border-control--color-border-strong--color-chart-1--color-chart-2--color-chart-3--color-chart-grid--color-chart-other--color-muted-text--color-surface--color-surface-raised--color-text--font-body--font-weight-medium--font-weight-regular--font-weight-semibold--popover-shadow--radius-sm--space-0-5--space-1--space-1-5--space-2--space-3--space-4--type-caption-family--type-caption-size--type-caption-weight--type-component-title-family--type-component-title-size--type-component-title-weight--z-tooltipcurl -X PUT https://storage.example.test/v1/buckets/acme-assets \
-H "Authorization: Bearer $AFARA_SAMPLE_TOKEN"<div class="af-code"><div class="af-code__head"><span class="af-code__lang">shell</span><button class="af-button af-button--ghost af-button--sm" type="button" data-copy-target="#dd-code" data-copy-label="Command"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg>Copy</button></div><pre tabindex="0"><code id="dd-code" data-lang="shell">curl -X PUT https://storage.example.test/v1/buckets/acme-assets \
-H "Authorization: Bearer $AFARA_SAMPLE_TOKEN"</code></pre></div>
<div class="af-json" data-json='{"bucket":"acme-assets","versioning":true,"lifecycle":{"expire_days":90,"classes":["standard","archive"]},"note":"Sample data"}' data-json-open="2"></div>/* styles/components/data-display.css · code-block */
/* Recessed well with a header (language / filename) and a copy button. Long lines scroll horizontally inside a focusable
* region. Syntax colours come from semantic tokens (≥4.5:1 on the inset surface in both themes). */
.af-code { margin: 0; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface-inset); overflow: hidden; min-inline-size: 0; }
.af-code__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); min-block-size: var(--space-10); padding: var(--space-1) var(--space-2) var(--space-1) var(--space-4); border-block-end: 1px solid var(--card-border); background: var(--color-surface); }
.af-code__lang { font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }
.af-code__tabs { display: flex; gap: var(--space-1); }
.af-code pre { margin: 0; padding: var(--space-4); overflow-x: auto; font: var(--type-code-weight) var(--type-code-size)/var(--type-code-line-height) var(--type-code-family); font-variant-ligatures: none; tab-size: 2; color: var(--color-text); box-shadow: var(--shadow-inset-well); }
.af-code pre:focus-visible { outline-offset: calc(-1 * var(--focus-ring-width)); }
.af-code pre code { padding: 0; border: 0; background: none; font: inherit; }
.af-code--bare { border-color: transparent; }
.tok-k { color: var(--color-accent-text); font-weight: var(--font-weight-medium); }
.tok-s { color: var(--status-success-text); }
.tok-n { color: var(--status-warning-text); }
.tok-f { color: var(--status-info-text); }
.tok-c { color: var(--color-muted-text); font-style: italic; }
.tok-p { color: var(--color-muted-text); }
.tok-a { color: var(--status-info-text); }
/* styles/components/data-display.css · json-viewer */
/* Collapsible JSON tree built from nested <details> (scripts/component-interactions.js: AfJson.render). Keys, strings, numbers
* and literals use the same token colours as code blocks. */
.af-json { padding: var(--space-3) var(--space-4); border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface-inset); font: var(--type-metadata-weight) var(--type-code-size)/1.7 var(--type-metadata-family); font-variant-ligatures: none; overflow-x: auto; box-shadow: var(--shadow-inset-well); }
.af-json details { margin: 0; }
.af-json summary { list-style: none; display: inline-flex; align-items: center; gap: var(--space-1); border-radius: var(--radius-xs); }
.af-json summary::-webkit-details-marker { display: none; }
.af-json summary::before { content: ""; inline-size: var(--space-3); block-size: var(--space-3); background: var(--color-muted-text); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat; transition: transform var(--motion-fast) var(--ease-standard); }
.af-json details[open] > summary::before { transform: rotate(90deg); }
.af-json details:not([open]) > summary .af-json__close { display: none; }
.af-json details[open] > summary .af-json__preview { display: none; }
.af-json__children { padding-inline-start: var(--space-5); margin: 0; list-style: none; border-inline-start: 1px solid var(--color-border); margin-inline-start: var(--space-1-5); }
.af-json__key { color: var(--status-info-text); }
.af-json__str { color: var(--status-success-text); }
.af-json__num { color: var(--status-warning-text); }
.af-json__lit { color: var(--color-accent-text); }
.af-json__punct, .af-json__preview { color: var(--color-muted-text); }
.af-json__leaf { padding-inline-start: var(--space-4); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-accent-text--color-muted-text--color-surface--color-surface-inset--color-text--focus-ring-width--font-weight-medium--shadow-inset-well--space-1--space-10--space-2--space-3--space-4--status-info-text--status-success-text--status-warning-text--type-code-family--type-code-line-height--type-code-size--type-code-weight--type-metadata-family--type-metadata-size--type-metadata-weight--color-border--ease-standard--motion-fast--radius-xs--space-1-5--space-5Bucket created
By a sample user
Versioning enabled
Quota warning
<ol class="af-timeline">
<li class="af-timeline__item"><span class="af-timeline__marker"><span class="af-pip" data-status="success" aria-hidden="true"></span></span><div><p class="af-timeline__title">Bucket created</p><p class="af-timeline__detail">By a sample user</p><time class="af-timeline__time" datetime="2026-09-12">2026-09-12</time></div></li>
<li class="af-timeline__item"><span class="af-timeline__marker"><span class="af-pip" data-status="info" aria-hidden="true"></span></span><div><p class="af-timeline__title">Versioning enabled</p><time class="af-timeline__time" datetime="2026-09-14">2026-09-14</time></div></li>
<li class="af-timeline__item"><span class="af-timeline__marker"><span class="af-pip" data-status="warning" aria-hidden="true"></span></span><div><p class="af-timeline__title">Quota warning</p><time class="af-timeline__time" datetime="2026-10-30">2026-10-30</time></div></li>
</ol>
<ul class="af-log">
<li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:14:02Z">09:14:02</time><span class="af-log__level"><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>OK</span></span><span class="af-log__msg"><span class="af-log__op">PutObject</span><span class="af-log__resource">acme-assets/logo.png</span></span></li>
<li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:14:09Z">09:14:09</time><span class="af-log__level"><span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>WARN</span></span><span class="af-log__msg"><span class="af-log__op">GetObject</span><span class="af-log__resource">acme-assets/old.zip</span></span></li>
</ul>/* styles/components/data-display.css · timeline */
/* Chronological activity with a left rail. Each item: marker (status pip shape), title, optional detail, a <time>. */
.af-timeline { position: relative; margin: 0; padding: 0; list-style: none; }
.af-timeline__item { position: relative; display: grid; grid-template-columns: var(--space-6) minmax(0, 1fr); gap: 0 var(--space-3); padding-block-end: var(--space-5); }
.af-timeline__item:last-child { padding-block-end: 0; }
.af-timeline__item::before { content: ""; position: absolute; inset-inline-start: calc(var(--space-3) - 1px); inset-block: var(--space-5) calc(-1 * var(--space-1)); inline-size: 1px; background: var(--color-border-strong); }
.af-timeline__item:last-child::before { display: none; }
.af-timeline__marker { display: grid; place-items: center; inline-size: var(--space-6); block-size: var(--space-6); background: var(--color-surface); }
.af-timeline__title { margin: 0; font: var(--font-weight-medium) var(--type-body-sm-size)/1.4 var(--font-body); }
.af-timeline__detail { margin: var(--space-0-5) 0 0; font: var(--type-caption-weight) var(--type-caption-size)/1.5 var(--type-caption-family); color: var(--color-muted-text); }
.af-timeline__time { display: block; margin-block-start: var(--space-1); font: var(--type-metadata-weight) var(--type-metadata-size)/1.4 var(--type-metadata-family); color: var(--color-muted-text); }
/* styles/components/data-display.css · event-log */
/* Dense, scannable system events: time · level · operation · resource · identifier. Rows with failures expand to show the
* error code, message and request ID (<details>). Never truncate an identifier a user may need to quote to support — wrap it. */
.af-log { margin: 0; padding: 0; list-style: none; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }
.af-log__row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-1) var(--space-3); align-items: baseline; padding: var(--space-2) var(--space-4); font-size: var(--type-body-sm-size); }
.af-log__row + .af-log__row, .af-log > details + .af-log__row, .af-log__row + details, .af-log > details + details { border-block-start: 1px solid var(--color-border); }
.af-log > details > summary { list-style: none; cursor: pointer; }
.af-log > details > summary::-webkit-details-marker { display: none; }
.af-log > details > summary:focus-visible { outline-offset: var(--focus-ring-offset-inset); }
@media (hover: hover) { .af-log > details > summary:hover { background: var(--color-surface-hover); } }
.af-log__time { font: var(--type-metadata-weight) var(--type-metadata-size)/1.5 var(--type-metadata-family); color: var(--color-muted-text); white-space: nowrap; }
.af-log__msg { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); min-inline-size: 0; }
.af-log__op { font-family: var(--font-mono); font-size: var(--type-metadata-size); font-weight: var(--font-weight-medium); }
.af-log__resource { font-family: var(--font-mono); font-size: var(--type-metadata-size); color: var(--color-muted-text); overflow-wrap: anywhere; }
.af-log__detail { grid-column: 1 / -1; margin: var(--space-1) 0 var(--space-2); padding: var(--space-3); border-radius: var(--radius-sm); background: var(--status-danger-bg); border: var(--border-width-hairline) solid var(--status-danger-border); color: var(--color-text); font: var(--type-metadata-weight) var(--type-metadata-size)/1.6 var(--type-metadata-family); overflow-wrap: anywhere; }
@media (min-width: 768px) { .af-log__row { grid-template-columns: 9.5rem 5.5rem minmax(0, 1fr); } .af-log__row > .af-log__level { order: 0; } }Design tokens this component reads. Override a token, not the component.
--color-border-strong--color-muted-text--color-surface--font-body--font-weight-medium--space-0-5--space-1--space-3--space-5--space-6--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--type-metadata-family--type-metadata-size--type-metadata-weight--border-width-hairline--card-border--card-radius--color-border--color-surface-hover--color-text--focus-ring-offset-inset--font-mono--radius-sm--space-2--space-4--status-danger-bg--status-danger-borderComponents
Overlays sit above the page and take responsibility for focus. They are built on native <dialog> and the popover attribute, so focus trapping, Escape and top-layer stacking come from the platform. Choose the lightest one that does the job.
| Need | Use | Blocks the page |
|---|---|---|
| Explain a control or show a shortcut | Tooltip | No |
| A small amount of extra content or controls | Popover | No |
| A list of actions | Menu | No |
| A short task or form | Modal | Yes |
| A destructive or irreversible action | Alert dialog | Yes |
| Editing or details beside a list | Drawer | Yes |
| Inspecting one object while browsing | Inspector | Not at 1280 px and wider |
| Mobile menu or short task | Bottom sheet | Yes |
| Confirm that something finished | Toast | No |
Supplementary only: never the sole place a label or instruction lives. Appears on hover and keyboard focus, dismisses with Escape.
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" aria-label="Refresh" data-tooltip="Refresh"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-refresh-cw"/></svg></button>
<button class="af-button af-button--secondary af-button--md" type="button" data-tooltip="Open search" data-tooltip-kbd="Ctrl K">Search</button>/* styles/components/overlays.css · tooltip */
/* Short supplementary text for a control, shown on hover AND keyboard focus, dismissible with Escape, hoverable (WCAG 1.4.13).
* role="tooltip", referenced by aria-describedby on the trigger. Not for essential information, not for touch-only flows, and
* never interactive. Add data-tooltip="…" to any element: scripts create and wire the tooltip. An icon-only button still needs
* aria-label — the tooltip is additional. */
.af-tooltip { position: fixed; margin: 0; inset: auto; max-inline-size: var(--tooltip-max-width); padding: var(--space-1) var(--space-2); border: 0; border-radius: var(--tooltip-radius); background: var(--tooltip-bg); color: var(--tooltip-text); box-shadow: var(--popover-shadow); font: var(--font-weight-medium) var(--type-caption-size)/1.4 var(--font-body); pointer-events: auto; overflow-wrap: anywhere; }
.af-tooltip:not(:popover-open) { display: none; }
.af-tooltip kbd { margin-inline-start: var(--space-1-5); background: transparent; border-color: color-mix(in srgb, var(--tooltip-text) 40%, transparent); color: inherit; box-shadow: none; }Design tokens this component reads. Override a token, not the component.
--font-body--font-weight-medium--popover-shadow--space-1--space-1-5--space-2--tooltip-bg--tooltip-max-width--tooltip-radius--tooltip-text--type-caption-sizeacme-assets-prod
Sample popover. Keep content short; link to a page for more.
<button class="af-button af-button--secondary af-button--md" type="button" popovertarget="ov-pop" aria-expanded="false">Bucket details</button>
<div class="af-popover" id="ov-pop" popover data-placement="bottom-start"><p class="af-popover__title">acme-assets-prod</p><p>Sample popover. Keep content short; link to a page for more.</p></div>/* styles/components/overlays.css · popover */
/* Non-modal floating panel for brief supplemental content or a small form. [popover] gives top-layer rendering, Escape and
* light-dismiss; scripts/component-interactions.js positions it next to its trigger (flipping at viewport edges) and
* manages aria-expanded and focus return. Never put essential information or the only path to an action here. */
.af-popover { position: fixed; margin: 0; inset: auto; min-inline-size: var(--popover-min-width); max-inline-size: min(24rem, calc(100vw - 2 * var(--space-4))); padding: var(--space-4); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--popover-radius); background: var(--color-surface-raised); color: var(--color-text); box-shadow: var(--popover-shadow); font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); }
.af-popover:not(:popover-open) { display: none; }
.af-popover--flush { padding: 0; overflow: hidden; }
.af-popover__title { margin: 0 0 var(--space-1); font: var(--font-weight-semibold) var(--type-body-sm-size)/1.35 var(--font-body); }
@supports (transition-behavior: allow-discrete) {
.af-popover, .af-menu, .af-tooltip { opacity: 0; transform: translateY(calc(-1 * var(--space-1))); transition: opacity var(--motion-fast) var(--ease-standard), transform var(--motion-fast) var(--ease-standard), display var(--motion-fast) allow-discrete, overlay var(--motion-fast) allow-discrete; }
.af-popover:popover-open, .af-menu:popover-open, .af-tooltip:popover-open { opacity: 1; transform: none; }
@starting-style { .af-popover:popover-open, .af-menu:popover-open, .af-tooltip:popover-open { opacity: 0; transform: translateY(calc(-1 * var(--space-1))); } }
}Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-border-strong--color-surface-raised--color-text--ease-standard--font-body--font-weight-semibold--motion-fast--popover-min-width--popover-radius--popover-shadow--space-1--space-4--type-body-sm-line-height--type-body-sm-size<div class="af-cluster">
<button class="af-button af-button--secondary af-button--md" type="button" popovertarget="ov-menu" aria-haspopup="menu" aria-expanded="false">Actions<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></button>
<div class="af-menu" id="ov-menu" popover role="menu" aria-label="Object actions" data-placement="bottom-start">
<button class="af-menu__item" role="menuitem" type="button" data-action="rename">Rename<span class="af-menu__shortcut">F2</span></button>
<button class="af-menu__item" role="menuitem" type="button" data-action="copy">Copy link</button>
<hr class="af-menu__sep" role="separator">
<button class="af-menu__item" role="menuitem" type="button" aria-disabled="true">Restore version</button>
<button class="af-menu__item af-menu__item--danger" role="menuitem" type="button" data-action="delete"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-trash-2"/></svg>Delete</button>
</div>
</div>
<div class="af-card af-card--inset" data-context-menu="#ov-ctx" tabindex="0" aria-label="logo.png. Press Shift+F10 for actions"><div class="af-card__body">logo.png (sample row). Right-click here.</div></div>
<div class="af-menu" id="ov-ctx" popover role="menu" aria-label="logo.png actions">
<button class="af-menu__item" role="menuitem" type="button">Download</button>
<button class="af-menu__item" role="menuitem" type="button">Copy link</button>
<button class="af-menu__item af-menu__item--danger" role="menuitem" type="button">Delete</button>
</div>/* styles/components/overlays.css · menu */
/* Dropdown menu and context menu share one panel: role="menu" > role="menuitem" (or menuitemcheckbox / menuitemradio).
* Keyboard: ArrowDown/Up move, Home/End jump, type-ahead, Enter/Space activate, Escape closes and returns focus to the
* trigger, Tab closes. The trigger has aria-haspopup="menu" and aria-expanded. Destructive items are last, after a separator,
* and open a confirmation — a menu item never deletes directly. */
.af-menu { position: fixed; margin: 0; inset: auto; min-inline-size: var(--popover-min-width); max-inline-size: min(20rem, calc(100vw - 2 * var(--space-4))); max-block-size: min(24rem, calc(100dvh - 2 * var(--space-4))); overflow-y: auto; overscroll-behavior: contain; padding: var(--space-1); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--popover-radius); background: var(--color-surface-raised); color: var(--color-text); box-shadow: var(--popover-shadow); }
.af-menu:not(:popover-open) { display: none; }
.af-menu__item { display: flex; align-items: center; gap: var(--space-3); inline-size: 100%; min-block-size: var(--menu-item-height); padding: var(--space-1-5) var(--space-3); border-radius: var(--menu-item-radius); color: var(--color-text); font: var(--font-weight-regular) var(--type-body-sm-size)/1.3 var(--font-body); text-align: start; text-decoration: none; cursor: pointer; }
.af-menu__item > .af-icon { inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); color: var(--color-muted-text); }
.af-menu__item:hover, .af-menu__item:focus { background: var(--color-surface-hover); }
.af-menu__item:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: calc(-1 * var(--focus-ring-width)); }
.af-menu__item[aria-disabled="true"] { color: var(--color-disabled-text); cursor: not-allowed; }
.af-menu__item[aria-disabled="true"] > .af-icon { color: inherit; }
.af-menu__item[aria-disabled="true"]:hover { background: none; }
.af-menu__item--danger { color: var(--status-danger-text); }
.af-menu__item--danger > .af-icon { color: var(--status-danger-solid); }
.af-menu__item--danger:hover, .af-menu__item--danger:focus { background: var(--status-danger-bg); }
.af-menu__shortcut { margin-inline-start: auto; padding-inline-start: var(--space-4); font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }
.af-menu__item[aria-checked="true"]::after { content: ""; margin-inline-start: auto; inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); background: var(--color-accent-text); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat; }
.af-menu__label { padding: var(--space-1-5) var(--space-3) var(--space-1); font: var(--type-eyebrow-weight) 0.6875rem/1.3 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); }
.af-menu__sep { block-size: 0; margin: var(--space-1) calc(-1 * var(--space-1)); border: 0; border-block-start: 1px solid var(--color-border); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-accent-text--color-border--color-border-strong--color-disabled-text--color-muted-text--color-surface-hover--color-surface-raised--color-text--focus-ring-color--focus-ring-width--font-body--font-weight-regular--icon-size-sm--menu-item-height--menu-item-radius--popover-min-width--popover-radius--popover-shadow--space-1--space-1-5--space-3--space-4--status-danger-bg--status-danger-solid--status-danger-text--type-body-sm-size--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-weight--type-metadata-family--type-metadata-size--type-metadata-weight<button class="af-button af-button--primary af-button--md" type="button" data-dialog-open="#ov-modal">Create bucket</button>
<dialog class="af-modal" id="ov-modal" aria-labelledby="ov-modal-t">
<form method="dialog" class="af-modal__panel">
<div class="af-modal__header"><div><h4 class="af-modal__title" id="ov-modal-t">Create bucket</h4><p class="af-modal__description">Sample form. Nothing is created.</p></div><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
<div class="af-modal__body"><div class="af-field"><label class="af-field__label" for="ov-m-name">Bucket name</label><input class="af-input" id="ov-m-name" placeholder="acme-assets" autocomplete="off"></div></div>
<div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close>Cancel</button><button class="af-button af-button--primary af-button--md" type="button" data-dialog-close="save" data-toast="success" data-toast-title="Bucket created (sample)">Create</button></div>
</form>
</dialog>/* styles/components/overlays.css · modal */
/* <dialog class="af-modal"> with a .af-modal__panel (form or div): header (title + close), body (scrolls), footer (actions).
* Always labelled (aria-labelledby → the title). Opened with showModal() — never by toggling display. Escape and the close
* button dismiss; focus returns to the opener. Primary action sits at the inline-end of the footer. */
.af-modal {
margin: auto; padding: 0;
inline-size: min(var(--modal-width-md), calc(100vw - 2 * var(--space-4)));
max-block-size: calc(100dvh - 2 * var(--space-8));
border: var(--border-width-hairline) solid var(--color-border-strong);
border-radius: var(--modal-radius);
background: var(--color-surface-raised); color: var(--color-text);
box-shadow: var(--modal-shadow);
overflow: hidden;
}
.af-modal[open] { display: flex; flex-direction: column; }
.af-modal--sm { inline-size: min(var(--modal-width-sm), calc(100vw - 2 * var(--space-4))); }
.af-modal--lg { inline-size: min(var(--modal-width-lg), calc(100vw - 2 * var(--space-4))); }
.af-modal__panel { display: flex; flex-direction: column; min-block-size: 0; max-block-size: inherit; margin: 0; }
.af-modal__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); padding: var(--modal-padding) var(--modal-padding) var(--space-2); }
.af-modal__title { margin: 0; font: var(--type-h4-weight) var(--type-h4-size)/var(--type-h4-line-height) var(--type-h4-family); letter-spacing: var(--type-h4-letter-spacing); }
.af-modal__description { margin: var(--space-1) 0 0; color: var(--color-muted-text); font-size: var(--type-body-sm-size); line-height: var(--type-body-line-height); }
.af-modal__body { flex: 1 1 auto; min-block-size: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-2) var(--modal-padding) var(--modal-padding); display: flex; flex-direction: column; gap: var(--space-4); }
.af-modal__footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-3); padding: var(--space-4) var(--modal-padding); border-block-start: 1px solid var(--color-border); background: var(--color-surface); }
.af-modal__footer--start { justify-content: space-between; }
@supports (transition-behavior: allow-discrete) {
.af-modal { opacity: 0; transform: translateY(var(--space-2)) scale(0.985); transition: opacity var(--motion-emphasis) var(--ease-enter), transform var(--motion-emphasis) var(--ease-enter), display var(--motion-emphasis) allow-discrete, overlay var(--motion-emphasis) allow-discrete; }
.af-modal[open] { opacity: 1; transform: none; }
@starting-style { .af-modal[open] { opacity: 0; transform: translateY(var(--space-2)) scale(0.985); } }
}Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-border--color-border-strong--color-muted-text--color-surface--color-surface-raised--color-text--ease-enter--modal-padding--modal-radius--modal-shadow--modal-width-lg--modal-width-md--modal-width-sm--motion-emphasis--space-1--space-2--space-3--space-4--space-8--type-body-line-height--type-body-sm-size--type-h4-family--type-h4-letter-spacing--type-h4-line-height--type-h4-size--type-h4-weightFor irreversible actions. It states the consequence, lists what will be affected, and asks for the resource name. The destructive button stays disabled until the name matches. Clicking the backdrop does not dismiss it, and Cancel has initial focus. Escape still closes it, as a Cancel.
<button class="af-button af-button--destructive af-button--md" type="button" data-dialog-open="#ov-alert">Delete bucket…</button>
<dialog class="af-modal af-modal--sm" id="ov-alert" role="alertdialog" aria-labelledby="ov-alert-t" aria-describedby="ov-alert-d">
<form method="dialog" class="af-modal__panel">
<div class="af-alertdialog__head"><span class="af-alertdialog__icon" aria-hidden="true"><svg class="af-icon" focusable="false"><use href="#i-triangle-alert"/></svg></span><div><h4 class="af-modal__title" id="ov-alert-t">Delete acme-assets-prod?</h4><p class="af-modal__description" id="ov-alert-d">This permanently removes the bucket. It cannot be undone.</p>
<ul class="af-confirm-list"><li>1,204 objects (sample)</li><li>All versions and metadata</li></ul></div></div>
<div class="af-modal__body"><div class="af-field"><label class="af-field__label" for="ov-a-in">Type <span class="af-confirm-input__name">acme-assets-prod</span> to confirm</label><input class="af-input" id="ov-a-in" data-confirm-input="acme-assets-prod" autocomplete="off"></div></div>
<div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close autofocus>Cancel</button><button class="af-button af-button--destructive af-button--md" type="button" data-confirm-button="delete" disabled data-dialog-close="delete" data-toast="danger" data-toast-title="Deleted (sample)">Delete bucket</button></div>
</form>
</dialog>/* styles/components/overlays.css · alert-dialog */
/* Confirmation, destructive confirmation and alert dialogs use role="alertdialog" and a small width. For destructive and
* irreversible actions: the initial focus is the SAFE action (autofocus on Cancel), the button says exactly what will happen
* ("Delete bucket", not "OK"), the body names the resource and the consequence, and irreversible deletes require typing the
* resource name (.af-confirm-input) before the destructive button enables. */
.af-alertdialog__icon { display: grid; place-items: center; flex: none; inline-size: var(--avatar-size-lg); block-size: var(--avatar-size-lg); border-radius: var(--radius-full); background: var(--status-danger-bg); color: var(--status-danger-solid); border: var(--border-width-hairline) solid var(--status-danger-border); }
.af-alertdialog__icon[data-tone="warning"] { background: var(--status-warning-bg); color: var(--status-warning-solid); border-color: var(--status-warning-border); }
.af-alertdialog__icon[data-tone="info"] { background: var(--status-info-bg); color: var(--status-info-solid); border-color: var(--status-info-border); }
.af-alertdialog__head { display: flex; gap: var(--space-4); align-items: flex-start; padding: var(--modal-padding) var(--modal-padding) var(--space-2); }
.af-alertdialog__head > div { min-inline-size: 0; }
.af-alertdialog__head .af-modal__title { margin-block-start: var(--space-1); }
.af-confirm-list { margin: var(--space-2) 0 0; padding-inline-start: var(--space-5); font-size: var(--type-body-sm-size); color: var(--color-muted-text); }
.af-confirm-input__name { font-family: var(--font-mono); font-size: 0.92em; padding: 0 var(--space-1); border-radius: var(--radius-xs); background: var(--color-surface-inset); border: var(--border-width-hairline) solid var(--color-border-strong); overflow-wrap: anywhere; }Design tokens this component reads. Override a token, not the component.
--avatar-size-lg--border-width-hairline--color-border-strong--color-muted-text--color-surface-inset--font-mono--modal-padding--radius-full--radius-xs--space-1--space-2--space-4--space-5--status-danger-bg--status-danger-border--status-danger-solid--status-info-bg--status-info-border--status-info-solid--status-warning-bg--status-warning-border--status-warning-solid--type-body-sm-size<button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#ov-drawer">Open drawer</button>
<button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#ov-insp">Open inspector</button>
<button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#ov-sheet">Open bottom sheet</button>
<dialog class="af-drawer" id="ov-drawer" aria-labelledby="ov-drawer-t">
<form method="dialog" class="af-modal__panel">
<div class="af-modal__header"><h4 class="af-modal__title" id="ov-drawer-t">Edit bucket</h4><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
<div class="af-modal__body af-stack"><div class="af-field"><label class="af-field__label" for="ov-d-n">Display name</label><input class="af-input" id="ov-d-n" value="acme-assets-prod"></div><label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch" checked><span class="af-switch-field__text">Versioning</span></label></div>
<div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close>Cancel</button><button class="af-button af-button--primary af-button--md" type="button" data-dialog-close="save">Save changes</button></div>
</form>
</dialog>
<dialog class="af-drawer af-drawer--inspector" id="ov-insp" aria-labelledby="ov-insp-t">
<div class="af-modal__panel">
<div class="af-modal__header"><h4 class="af-modal__title" id="ov-insp-t">logo.png</h4><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close inspector"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
<div class="af-modal__body"><dl class="af-meta"><div class="af-meta__row"><dt>Type</dt><dd>image/png</dd></div><div class="af-meta__row"><dt>Size</dt><dd>18,400 B</dd></div><div class="af-meta__row"><dt>Modified</dt><dd>2026-11-02 (sample)</dd></div></dl></div>
</div>
</dialog>
<dialog class="af-sheet" id="ov-sheet" aria-labelledby="ov-sheet-t">
<div class="af-modal__panel"><span class="af-sheet__handle" aria-hidden="true"></span>
<div class="af-modal__header"><h4 class="af-modal__title" id="ov-sheet-t">Object actions</h4><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
<div class="af-modal__body"><div class="af-actionsheet"><button class="af-menu__item" type="button" data-dialog-close>Download</button><button class="af-menu__item" type="button" data-dialog-close>Copy link</button><button class="af-menu__item af-menu__item--danger" type="button" data-dialog-close>Delete</button></div></div>
</div>
</dialog>/* styles/components/overlays.css · drawer */
/* Side drawer: a modal panel that slides in from the inline-end edge, keeping the page visible behind the scrim. Same
* anatomy and behaviour as a modal. Use for secondary tasks with several fields. Full width below 640px. */
.af-drawer {
margin: 0; padding: 0;
position: fixed; inset-block: 0; inset-inline: auto 0;
inline-size: min(var(--drawer-width), 100vw); block-size: 100dvh; max-block-size: none; max-inline-size: 100vw;
border: 0; border-inline-start: var(--border-width-hairline) solid var(--color-border-strong); border-radius: 0;
background: var(--color-surface-raised); color: var(--color-text);
box-shadow: var(--drawer-shadow);
}
.af-drawer[open] { display: flex; flex-direction: column; }
.af-drawer .af-modal__panel { flex: 1; min-block-size: 0; max-block-size: none; block-size: 100%; }
.af-drawer .af-modal__header { padding: var(--drawer-padding) var(--drawer-padding) var(--space-3); border-block-end: 1px solid var(--color-border); }
.af-drawer .af-modal__body { padding: var(--drawer-padding); }
.af-drawer .af-modal__footer { padding: var(--space-4) var(--drawer-padding); }
.af-drawer--wide { --drawer-width: 640px; }
@supports (transition-behavior: allow-discrete) {
.af-drawer { transform: translateX(100%); transition: transform var(--motion-emphasis) var(--ease-exit), display var(--motion-emphasis) allow-discrete, overlay var(--motion-emphasis) allow-discrete; }
.af-drawer[open] { transform: none; transition-timing-function: var(--ease-enter); }
@starting-style { .af-drawer[open] { transform: translateX(100%); } }
[dir="rtl"] .af-drawer { transform: translateX(-100%); }
}
/* styles/components/overlays.css · inspector */
/* Right-side inspector: details of the SELECTED item while the user keeps browsing. At ≥1280px it opens NON-modally (show(),
* no scrim, page stays interactive, sits below the header). Below 1280px scripts open it with showModal() as a drawer. It
* updates in place as the selection changes; Escape or the close button dismisses it and returns focus to the selection. */
.af-drawer--inspector { inline-size: min(var(--layout-inspector-width), 100vw); }
@media (min-width: 1280px) {
.af-drawer--inspector:not(:modal) { inset-block-start: var(--layout-header-height); block-size: calc(100dvh - var(--layout-header-height)); box-shadow: none; border-inline-start: var(--border-width-hairline) solid var(--color-border); background: var(--color-surface); }
.af-drawer--inspector:not(:modal)::backdrop { display: none; }
}
/* styles/components/overlays.css · bottom-sheet */
/* Bottom sheet: a modal panel anchored to the block-end edge, for mobile. Has a drag-handle affordance (decorative — dismiss
* with the Close/Cancel button, Escape or a tap on the scrim; swipe-to-dismiss is an enhancement, never the only way). */
.af-sheet {
margin: 0; padding: 0;
position: fixed; inset-block: auto 0; inset-inline: 0;
inline-size: 100%; max-inline-size: 100%; max-block-size: calc(var(--sheet-max-height) * 1dvh);
border: 0; border-block-start: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
background: var(--color-surface-raised); color: var(--color-text);
box-shadow: var(--drawer-shadow); overflow: hidden;
}
.af-sheet[open] { display: flex; flex-direction: column; }
.af-sheet__handle { flex: none; inline-size: var(--space-10); block-size: var(--space-1); margin: var(--space-2) auto var(--space-1); border-radius: var(--radius-full); background: var(--color-border-strong); }
.af-sheet .af-modal__panel { max-block-size: none; min-block-size: 0; }
.af-sheet .af-modal__body { padding-block-end: calc(var(--modal-padding) + env(safe-area-inset-bottom, 0px)); }
@supports (transition-behavior: allow-discrete) {
.af-sheet { transform: translateY(100%); transition: transform var(--motion-emphasis) var(--ease-exit), display var(--motion-emphasis) allow-discrete, overlay var(--motion-emphasis) allow-discrete; }
.af-sheet[open] { transform: none; transition-timing-function: var(--ease-enter); }
@starting-style { .af-sheet[open] { transform: translateY(100%); } }
}
/* Mobile action sheet: a sheet whose body is a list of actions + a Cancel. Buttons are full-width and touch-sized. */
.af-actionsheet { display: flex; flex-direction: column; gap: var(--space-1); }
.af-actionsheet .af-menu__item { min-block-size: var(--button-height-lg); font-size: var(--type-body-size); padding-inline: var(--space-4); }
.af-actionsheet__title { padding: var(--space-1) var(--space-4) var(--space-2); font: var(--type-eyebrow-weight) var(--type-eyebrow-size)/1.4 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); overflow-wrap: anywhere; }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-border--color-border-strong--color-surface-raised--color-text--drawer-padding--drawer-shadow--drawer-width--ease-enter--ease-exit--motion-emphasis--space-3--space-4--color-surface--layout-header-height--layout-inspector-width--button-height-lg--color-muted-text--modal-padding--radius-full--sheet-max-height--sheet-radius--space-1--space-10--space-2--type-body-size--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-size--type-eyebrow-weightThe inspector is non-modal at 1280 px and wider, so the list behind it stays usable; below that it behaves as a drawer. A menu can become a bottom sheet on small screens with data-menu-sheet.
For confirmation of something that has already happened. Success and info toasts announce politely; danger toasts announce assertively. Toasts never carry the only route to an action, and they wait until an open modal closes.
<button class="af-button af-button--secondary af-button--md" type="button" data-toast="success" data-toast-title="Settings saved" data-toast-text="Sample notification.">Success</button>
<button class="af-button af-button--secondary af-button--md" type="button" data-toast="warning" data-toast-title="Quota at 86%" data-toast-text="Sample notification.">Warning</button>
<button class="af-button af-button--secondary af-button--md" type="button" data-toast="danger" data-toast-title="Upload failed" data-toast-text="ERR_CONNECTION_RESET (sample)">Danger</button>/* styles/components/overlays.css · toast */
/* Transient confirmation of something that HAPPENED, announced politely (role="status"); errors use role="alert", persist until
* dismissed, and say what failed and what to do. Auto-dismiss (success/info only) is ≥ 6s and pauses on hover and focus; every
* toast has a Dismiss button; at most one action. Toasts never carry information that exists nowhere else. */
.af-toast-region__polite, .af-toast-region__assertive { display: contents; }
.af-toast-region { position: fixed; z-index: var(--z-toast); inset-block-end: var(--space-4); inset-inline: var(--space-4); display: flex; flex-direction: column-reverse; align-items: stretch; gap: var(--space-2); pointer-events: none; }
.af-toast { pointer-events: auto; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-1) var(--space-3); align-items: start; inline-size: 100%; max-inline-size: var(--toast-width); margin-inline-start: auto; padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4); border-radius: var(--toast-radius); background: var(--color-surface-inverse); color: var(--color-inverse-text); box-shadow: var(--shadow-toast); animation: af-toast-in var(--motion-standard) var(--ease-enter); }
.af-toast[data-leaving="true"] { animation: af-toast-out var(--motion-fast) var(--ease-exit) forwards; }
.af-toast__icon { inline-size: var(--icon-size-md); block-size: var(--icon-size-md); margin-block-start: 1px; }
.af-toast[data-tone="success"] .af-toast__icon { color: var(--status-success-solid); }
.af-toast[data-tone="danger"] .af-toast__icon { color: var(--status-danger-solid); }
.af-toast[data-tone="warning"] .af-toast__icon { color: var(--status-warning-solid); }
.af-toast__title { margin: 0; font: var(--font-weight-semibold) var(--type-body-sm-size)/1.4 var(--font-body); }
.af-toast__text { margin: var(--space-0-5) 0 0; font: var(--font-weight-regular) var(--type-caption-size)/1.5 var(--font-body); color: var(--color-inverse-muted-text); overflow-wrap: anywhere; }
.af-toast__actions { display: flex; gap: var(--space-2); margin-block-start: var(--space-2); }
.af-toast .af-button { --_text: var(--color-inverse-text); --_bg-hover: color-mix(in srgb, var(--color-inverse-text) 14%, transparent); --_bg-active: color-mix(in srgb, var(--color-inverse-text) 22%, transparent); }
@keyframes af-toast-in { from { opacity: 0; transform: translateY(var(--space-2)); } to { opacity: 1; transform: none; } }
@keyframes af-toast-out { to { opacity: 0; transform: translateY(var(--space-1)); } }
@media (min-width: 640px) { .af-toast-region { inset-inline: auto var(--space-6); inset-block-end: var(--space-6); } }Design tokens this component reads. Override a token, not the component.
--color-inverse-muted-text--color-inverse-text--color-surface-inverse--ease-enter--ease-exit--font-body--font-weight-regular--font-weight-semibold--icon-size-md--motion-fast--motion-standard--shadow-toast--space-0-5--space-1--space-2--space-3--space-4--space-6--status-danger-solid--status-success-solid--status-warning-solid--toast-radius--toast-width--type-body-sm-size--type-caption-size--z-toast<button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#cmd"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-command"/></svg>Open command palette<kbd>Ctrl K</kbd></button>/* styles/components/overlays.css · command-palette */
/* ⌘K / Ctrl+K. A modal <dialog> whose search input is an ARIA combobox controlling a listbox of commands grouped by kind
* (Navigate · Actions · Resources). Focus stays in the input; arrows move the active option (aria-activedescendant); Enter runs
* it; Escape closes. Results announce their count via a polite live region. Every command here must also exist elsewhere in
* the UI — the palette is a shortcut, never the only path. */
.af-command { margin: 0 auto; margin-block-start: min(12vh, var(--space-16)); padding: 0; inline-size: min(40rem, calc(100vw - 2 * var(--space-4))); max-block-size: min(32rem, calc(100dvh - 2 * var(--space-8))); border: var(--border-width-hairline) solid var(--color-border-strong); border-radius: var(--modal-radius); background: var(--color-surface-raised); color: var(--color-text); box-shadow: var(--modal-shadow); overflow: hidden; }
.af-command[open] { display: flex; flex-direction: column; }
.af-command__search { display: flex; align-items: center; gap: var(--space-3); padding: 0 var(--space-4); border-block-end: 1px solid var(--color-border); }
.af-command__search > .af-icon { color: var(--color-muted-text); }
.af-command__input { flex: 1; min-inline-size: 0; block-size: var(--button-height-lg); border: 0; background: transparent; color: var(--color-text); font-size: var(--type-body-size); outline: none; }
.af-command__input::placeholder { color: var(--input-placeholder); opacity: 1; }
.af-command__search:focus-within { box-shadow: inset 0 -2px 0 var(--focus-ring-color); }
.af-command__list { flex: 1; min-block-size: 0; overflow-y: auto; overscroll-behavior: contain; margin: 0; padding: var(--space-2); list-style: none; }
.af-command__group-label { padding: var(--space-2) var(--space-3) var(--space-1); font: var(--type-eyebrow-weight) 0.6875rem/1.3 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); }
.af-command__empty { padding: var(--space-8) var(--space-4); text-align: center; color: var(--color-muted-text); font-size: var(--type-body-sm-size); }
.af-command__foot { display: flex; flex-wrap: wrap; gap: var(--space-4); padding: var(--space-2) var(--space-4); border-block-start: 1px solid var(--color-border); background: var(--color-surface); font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-command__foot > span { display: inline-flex; align-items: center; gap: var(--space-1-5); }
@supports (transition-behavior: allow-discrete) {
.af-command { opacity: 0; transform: translateY(calc(-1 * var(--space-2))); transition: opacity var(--motion-standard) var(--ease-enter), transform var(--motion-standard) var(--ease-enter), display var(--motion-standard) allow-discrete, overlay var(--motion-standard) allow-discrete; }
.af-command[open] { opacity: 1; transform: none; }
@starting-style { .af-command[open] { opacity: 0; transform: translateY(calc(-1 * var(--space-2))); } }
}
/* Command palette groups: a group is a labelled <ul role="group"> nested in the listbox (valid ARIA). Reset the list defaults. */
.af-command__group, .af-command__group > ul { margin: 0; padding: 0; list-style: none; }
.af-command__group + .af-command__group { margin-block-start: var(--space-1); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--button-height-lg--color-border--color-border-strong--color-muted-text--color-surface--color-surface-raised--color-text--ease-enter--focus-ring-color--input-placeholder--modal-radius--modal-shadow--motion-standard--space-1--space-1-5--space-16--space-2--space-3--space-4--space-8--type-body-size--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-weightComponents
Say what happened, what it means, and what to do next. Every tone pairs an icon and a word with its colour, and no state is ever shown as colour alone.
Keys are shown once
Copy the secret now. You cannot view it again.
Bucket created
You can start uploading objects.
Approaching your storage limit
Sample usage is above the 80% warning line.
Upload failed
The connection was reset. Error ERR_CONNECTION_RESET. Try again.
Read-only for 5 minutes
Compact alert for inline use inside a form or card.
<div class="af-alert" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div class="af-alert__body"><p class="af-alert__title">Keys are shown once</p><p class="af-alert__text">Copy the secret now. You cannot view it again.</p></div></div>
<div class="af-alert af-alert--success" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg><div class="af-alert__body"><p class="af-alert__title">Bucket created</p><p class="af-alert__text">You can start uploading objects.</p></div></div>
<div class="af-alert af-alert--warning" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg><div class="af-alert__body"><p class="af-alert__title">Approaching your storage limit</p><p class="af-alert__text">Sample usage is above the 80% warning line.</p></div><div class="af-alert__actions"><button class="af-button af-button--secondary af-button--sm" type="button">Review usage</button></div></div>
<div class="af-alert af-alert--danger" role="alert"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-circle-x"/></svg><div class="af-alert__body"><p class="af-alert__title">Upload failed</p><p class="af-alert__text">The connection was reset. Error <code>ERR_CONNECTION_RESET</code>. Try again.</p></div></div>
<div class="af-alert af-alert--neutral"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div class="af-alert__body"><p class="af-alert__title">Read-only for 5 minutes</p></div></div>
<div class="af-alert af-alert--compact" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div class="af-alert__body"><p class="af-alert__text">Compact alert for inline use inside a form or card.</p></div><button type="button" class="af-button af-button--ghost af-button--icon af-button--sm af-alert__dismiss" aria-label="Dismiss alert"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>/* styles/components/feedback.css · alert */
/* Inline, persistent message about the current view or form. Choose the live-region role by urgency:
* role="alert" (assertive) — errors that block the task
* role="status" (polite) — success / info that appears after an action
* no role — static information present at page load (it is already in reading order)
* Anatomy: icon · title · description · optional actions · optional dismiss. */
.af-alert { --_bg: var(--status-info-bg); --_border: var(--status-info-border); --_icon: var(--status-info-solid);
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-1) var(--space-3); align-items: start;
padding: var(--space-3) var(--space-4); border: var(--border-width-hairline) solid var(--_border); border-radius: var(--card-radius); background: var(--_bg); color: var(--color-text); }
.af-alert--success { --_bg: var(--status-success-bg); --_border: var(--status-success-border); --_icon: var(--status-success-solid); }
.af-alert--warning { --_bg: var(--status-warning-bg); --_border: var(--status-warning-border); --_icon: var(--status-warning-solid); }
.af-alert--danger, .af-alert--error { --_bg: var(--status-danger-bg); --_border: var(--status-danger-border); --_icon: var(--status-danger-solid); }
.af-alert--neutral { --_bg: var(--status-neutral-bg); --_border: var(--status-neutral-border); --_icon: var(--status-neutral-solid); }
.af-alert__icon { inline-size: var(--icon-size-md); block-size: var(--icon-size-md); margin-block-start: 1px; color: var(--_icon); }
.af-alert__body { min-inline-size: 0; }
.af-alert__title { margin: 0; font: var(--font-weight-semibold) var(--type-body-sm-size)/1.4 var(--font-body); }
.af-alert__text { margin: var(--space-0-5) 0 0; font: var(--font-weight-regular) var(--type-body-sm-size)/1.5 var(--font-body); overflow-wrap: anywhere; }
.af-alert__text code { background: transparent; border-color: var(--_border); }
.af-alert__actions { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: var(--space-2); margin-block-start: var(--space-2); }
.af-alert__dismiss { margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-2)) 0 0; }
.af-alert--compact { padding: var(--space-2) var(--space-3); }
.af-alert--compact .af-alert__icon { inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-radius--color-text--font-body--font-weight-regular--font-weight-semibold--icon-size-md--icon-size-sm--space-0-5--space-1--space-2--space-3--space-4--status-danger-bg--status-danger-border--status-danger-solid--status-info-bg--status-info-border--status-info-solid--status-neutral-bg--status-neutral-border--status-neutral-solid--status-success-bg--status-success-border--status-success-solid--status-warning-bg--status-warning-border--status-warning-solid--type-body-sm-sizeUse role="alert" only for something that appears after the page has loaded and needs immediate attention. Static messages need no role; most dynamic ones use role="status".
<div class="af-banner" role="status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><span>A new console layout is available. Sample announcement.</span><a href="#feedback">Read more</a></div>
<div class="af-banner af-banner--warning" role="status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg><span>Scheduled maintenance is shown here when there is some. Sample text.</span></div>/* styles/components/feedback.css · banner */
/* Full-width, page-level notice (maintenance window, billing threshold, read-only mode). Sits directly under the header, above
* the page content. One banner at a time. */
.af-banner { display: flex; align-items: center; justify-content: center; gap: var(--space-3); flex-wrap: wrap; padding: var(--space-2) var(--app-gutter); border-block-end: 1px solid var(--status-info-border); background: var(--status-info-bg); color: var(--color-text); font-size: var(--type-body-sm-size); line-height: 1.4; text-align: center; }
.af-banner > .af-icon { color: var(--status-info-solid); }
.af-banner--warning { background: var(--status-warning-bg); border-color: var(--status-warning-border); }
.af-banner--warning > .af-icon { color: var(--status-warning-solid); }
.af-banner--danger { background: var(--status-danger-bg); border-color: var(--status-danger-border); }
.af-banner--danger > .af-icon { color: var(--status-danger-solid); }Design tokens this component reads. Override a token, not the component.
--app-gutter--color-text--space-2--space-3--status-danger-bg--status-danger-border--status-danger-solid--status-info-bg--status-info-border--status-info-solid--status-warning-bg--status-warning-border--status-warning-solid--type-body-sm-sizeUsed at the top of a form after a failed submit. See Forms for the live version: it takes focus, and each item links to its field.
<div class="af-error-summary" role="alert" tabindex="-1"><h4 class="af-error-summary__title">There is a problem</h4><ul><li><a href="#feedback">Enter a bucket name.</a></li><li><a href="#feedback">Enter an email address.</a></li></ul></div>/* styles/components/feedback.css · error-summary */
/* At the top of a form after a failed submit: lists every error with a link to its field. It receives focus when shown (tabindex
* -1) so screen-reader and keyboard users land on it. */
.af-error-summary { padding: var(--space-4); border: var(--border-width-thick) solid var(--status-danger-solid); border-radius: var(--card-radius); background: var(--status-danger-bg); }
.af-error-summary:focus-visible { outline-offset: var(--space-1); }
.af-error-summary__title { margin: 0 0 var(--space-2); font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); color: var(--status-danger-text); display: flex; align-items: center; gap: var(--space-2); }
.af-error-summary ul { margin: 0; padding-inline-start: var(--space-5); font-size: var(--type-body-sm-size); }
.af-error-summary a { color: var(--status-danger-text); font-weight: var(--font-weight-medium); }Design tokens this component reads. Override a token, not the component.
--border-width-thick--card-radius--font-weight-medium--space-1--space-2--space-4--space-5--status-danger-bg--status-danger-solid--status-danger-text--type-body-sm-size--type-component-title-family--type-component-title-size--type-component-title-weightKnow the fraction? Use a progress bar. Know the shape of the content? Use a skeleton. Neither? Use a spinner with a sentence that says what is happening. The skeleton shimmer is the only continuous animation in the system, and it stops under reduced motion.
<div class="af-stack"><span class="af-loading"><span class="af-spinner" aria-hidden="true"></span>Verifying bucket name…</span><span class="af-loading"><span class="af-spinner af-spinner--sm" aria-hidden="true"></span>Small</span><span class="af-loading"><span class="af-spinner af-spinner--lg" aria-hidden="true"></span>Large</span></div>
<div class="af-card" aria-busy="true"><div class="af-card__body af-stack"><span class="af-visually-hidden">Loading bucket details</span><span class="af-skeleton af-skeleton--title"></span><span class="af-skeleton af-skeleton--text"></span><span class="af-skeleton af-skeleton--text"></span><span class="af-skeleton af-skeleton--block"></span></div></div>/* styles/components/feedback.css · spinner */
/* Indeterminate activity for operations without measurable progress, shown with text that says what is happening. Prefer
* <progress> when you know the fraction. Under reduced motion the ring is static (a three-quarter arc) — the label still says
* what is happening. Decorative: aria-hidden="true"; announce busy state in a live region or via aria-busy on the region. */
.af-spinner { inline-size: var(--icon-size-md); block-size: var(--icon-size-md); flex: none; border: 2px solid color-mix(in srgb, currentColor 25%, transparent); border-block-start-color: currentColor; border-radius: 50%; animation: af-spin 0.8s linear infinite; }
.af-spinner--sm { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); border-width: 1.5px; }
.af-spinner--lg { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); border-width: 2.5px; }
.af-button > .af-spinner { inline-size: var(--_icon); block-size: var(--_icon); }
@keyframes af-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .af-spinner { animation: none; border-color: currentColor; border-inline-end-color: transparent; } }
.af-loading { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--color-muted-text); font-size: var(--type-body-sm-size); }
/* styles/components/feedback.css · skeleton */
/* Placeholder shaped like the content that is loading, so layout does not jump. Mark the container aria-busy="true" and
* provide a visually hidden "Loading …" message; remove the skeleton when data arrives. The shimmer is the only continuous
* animation in the system and stops under reduced motion. */
.af-skeleton { --_a: var(--color-surface-inset); --_b: var(--color-border); display: block; block-size: var(--space-3); border-radius: var(--radius-sm); background: linear-gradient(90deg, var(--_a) 0%, var(--_b) 50%, var(--_a) 100%) 0 0 / 200% 100%; animation: af-shimmer 1.6s var(--ease-standard) infinite; }
.af-skeleton--text { block-size: var(--space-3); }
.af-skeleton--title { block-size: var(--space-5); inline-size: 40%; }
.af-skeleton--block { block-size: var(--space-16); }
.af-skeleton--circle { border-radius: 50%; inline-size: var(--avatar-size-md); block-size: var(--avatar-size-md); }
@keyframes af-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .af-skeleton { animation: none; background: var(--_a); } }Design tokens this component reads. Override a token, not the component.
--color-muted-text--icon-size-lg--icon-size-md--icon-size-xs--space-2--type-body-sm-size--avatar-size-md--color-border--color-surface-inset--ease-standard--radius-sm--space-16--space-3--space-5For whole views that cannot show their content. Each says what happened, whether it is your fault or ours, and what to do. The code is selectable so it can be quoted to support.
This is a problem on our side. Your data is not affected.
Ask an owner of this project to grant you the Viewer role.
<div class="af-state" data-status="danger"><span class="af-state__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-x"/></svg></span><span class="af-state__code">ERR_REQUEST_FAILED</span><h4 class="af-state__title">We could not load your buckets</h4><p class="af-state__text">This is a problem on our side. Your data is not affected.</p><div class="af-state__actions"><button class="af-button af-button--primary af-button--md" type="button">Try again</button><a class="af-button af-button--ghost af-button--md" href="#feedback">Contact support</a></div></div>
<div class="af-state" data-status="warning"><span class="af-state__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-lock"/></svg></span><span class="af-state__code">403</span><h4 class="af-state__title">You do not have access</h4><p class="af-state__text">Ask an owner of this project to grant you the Viewer role.</p></div>
<div class="af-state" data-status="info"><span class="af-state__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-search"/></svg></span><span class="af-state__code">404</span><h4 class="af-state__title">Bucket not found</h4><p class="af-state__text">It may have been deleted, or the address is wrong.</p><div class="af-state__actions"><a class="af-button af-button--secondary af-button--md" href="#feedback">Back to buckets</a></div></div>/* styles/components/feedback.css · state-panel */
/* Full-region states: error, connection lost, offline, maintenance, permission denied, not found. Each says what happened,
* what it means for the user's data, and the next action. Icon + heading carry the state; tone is a supporting cue only. */
.af-state { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); max-inline-size: 34rem; margin-inline: auto; padding: var(--space-12) var(--space-6); text-align: center; }
.af-state__icon { display: grid; place-items: center; inline-size: var(--space-16); block-size: var(--space-16); border-radius: var(--radius-lg); border: var(--border-width-hairline) solid var(--status-neutral-border); background: var(--status-neutral-bg); color: var(--status-neutral-solid); }
.af-state__icon > .af-icon { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); }
.af-state[data-status="danger"] .af-state__icon { background: var(--status-danger-bg); border-color: var(--status-danger-border); color: var(--status-danger-solid); }
.af-state[data-status="warning"] .af-state__icon { background: var(--status-warning-bg); border-color: var(--status-warning-border); color: var(--status-warning-solid); }
.af-state[data-status="info"] .af-state__icon { background: var(--status-info-bg); border-color: var(--status-info-border); color: var(--status-info-solid); }
.af-state__code { font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); letter-spacing: 0.04em; }
.af-state__title { margin: 0; font: var(--type-h3-weight) var(--type-h3-size)/var(--type-h3-line-height) var(--type-h3-family); letter-spacing: var(--type-h3-letter-spacing); }
.af-state__text { margin: 0; color: var(--color-muted-text); line-height: var(--type-body-line-height); }
.af-state__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin-block-start: var(--space-2); }
.af-state__detail { inline-size: 100%; margin-block-start: var(--space-2); text-align: start; }
/* Connection indicator: a compact live status for realtime streams (event log, upload queue). */
.af-connection { display: inline-flex; align-items: center; gap: var(--space-2); font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); color: var(--color-muted-text); }
.af-connection[data-state="online"] { color: var(--status-success-text); }
.af-connection[data-state="reconnecting"] { color: var(--status-warning-text); }
.af-connection[data-state="offline"] { color: var(--status-danger-text); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-muted-text--font-body--font-weight-medium--icon-size-lg--radius-lg--space-12--space-16--space-2--space-3--space-6--status-danger-bg--status-danger-border--status-danger-solid--status-danger-text--status-info-bg--status-info-border--status-info-solid--status-neutral-bg--status-neutral-border--status-neutral-solid--status-success-text--status-warning-bg--status-warning-border--status-warning-solid--status-warning-text--type-body-line-height--type-caption-size--type-h3-family--type-h3-letter-spacing--type-h3-line-height--type-h3-size--type-h3-weight--type-metadata-family--type-metadata-size--type-metadata-weight<span class="af-connection" data-state="online"><span class="af-pip" data-status="success" aria-hidden="true"></span>Live</span>
<span class="af-connection" data-state="reconnecting"><span class="af-pip" data-status="warning" aria-hidden="true"></span>Reconnecting…</span>
<span class="af-connection" data-state="offline"><span class="af-pip" data-status="danger" aria-hidden="true"></span>Offline</span>/* styles/components/feedback.css · state-panel */
/* Full-region states: error, connection lost, offline, maintenance, permission denied, not found. Each says what happened,
* what it means for the user's data, and the next action. Icon + heading carry the state; tone is a supporting cue only. */
.af-state { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); max-inline-size: 34rem; margin-inline: auto; padding: var(--space-12) var(--space-6); text-align: center; }
.af-state__icon { display: grid; place-items: center; inline-size: var(--space-16); block-size: var(--space-16); border-radius: var(--radius-lg); border: var(--border-width-hairline) solid var(--status-neutral-border); background: var(--status-neutral-bg); color: var(--status-neutral-solid); }
.af-state__icon > .af-icon { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); }
.af-state[data-status="danger"] .af-state__icon { background: var(--status-danger-bg); border-color: var(--status-danger-border); color: var(--status-danger-solid); }
.af-state[data-status="warning"] .af-state__icon { background: var(--status-warning-bg); border-color: var(--status-warning-border); color: var(--status-warning-solid); }
.af-state[data-status="info"] .af-state__icon { background: var(--status-info-bg); border-color: var(--status-info-border); color: var(--status-info-solid); }
.af-state__code { font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); letter-spacing: 0.04em; }
.af-state__title { margin: 0; font: var(--type-h3-weight) var(--type-h3-size)/var(--type-h3-line-height) var(--type-h3-family); letter-spacing: var(--type-h3-letter-spacing); }
.af-state__text { margin: 0; color: var(--color-muted-text); line-height: var(--type-body-line-height); }
.af-state__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin-block-start: var(--space-2); }
.af-state__detail { inline-size: 100%; margin-block-start: var(--space-2); text-align: start; }
/* Connection indicator: a compact live status for realtime streams (event log, upload queue). */
.af-connection { display: inline-flex; align-items: center; gap: var(--space-2); font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); color: var(--color-muted-text); }
.af-connection[data-state="online"] { color: var(--status-success-text); }
.af-connection[data-state="reconnecting"] { color: var(--status-warning-text); }
.af-connection[data-state="offline"] { color: var(--status-danger-text); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-muted-text--font-body--font-weight-medium--icon-size-lg--radius-lg--space-12--space-16--space-2--space-3--space-6--status-danger-bg--status-danger-border--status-danger-solid--status-danger-text--status-info-bg--status-info-border--status-info-solid--status-neutral-bg--status-neutral-border--status-neutral-solid--status-success-text--status-warning-bg--status-warning-border--status-warning-solid--status-warning-text--type-body-line-height--type-caption-size--type-h3-family--type-h3-letter-spacing--type-h3-line-height--type-h3-size--type-h3-weight--type-metadata-family--type-metadata-size--type-metadata-weightComponents and patterns
Twenty components that describe cloud storage as it actually is: a hierarchy of resources, each with a type, a name, a status and an environment. They are composed from the primitives above and add no new colours, radii, shadows or spacing.
Every value on this page is sample data
Names, sizes, regions, keys, timestamps and statuses are invented to show the components. Afara has no live service behind this documentation. In a real product, never invent infrastructure statistics, uptime or operational status.
<span class="af-sample"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-flask-conical"/></svg>Sample data</span>/* styles/patterns/infrastructure.css · sample-flag */
/* Marks demonstration data. Required on any card, chart or table that shows invented figures, and on every estimate that is not
* production billing data. Wording is fixed: "Sample data". */
.af-sample { display: inline-flex; align-items: center; gap: var(--space-1); block-size: var(--badge-height); padding-inline: var(--badge-padding-x); border: var(--border-width-hairline) dashed var(--color-border-control); border-radius: var(--badge-radius); color: var(--color-muted-text); font: var(--font-weight-medium) var(--type-caption-size)/1 var(--font-body); white-space: nowrap; }
.af-sample > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }Design tokens this component reads. Override a token, not the component.
--badge-height--badge-padding-x--badge-radius--border-width-hairline--color-border-control--color-muted-text--font-body--font-weight-medium--icon-size-xs--space-1--type-caption-sizeWhere am I? The path is always visible above a resource view: Organization, Project, Environment, Bucket, Object. Each level shows its type as well as its name, and the current level is marked in text weight and a rule, not colour.
<nav aria-label="Resource path"><ol class="af-resource-path">
<li class="af-resource-path__item" data-level="organization"><span class="af-resource-path__type">Organization</span><a class="af-resource-path__name" href="#infrastructure">Acme (sample)</a></li>
<li class="af-resource-path__item" data-level="project"><span class="af-resource-path__type">Project</span><a class="af-resource-path__name" href="#infrastructure">web-app</a></li>
<li class="af-resource-path__item" data-level="environment"><span class="af-resource-path__type">Environment</span><span class="af-env" data-env="production"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-server"/></svg>Production</span></li>
<li class="af-resource-path__item" data-level="bucket" aria-current="location"><span class="af-resource-path__type">Bucket</span><a class="af-resource-path__name" href="#infrastructure" aria-current="location">acme-assets-prod</a></li>
</ol></nav>/* styles/patterns/infrastructure.css · resource-path */
/* Where am I? Organization → Project → Environment → Bucket → Object, always visible above a resource view. Each level shows its
* TYPE (small label) and NAME; identifiers are monospaced; the environment is an .af-env badge; the current level is
* aria-current="location". Wraps on narrow screens; each name is a link to that level. */
.af-resource-path { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-1); margin: 0; padding: 0; list-style: none; }
.af-resource-path__item { display: flex; flex-direction: column; gap: var(--space-0-5); min-inline-size: 0; padding: var(--space-0-5) var(--space-2); border-radius: var(--radius-sm); }
.af-resource-path__item + .af-resource-path__item { position: relative; margin-inline-start: var(--space-4); }
.af-resource-path__item + .af-resource-path__item::before { content: ""; position: absolute; inset-inline-start: calc(-1 * var(--space-4)); inset-block: 0; inline-size: var(--space-4); margin-block: auto; block-size: var(--icon-size-xs); background: var(--color-border-control); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / var(--icon-size-xs) no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / var(--icon-size-xs) no-repeat; }
.af-resource-path__type { font: var(--type-eyebrow-weight) 0.625rem/1.1 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); }
.af-resource-path__name { font: var(--font-weight-medium) var(--type-body-sm-size)/1.3 var(--font-body); color: var(--color-text); text-decoration: none; overflow-wrap: anywhere; border-radius: var(--radius-xs); }
a.af-resource-path__name:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.af-resource-path__item[data-level="bucket"] .af-resource-path__name, .af-resource-path__item[data-level="object"] .af-resource-path__name { font-family: var(--font-mono); font-size: var(--type-metadata-size); }
.af-resource-path__item[aria-current="location"] { background: var(--color-accent-bg-strong); box-shadow: inset 0 -2px 0 var(--color-accent-solid); }
.af-resource-path__item[aria-current="location"] .af-resource-path__name { font-weight: var(--font-weight-semibold); }
/* styles/components/data-display.css · env-badge */
/* Environment identity — distinguished by FILL, BORDER STYLE and LABEL, never colour alone:
* production solid dark fill · staging tinted fill, solid border · development outline, dashed border */
.af-env { display: inline-flex; align-items: center; gap: var(--space-1-5); block-size: var(--badge-height); padding-inline: var(--badge-padding-x); border: var(--border-width-hairline) solid var(--env-development-border); border-radius: var(--radius-sm); background: var(--env-development-bg); color: var(--env-development-text); font: var(--font-weight-semibold) var(--type-eyebrow-size)/1 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; white-space: nowrap; border-style: dashed; }
.af-env[data-env="production"] { background: var(--env-production-bg); color: var(--env-production-text); border-color: var(--env-production-border); border-style: solid; }
.af-env[data-env="staging"] { background: var(--env-staging-bg); color: var(--env-staging-text); border-color: var(--env-staging-border); border-style: solid; }
.af-env > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }Design tokens this component reads. Override a token, not the component.
--color-accent-bg-strong--color-accent-solid--color-border-control--color-muted-text--color-text--font-body--font-mono--font-weight-medium--font-weight-semibold--icon-size-xs--radius-sm--radius-xs--space-0-5--space-1--space-2--space-4--type-body-sm-size--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-weight--type-metadata-size--badge-height--badge-padding-x--border-width-hairline--env-development-bg--env-development-border--env-development-text--env-production-bg--env-production-border--env-production-text--env-staging-bg--env-staging-border--env-staging-text--space-1-5--type-eyebrow-sizeBucket · Sample region A
Bucket · Sample region B
<article class="af-card af-card--interactive"><div class="af-card__body">
<div class="af-resource-card__head"><span class="af-resource-card__tile"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-database"/></svg></span><div><h4 class="af-resource-card__name"><a class="af-card__link" href="#infrastructure">acme-assets-prod</a></h4><p class="af-resource-card__type">Bucket · Sample region A</p></div></div>
<dl class="af-resource-card__facts"><div><dt>Objects</dt><dd>1,204</dd></div><div><dt>Stored</dt><dd>62.4 GB</dd></div><div><dt>Environment</dt><dd><span class="af-env" data-env="production">Production</span></dd></div><div><dt>Status</dt><dd><span class="af-status"><span class="af-pip" data-status="success" aria-hidden="true"></span>Active</span></dd></div></dl>
<div><span class="af-sample">Sample data</span></div>
</div></article>
<article class="af-card af-card--interactive"><div class="af-card__body">
<div class="af-resource-card__head"><span class="af-resource-card__tile"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-database"/></svg></span><div><h4 class="af-resource-card__name"><a class="af-card__link" href="#infrastructure">acme-assets-staging</a></h4><p class="af-resource-card__type">Bucket · Sample region B</p></div></div>
<dl class="af-resource-card__facts"><div><dt>Objects</dt><dd>212</dd></div><div><dt>Stored</dt><dd>3.1 GB</dd></div><div><dt>Environment</dt><dd><span class="af-env" data-env="staging">Staging</span></dd></div><div><dt>Status</dt><dd><span class="af-status"><span class="af-pip" data-status="pending" aria-hidden="true"></span>Provisioning</span></dd></div></dl>
<div><span class="af-sample">Sample data</span></div>
</div></article>/* styles/patterns/infrastructure.css · resource-card */
/* A bucket (or later: any resource) in a list of cards. Name is a stretched link; facts are a key-value grid; status and
* environment are always visible. */
.af-resource-card__head { display: flex; align-items: flex-start; gap: var(--space-3); }
.af-resource-card__tile { display: grid; place-items: center; flex: none; inline-size: var(--avatar-size-lg); block-size: var(--avatar-size-lg); border-radius: var(--radius-sm); background: var(--color-accent-bg-strong); color: var(--color-text); }
.af-resource-card__name { margin: 0; font: var(--font-weight-semibold) var(--type-body-size)/1.3 var(--font-mono); font-size: var(--type-code-size); overflow-wrap: anywhere; }
.af-resource-card__type { margin: var(--space-0-5) 0 0; font: var(--type-caption-weight) var(--type-caption-size)/1.3 var(--type-caption-family); color: var(--color-muted-text); }
.af-resource-card__facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3) var(--space-4); margin: 0; }
.af-resource-card__facts > div { display: flex; flex-direction: column; gap: var(--space-0-5); min-inline-size: 0; }
.af-resource-card__facts dt { font: var(--type-label-weight) var(--type-caption-size)/1.3 var(--type-label-family); color: var(--color-muted-text); }
.af-resource-card__facts dd { margin: 0; font: var(--font-weight-medium) var(--type-body-sm-size)/1.35 var(--font-body); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
/* styles/components/surfaces.css · card */
.af-card {
--_pad: var(--card-padding-md);
position: relative;
display: flex; flex-direction: column;
min-inline-size: 0;
background: var(--color-surface);
border: var(--border-width-hairline) solid var(--card-border);
border-radius: var(--card-radius);
color: var(--color-text);
}
.af-card--sm { --_pad: var(--card-padding-sm); }
.af-card--lg { --_pad: var(--card-padding-lg); }
.af-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); padding: var(--_pad) var(--_pad) 0; }
.af-card__header--divided { padding-block-end: var(--_pad); border-block-end: 1px solid var(--color-border); }
.af-card__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/var(--type-component-title-line-height) var(--type-component-title-family); letter-spacing: var(--type-component-title-letter-spacing); }
.af-card__subtitle { margin: 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); color: var(--color-muted-text); }
.af-card__actions { display: flex; align-items: center; gap: var(--space-2); flex: none; margin-inline-start: auto; }
.af-card__body { flex: 1; padding: var(--_pad); display: flex; flex-direction: column; gap: var(--card-gap); min-inline-size: 0; }
.af-card__header + .af-card__body { padding-block-start: var(--space-3); }
.af-card__header--divided + .af-card__body { padding-block-start: var(--_pad); }
.af-card__body--flush { padding: 0; }
.af-card__footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--_pad); border-block-start: 1px solid var(--color-border); background: var(--color-surface-inset); border-end-start-radius: calc(var(--card-radius) - var(--border-width-hairline)); border-end-end-radius: calc(var(--card-radius) - var(--border-width-hairline)); }
/* Variants */
.af-card--inset { background: var(--color-surface-inset); border-color: transparent; box-shadow: var(--shadow-inset-well); }
.af-card--elevated { border-color: var(--color-border-strong); box-shadow: var(--popover-shadow); background: var(--color-surface-raised); }
.af-card--dashed { border-style: dashed; border-color: var(--color-border-strong); background: transparent; }
.af-card--selected { border-color: var(--color-accent-solid); background: var(--color-surface-selected); box-shadow: inset 0 0 0 var(--border-width-hairline) var(--color-accent-solid); }
/* Interactive card: the whole card is one target via a stretched link/button inside the title (.af-card__link). Only ONE
* interactive element per card carries the stretch; secondary actions sit above it with position: relative. */
.af-card--interactive { transition: border-color var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard); }
.af-card__link { color: inherit; text-decoration: none; }
.af-card__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.af-card__link:focus-visible { outline: none; }
.af-card--interactive:has(.af-card__link:focus-visible) { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); }
@media (hover: hover) { .af-card--interactive:hover { border-color: var(--card-border-hover); box-shadow: var(--shadow-card-hover); } }
.af-card--interactive:active { box-shadow: none; }
.af-card--interactive .af-card__actions, .af-card--interactive .af-button:not(.af-card__link) { position: relative; z-index: 1; }Design tokens this component reads. Override a token, not the component.
--avatar-size-lg--color-accent-bg-strong--color-muted-text--color-text--font-body--font-mono--font-weight-medium--font-weight-semibold--radius-sm--space-0-5--space-3--space-4--type-body-size--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--type-code-size--type-label-family--type-label-weight--border-width-hairline--card-border--card-border-hover--card-gap--card-padding-lg--card-padding-md--card-padding-sm--card-radius--color-accent-solid--color-border--color-border-strong--color-surface--color-surface-inset--color-surface-raised--color-surface-selected--ease-standard--focus-ring-color--focus-ring-offset--focus-ring-width--motion-fast--popover-shadow--shadow-card-hover--shadow-inset-well--space-2--type-body-sm-line-height--type-component-title-family--type-component-title-letter-spacing--type-component-title-line-height--type-component-title-size--type-component-title-weightA closed set. Do not invent statuses. Each has its own shape and a word; “since” is shown when known.
<span class="af-status"><span class="af-pip" data-status="success" aria-hidden="true"></span>Active <span class="af-status__since">since 2026-09-12</span></span>
<span class="af-status"><span class="af-pip" data-status="pending" aria-hidden="true"></span>Provisioning</span>
<span class="af-status"><span class="af-pip" data-status="warning" aria-hidden="true"></span>Degraded</span>
<span class="af-status"><span class="af-pip" data-status="danger" aria-hidden="true"></span>Failed</span>
<span class="af-status"><span class="af-pip" data-status="neutral" aria-hidden="true"></span>Suspended</span>
<span class="af-status"><span class="af-pip" data-status="info" aria-hidden="true"></span>Deleting</span>/* styles/patterns/infrastructure.css · resource-status */
/* Resource status indicator: pip (distinct shape) + word, optionally a detail and "since" time. Statuses are a closed set —
* do not invent new ones: active · provisioning · degraded · failed · suspended · deleting. */
.af-status { display: inline-flex; align-items: center; gap: var(--space-2); font: var(--font-weight-medium) var(--type-body-sm-size)/1.3 var(--font-body); color: var(--color-text); }
.af-status__since { font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); font-weight: var(--font-weight-regular); }
.af-status-panel { display: flex; flex-direction: column; gap: var(--space-3); }
.af-status-panel__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); }
.af-status-panel__checks { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); }
.af-status-panel__checks > li { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-4); font-size: var(--type-body-sm-size); }
.af-status-panel__checks > li + li { border-block-start: 1px solid var(--color-border); }
.af-status-panel__checks > li > time { margin-inline-start: auto; font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }
/* styles/components/data-display.css · status-pip */
/* The status indicator. Each status has its own SHAPE so it is readable without colour:
* success ● filled circle warning ▲ triangle danger ◆ diamond info ○ ring neutral ▬ bar pending ◐ half circle
* Pair it with a visible label (or an aria-label if the label is truly absent). */
.af-pip { --_c: var(--status-neutral-solid); display: inline-block; flex: none; inline-size: var(--pip-size); block-size: var(--pip-size); background: var(--_c); border-radius: 50%; }
.af-badge .af-pip { --_c: var(--_pip); }
/* shape + colour per status; inside a badge the tone class selects the same shape automatically */
.af-pip[data-status="success"], .af-badge--success .af-pip { --_c: var(--status-success-solid); }
.af-pip[data-status="warning"], .af-badge--warning .af-pip { --_c: var(--status-warning-solid); border-radius: 0; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.af-pip[data-status="danger"], .af-badge--danger .af-pip { --_c: var(--status-danger-solid); border-radius: 0; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.af-pip[data-status="info"], .af-badge--info .af-pip { --_c: var(--status-info-solid); background: transparent; box-shadow: inset 0 0 0 2px var(--_c); }
.af-pip[data-status="neutral"], .af-badge:not([class*="--"]) .af-pip { --_c: var(--status-neutral-solid); border-radius: var(--radius-full); block-size: calc(var(--pip-size) / 3); }
.af-pip[data-status="pending"] { --_c: var(--status-info-solid); background: linear-gradient(90deg, var(--_c) 50%, transparent 50%); box-shadow: inset 0 0 0 1.5px var(--_c); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-border--color-muted-text--color-surface--color-text--font-body--font-weight-medium--font-weight-regular--space-2--space-3--space-4--type-body-sm-size--type-metadata-family--type-metadata-size--type-metadata-weight--pip-size--radius-full--status-danger-solid--status-info-solid--status-neutral-solid--status-success-solid--status-warning-solid<div class="af-status-panel"><div class="af-status-panel__head"><span class="af-status"><span class="af-pip" data-status="warning" aria-hidden="true"></span>Degraded</span><span class="af-sample">Sample data</span></div>
<ul class="af-status-panel__checks"><li><span class="af-pip" data-status="success" aria-hidden="true"></span>Reads<time datetime="2026-11-05T09:00:00Z">09:00 UTC</time></li><li><span class="af-pip" data-status="warning" aria-hidden="true"></span>Writes (elevated latency)<time datetime="2026-11-05T09:00:00Z">09:00 UTC</time></li></ul></div>/* styles/patterns/infrastructure.css · resource-status */
/* Resource status indicator: pip (distinct shape) + word, optionally a detail and "since" time. Statuses are a closed set —
* do not invent new ones: active · provisioning · degraded · failed · suspended · deleting. */
.af-status { display: inline-flex; align-items: center; gap: var(--space-2); font: var(--font-weight-medium) var(--type-body-sm-size)/1.3 var(--font-body); color: var(--color-text); }
.af-status__since { font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); font-weight: var(--font-weight-regular); }
.af-status-panel { display: flex; flex-direction: column; gap: var(--space-3); }
.af-status-panel__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); }
.af-status-panel__checks { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); }
.af-status-panel__checks > li { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-4); font-size: var(--type-body-sm-size); }
.af-status-panel__checks > li + li { border-block-start: 1px solid var(--color-border); }
.af-status-panel__checks > li > time { margin-inline-start: auto; font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-border--color-muted-text--color-surface--color-text--font-body--font-weight-medium--font-weight-regular--space-2--space-3--space-4--type-body-sm-size--type-metadata-family--type-metadata-size--type-metadata-weightNo buckets match.
<div data-switcher="bucket">
<button type="button" class="af-switcher" popovertarget="in-bsel" aria-haspopup="listbox" aria-expanded="false"><span class="af-switcher__tile">A</span><span class="af-switcher__text"><span class="af-switcher__type">Bucket</span><span class="af-switcher__name">acme-assets-prod</span></span><svg class="af-icon af-switcher__chevron" aria-hidden="true" focusable="false"><use href="#i-chevrons-up-down"/></svg></button>
<div class="af-popover af-popover--flush af-switcher__panel" id="in-bsel" popover data-placement="bottom-start">
<div class="af-switcher__search"><input class="af-input af-input--sm" data-switcher-filter aria-label="Filter buckets" placeholder="Filter buckets" autocomplete="off"></div>
<ul class="af-switcher__list" role="listbox" aria-label="Buckets">
<li class="af-option" role="option" aria-selected="true" tabindex="0" data-value="acme-assets-prod" data-initial="A" data-label="acme-assets-prod"><span class="af-bucket-option"><span class="af-bucket-option__name">acme-assets-prod</span><span class="af-bucket-option__meta"><span>Sample region A</span><span>Private</span></span></span></li>
<li class="af-option" role="option" aria-selected="false" tabindex="-1" data-value="acme-assets-staging" data-initial="A" data-label="acme-assets-staging"><span class="af-bucket-option"><span class="af-bucket-option__name">acme-assets-staging</span><span class="af-bucket-option__meta"><span>Sample region B</span><span>Private</span></span></span></li>
<li class="af-option" role="option" aria-selected="false" tabindex="-1" data-value="acme-public-site" data-initial="A" data-label="acme-public-site"><span class="af-bucket-option"><span class="af-bucket-option__name">acme-public-site</span><span class="af-bucket-option__meta"><span>Sample region A</span><span>Public read</span></span></span></li>
</ul>
<p class="af-listbox__empty" data-switcher-empty hidden>No buckets match.</p>
</div>
</div>/* styles/patterns/infrastructure.css · bucket-selector */
/* Switcher (navigation.css) specialised for buckets: option = name (mono) + region + visibility. Choosing a bucket changes the
* resource path; the new bucket is announced via the page's live region. */
.af-bucket-option { display: flex; flex-direction: column; gap: var(--space-0-5); min-inline-size: 0; flex: 1; }
.af-bucket-option__name { font: var(--font-weight-medium) var(--type-metadata-size)/1.3 var(--font-mono); overflow-wrap: anywhere; }
.af-bucket-option__meta { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2); font: var(--type-caption-weight) var(--type-caption-size)/1.3 var(--type-caption-family); color: var(--color-muted-text); }
.af-option:has(.af-bucket-option) { align-items: flex-start; padding-block: var(--space-2); }
/* styles/components/navigation.css · switcher */
/* Organization / project / environment / resource switcher. A button that shows the CURRENT resource and its type, opening a
* popover listbox with search and a create action. Anatomy: [tile] [type label + name] [chevrons]. */
.af-switcher {
display: flex; align-items: center; gap: var(--space-2);
min-block-size: var(--button-height-md); max-inline-size: 100%; padding: var(--space-1) var(--space-2);
border: var(--border-width-hairline) solid transparent; border-radius: var(--radius-sm);
color: var(--color-text); text-align: start;
transition: background-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard);
}
@media (hover: hover) { .af-switcher:hover { background: var(--color-surface-hover); border-color: var(--color-border); } }
.af-switcher[aria-expanded="true"] { background: var(--color-surface-hover); border-color: var(--color-border-strong); }
.af-switcher__tile { display: grid; place-items: center; flex: none; inline-size: var(--avatar-size-md); block-size: var(--avatar-size-md); border-radius: var(--radius-sm); background: var(--action-primary-bg); color: var(--action-primary-text); font: var(--font-weight-semibold) var(--type-label-size)/1 var(--font-display); text-transform: uppercase; }
.af-switcher__tile--outline { background: var(--color-surface-inset); color: var(--color-text); border: var(--border-width-hairline) solid var(--color-border-strong); }
.af-switcher__text { display: flex; flex-direction: column; min-inline-size: 0; line-height: 1.2; }
.af-switcher__type { font: var(--type-eyebrow-weight) 0.6875rem/1.2 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); }
.af-switcher__name { font: var(--font-weight-semibold) var(--type-body-sm-size)/1.3 var(--font-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.af-switcher__chevron { margin-inline-start: auto; color: var(--color-muted-text); inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }
.af-switcher--compact { min-block-size: var(--button-height-sm); }
.af-switcher--compact .af-switcher__text { flex-direction: row; align-items: baseline; gap: var(--space-2); }
.af-switcher__panel { inline-size: 18rem; max-inline-size: calc(100vw - 2 * var(--space-4)); }
.af-switcher__search { padding: var(--space-2); border-block-end: 1px solid var(--color-border); }
.af-switcher__list { max-block-size: 15rem; overflow-y: auto; padding: var(--space-1); margin: 0; list-style: none; }
.af-switcher__foot { padding: var(--space-1); border-block-start: 1px solid var(--color-border); }Design tokens this component reads. Override a token, not the component.
--color-muted-text--font-mono--font-weight-medium--space-0-5--space-1--space-2--type-caption-family--type-caption-size--type-caption-weight--type-metadata-size--action-primary-bg--action-primary-text--avatar-size-md--border-width-hairline--button-height-md--button-height-sm--color-border--color-border-strong--color-surface-hover--color-surface-inset--color-text--ease-standard--font-body--font-display--font-weight-semibold--icon-size-sm--motion-fast--radius-sm--space-4--type-body-sm-size--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-weight--type-label-sizeA region that is not available yet is shown disabled with the reason, never hidden and never selectable. Selection is shown by border, fill and the native checked state.
<fieldset class="af-choice-group"><legend class="af-fieldset__legend">Region</legend>
<label class="af-choice"><input class="af-choice__input af-check__input af-radio__input" type="radio" name="in-reg" checked><span class="af-choice__title">Sample region A</span><span class="af-choice__code">sample-a-1</span><span class="af-choice__desc">Lowest latency for sample users.</span></label>
<label class="af-choice"><input class="af-choice__input af-check__input af-radio__input" type="radio" name="in-reg"><span class="af-choice__title">Sample region B</span><span class="af-choice__code">sample-b-1</span><span class="af-choice__desc">A second location for sample redundancy.</span></label>
<label class="af-choice"><input class="af-choice__input af-check__input af-radio__input" type="radio" name="in-reg" disabled><span class="af-choice__title">Sample region C</span><span class="af-choice__code">sample-c-1</span><span class="af-choice__desc">Not available yet.</span><span class="af-choice__foot"><span class="af-badge af-badge--outline">Planned</span></span></label>
</fieldset>/* styles/patterns/infrastructure.css · region-selector */
/* Choose one region. Native radio inputs in selectable cards (.af-choice). A region that is not yet available is shown disabled
* with the reason ("Planned"), never hidden and never selectable. Selection is shown by border, tick mark AND checked state. */
.af-choice-group { display: grid; gap: var(--space-3); grid-template-columns: minmax(0, 1fr); border: 0; padding: 0; margin: 0; min-inline-size: 0; }
@media (min-width: 640px) { .af-choice-group { grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); } }
.af-choice { position: relative; display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-4); border: var(--border-width-hairline) solid var(--color-border-control); border-radius: var(--card-radius); background: var(--color-surface); cursor: pointer; transition: border-color var(--motion-fast) var(--ease-standard), background-color var(--motion-fast) var(--ease-standard); }
.af-choice__input { position: absolute; inset-block-start: var(--space-4); inset-inline-end: var(--space-4); }
.af-choice__title { font: var(--font-weight-semibold) var(--type-body-size)/1.3 var(--font-body); padding-inline-end: var(--space-8); }
.af-choice__code { font: var(--type-metadata-weight) var(--type-metadata-size)/1.4 var(--type-metadata-family); color: var(--color-muted-text); }
.af-choice__desc { margin-block-start: var(--space-1); font: var(--type-caption-weight) var(--type-caption-size)/1.5 var(--type-caption-family); color: var(--color-muted-text); }
.af-choice__foot { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-block-start: var(--space-2); }
@media (hover: hover) { .af-choice:hover:not(:has(input:disabled)) { border-color: var(--color-muted-text); } }
.af-choice:has(input:checked) { border-color: var(--color-accent-solid); background: var(--color-surface-selected); box-shadow: inset 0 0 0 var(--border-width-hairline) var(--color-accent-solid); }
.af-choice:has(input:focus-visible) { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); }
.af-choice:has(input:disabled) { border-style: dashed; border-color: var(--action-disabled-border); background: var(--color-surface-disabled); color: var(--color-disabled-text); cursor: not-allowed; }
.af-choice:has(input:disabled) :is(.af-choice__code, .af-choice__desc) { color: var(--color-disabled-text); }Design tokens this component reads. Override a token, not the component.
--action-disabled-border--border-width-hairline--card-radius--color-accent-solid--color-border-control--color-disabled-text--color-muted-text--color-surface--color-surface-disabled--color-surface-selected--ease-standard--focus-ring-color--focus-ring-offset--focus-ring-width--font-body--font-weight-semibold--motion-fast--space-1--space-2--space-3--space-4--space-8--type-body-size--type-caption-family--type-caption-size--type-caption-weight--type-metadata-family--type-metadata-size--type-metadata-weightThe explorer is a toolbar, a path, a table and a footer. Folders sort before objects. Access (private or public) is visible in every row, and names wrap so an extension is never cut off.
| Name | Size | Modified | Access |
|---|---|---|---|
| 2026/ | — | — | Private |
| logo.png | 18.4 kB | 2026-11-02 | Public |
| backup-2026-10.zip | 48.2 MB | 2026-10-30 | Private |
<div class="af-explorer">
<div class="af-explorer__toolbar"><div class="af-search"><svg class="af-icon af-search__icon" aria-hidden="true" focusable="false"><use href="#i-search"/></svg><input class="af-input" type="search" placeholder="Search in this folder" aria-label="Search in this folder"></div><button class="af-button af-button--primary af-button--md" type="button" data-toast="info" data-toast-title="Sample: the upload dialog would open here"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload</button></div>
<nav class="af-explorer__path" aria-label="Object path"><ol class="af-breadcrumbs af-breadcrumbs--mono"><li><a href="#infrastructure">acme-assets-prod</a></li><li><a href="#infrastructure" aria-current="location">images</a></li></ol></nav>
<div class="af-table-wrap" role="region" aria-label="Objects in images" tabindex="0"><table class="af-table">
<caption class="af-visually-hidden">Objects in images (sample)</caption>
<thead><tr><th scope="col">Name</th><th scope="col" class="af-table__num">Size</th><th scope="col">Modified</th><th scope="col">Access</th></tr></thead>
<tbody>
<tr><th scope="row"><span class="af-cluster af-cluster--3"><span class="af-file-icon" data-type="folder"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-folder"/></svg></span><span class="af-file-row__name">2026/</span></span></th><td class="af-table__num">—</td><td>—</td><td><span class="af-badge af-access" data-access="private">Private</span></td></tr>
<tr><th scope="row"><span class="af-cluster af-cluster--3"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-image"/></svg></span><span class="af-file-row__name">logo.png</span></span></th><td class="af-table__num">18.4 kB</td><td>2026-11-02</td><td><span class="af-badge af-access" data-access="public"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-globe"/></svg>Public</span></td></tr>
<tr><th scope="row"><span class="af-cluster af-cluster--3"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-archive"/></svg></span><span class="af-file-row__name">backup-2026-10.zip</span></span></th><td class="af-table__num">48.2 MB</td><td>2026-10-30</td><td><span class="af-badge af-access" data-access="private">Private</span></td></tr>
</tbody></table></div>
<div class="af-table-foot"><span>3 items (sample)</span></div>
</div>/* styles/patterns/infrastructure.css · object-explorer */
/* The file browser: toolbar (path + search + view + upload), listing, bulk bar, footer pagination. The listing is a table (see
* data-display.css) whose first column is a file row. Folders sort before objects. Public/private is visible in every row. */
.af-explorer { display: flex; flex-direction: column; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); min-inline-size: 0; overflow: hidden; }
.af-explorer__toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); padding: var(--space-3) var(--space-4); border-block-end: 1px solid var(--color-border); }
.af-explorer__toolbar .af-search { flex: 1 1 14rem; max-inline-size: 22rem; }
.af-explorer__path { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); padding: var(--space-2) var(--space-4); border-block-end: 1px solid var(--color-border); background: var(--color-surface-inset); }
.af-explorer .af-table-wrap { border: 0; border-radius: 0; }
.af-explorer .af-table-foot { border-block-start: 1px solid var(--table-border); }
/* styles/patterns/infrastructure.css · file-row */
/* One object or folder: icon tile (shape tells the type — never a rainbow of colours), name, type, size, modified, access,
* actions. Name wraps (never truncate away the extension). The access badge is always present. */
.af-file-icon { display: grid; place-items: center; flex: none; inline-size: var(--space-8); block-size: var(--space-8); border: var(--border-width-hairline) solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface-inset); color: var(--color-muted-text); }
.af-file-icon > .af-icon { inline-size: var(--icon-size-md); block-size: var(--icon-size-md); }
.af-file-icon[data-type="folder"] { background: var(--color-accent-bg-strong); border-color: transparent; color: var(--color-text); }
.af-file-icon--lg { inline-size: var(--space-16); block-size: var(--space-16); border-radius: var(--radius-md); }
.af-file-icon--lg > .af-icon { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); }
.af-access { --_bg: var(--status-neutral-bg); --_text: var(--status-neutral-text); --_border: var(--status-neutral-border); }
.af-access[data-access="public"] { --_bg: var(--status-warning-bg); --_text: var(--status-warning-text); --_border: var(--status-warning-border); }
.af-file-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); }
.af-file-row + .af-file-row { border-block-start: 1px solid var(--color-border); }
.af-file-row__name { font: var(--font-weight-medium) var(--type-body-sm-size)/1.35 var(--font-body); overflow-wrap: anywhere; }
.af-file-row__meta { display: flex; flex-wrap: wrap; gap: var(--space-0-5) var(--space-3); font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
/* styles/components/data-display.css · table */
/* A real <table>. The scroll wrapper is focusable (role="region" + aria-label + tabindex="0") so keyboard users can scroll it.
* Density: default 48px · --compact 40px · --dense 32px rows. Numeric columns: .af-table__num (right-aligned, tabular).
* Sorting: <th aria-sort> with a <button class="af-table__sort"> inside. Selection: a checkbox column; selected rows carry
* aria-selected="true" and are announced by the checkbox, not by colour. */
.af-table-wrap { position: relative; overflow-x: auto; border: var(--border-width-hairline) solid var(--table-border); border-radius: var(--card-radius); background: var(--color-surface); }
.af-table-wrap:focus-visible { outline-offset: 0; }
.af-table { inline-size: 100%; min-inline-size: var(--table-min, 40rem); border-collapse: separate; border-spacing: 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line-height); text-align: start; --_row: var(--table-row-height-comfortable); }
.af-table--fit { min-inline-size: 0; }
.af-table--compact { --_row: var(--table-row-height-compact); }
.af-table--dense { --_row: var(--table-row-height-dense); font-size: var(--type-caption-size); }
.af-table caption { padding: var(--space-3) var(--table-cell-padding-x); text-align: start; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); caption-side: top; }
.af-table th, .af-table td { block-size: var(--_row); padding: 0 var(--table-cell-padding-x); border-block-end: 1px solid var(--table-border); text-align: start; vertical-align: middle; }
.af-table tbody tr:last-child > :is(td, th) { border-block-end: 0; }
.af-table thead th {
block-size: var(--table-header-height); background: var(--color-surface-inset);
font: var(--type-label-weight) var(--type-caption-size)/1.2 var(--type-label-family); letter-spacing: 0.02em; color: var(--color-muted-text); white-space: nowrap;
}
.af-table--sticky thead th { position: sticky; inset-block-start: 0; z-index: 1; }
.af-table tbody th[scope="row"] { font-weight: var(--font-weight-medium); }
.af-table tbody tr { transition: background-color var(--motion-fast) var(--ease-standard); }
@media (hover: hover) { .af-table tbody tr:hover { background: var(--color-surface-hover); } }
.af-table tbody tr[aria-selected="true"] { background: var(--color-surface-selected); }
.af-table tbody tr[aria-selected="true"] > :first-child { box-shadow: inset 3px 0 0 var(--color-accent-solid); }
.af-table__num { text-align: end !important; font-variant-numeric: tabular-nums; white-space: nowrap; }
.af-table__mono { font-family: var(--font-mono); font-size: var(--type-metadata-size); font-variant-ligatures: none; }
.af-table__select { inline-size: var(--space-12); padding-inline-end: 0; }
.af-table__actions { inline-size: 1%; white-space: nowrap; text-align: end !important; }
.af-table__primary { display: flex; align-items: center; gap: var(--space-3); min-inline-size: 0; }
.af-table__name { font-weight: var(--font-weight-medium); color: var(--color-text); overflow-wrap: anywhere; }
.af-table__sub { display: block; font: var(--type-caption-weight) var(--type-caption-size)/1.3 var(--type-caption-family); color: var(--color-muted-text); }
.af-table__sort { display: inline-flex; align-items: center; gap: var(--space-1); margin-inline: calc(-1 * var(--space-1)); padding: var(--space-0-5) var(--space-1); border-radius: var(--radius-sm); font: inherit; letter-spacing: inherit; color: inherit; }
.af-table__sort > .af-icon { inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); opacity: 0.55; }
@media (hover: hover) { .af-table__sort:hover { color: var(--color-text); background: var(--color-surface-hover); } }
th[aria-sort="ascending"] .af-table__sort, th[aria-sort="descending"] .af-table__sort { color: var(--color-text); }
th[aria-sort="ascending"] .af-table__sort > .af-icon, th[aria-sort="descending"] .af-table__sort > .af-icon { opacity: 1; color: var(--color-accent-text); }
.af-table-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--table-cell-padding-x); border-block-start: 1px solid var(--table-border); background: var(--color-surface); }
.af-table-bulkbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--table-cell-padding-x); background: var(--color-accent-bg-strong); border-block-end: 1px solid var(--color-border-strong); font-size: var(--type-body-sm-size); font-weight: var(--font-weight-medium); }
/* Stacked (mobile): below 640px a table with .af-table--stack becomes a list of labelled cards. Every <td> needs data-label. */
@media (max-width: 639.98px) {
.af-table--stack { min-inline-size: 0; }
.af-table--stack thead { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
.af-table--stack, .af-table--stack tbody, .af-table--stack tr, .af-table--stack td, .af-table--stack th[scope="row"] { display: block; }
.af-table--stack tr { padding: var(--space-3) var(--table-cell-padding-x); border-block-end: 1px solid var(--table-border); }
.af-table--stack tbody tr:last-child { border-block-end: 0; }
.af-table--stack td, .af-table--stack th[scope="row"] { block-size: auto; padding: var(--space-1) 0; border: 0; display: flex; justify-content: space-between; gap: var(--space-4); text-align: end; }
.af-table--stack td::before { content: attr(data-label); flex: none; color: var(--color-muted-text); font-weight: var(--font-weight-medium); text-align: start; }
.af-table--stack td.af-table__select, .af-table--stack td.af-table__actions { justify-content: flex-end; inline-size: auto; }
.af-table--stack td.af-table__select::before, .af-table--stack td.af-table__actions::before { content: none; }
}Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-border--color-surface--color-surface-inset--space-1--space-2--space-3--space-4--table-border--color-accent-bg-strong--color-muted-text--color-text--font-body--font-weight-medium--icon-size-lg--icon-size-md--radius-md--radius-sm--space-0-5--space-16--space-8--status-neutral-bg--status-neutral-border--status-neutral-text--status-warning-bg--status-warning-border--status-warning-text--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--color-accent-solid--color-accent-text--color-border-strong--color-surface-hover--color-surface-selected--ease-standard--font-mono--icon-size-xs--motion-fast--space-12--table-cell-padding-x--table-header-height--table-min--table-row-height-comfortable--table-row-height-compact--table-row-height-dense--type-body-sm-line-height--type-component-title-family--type-component-title-size--type-component-title-weight--type-label-family--type-label-weight--type-metadata-size<div class="af-file-meta">
<div class="af-file-meta__preview"><span class="af-file-icon af-file-icon--lg"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-image"/></svg></span></div>
<div><h4 class="af-file-meta__name">logo.png</h4><span class="af-badge af-access" data-access="public"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-globe"/></svg>Public</span></div>
<section class="af-file-meta__section"><h5 class="af-file-meta__heading">Metadata</h5><dl class="af-meta"><div class="af-meta__row"><dt>Content type</dt><dd>image/png</dd></div><div class="af-meta__row"><dt>Size</dt><dd>18,400 B</dd></div><div class="af-meta__row"><dt>Modified</dt><dd>2026-11-02 (sample)</dd></div></dl></section>
<section class="af-file-meta__section"><h5 class="af-file-meta__heading">Public URL (sample)</h5><div class="af-input-group af-copyable"><input class="af-input" id="in-url" value="https://assets.example.test/logo.png" readonly aria-label="Public URL"><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-copy-target="#in-url" data-copy-label="URL" aria-label="Copy URL"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button></div></section>
</div>/* styles/patterns/infrastructure.css · file-metadata */
/* Inspector content for one selected object: preview tile, name, access, public/signed URL controls, metadata viewer, actions.
* Shown in the right-side inspector (overlays.css) or below the list on small screens. */
.af-file-meta { display: flex; flex-direction: column; gap: var(--space-5); }
.af-file-meta__preview { display: grid; place-items: center; min-block-size: 10rem; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); color: var(--color-muted-text); }
.af-file-meta__name { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.35 var(--type-component-title-family); overflow-wrap: anywhere; }
.af-file-meta__section { display: flex; flex-direction: column; gap: var(--space-2); }
.af-file-meta__heading { margin: 0; font: var(--type-eyebrow-weight) var(--type-eyebrow-size)/1.4 var(--type-eyebrow-family); letter-spacing: var(--type-eyebrow-letter-spacing); text-transform: uppercase; color: var(--color-muted-text); }
/* styles/components/data-display.css · metadata-viewer */
/* Object metadata as ruled key/value rows in monospace — content type, size, ETag, checksum, headers. */
.af-meta { margin: 0; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }
.af-meta__row { display: grid; grid-template-columns: minmax(6rem, 9rem) minmax(0, 1fr); gap: var(--space-3); align-items: baseline; padding: var(--space-2) var(--space-4); }
.af-meta__row + .af-meta__row { border-block-start: 1px solid var(--color-border); }
.af-meta dt { font: var(--type-label-weight) var(--type-caption-size)/1.4 var(--type-label-family); color: var(--color-muted-text); }
.af-meta dd { margin: 0; font: var(--type-metadata-weight) var(--type-metadata-size)/1.5 var(--type-metadata-family); font-variant-ligatures: none; overflow-wrap: anywhere; display: flex; align-items: flex-start; gap: var(--space-2); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-muted-text--color-surface-inset--shadow-inset-well--space-2--space-5--type-component-title-family--type-component-title-size--type-component-title-weight--type-eyebrow-family--type-eyebrow-letter-spacing--type-eyebrow-size--type-eyebrow-weight--color-border--color-surface--space-3--space-4--type-caption-size--type-label-family--type-label-weight--type-metadata-family--type-metadata-size--type-metadata-weightEstimate: projected from the last 7 days of sample growth. Decimal units (1 GB = 1,000,000,000 B).
Total of GET and PUT. Sample data.
As of 2026-11-05 09:00 UTC (sample)<div class="af-card"><div class="af-card__header"><h4 class="af-card__title">Storage capacity</h4><span class="af-sample">Sample data</span></div><div class="af-card__body"><div class="af-capacity">
<div class="af-meter" data-level="warning"><div class="af-meter__head"><span class="af-meter__label" id="in-cap">Stored</span><span class="af-meter__figures"><strong>86 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="in-cap" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86 GB of 100 GB used, 86%"><div class="af-meter__fill" style="--value: 86%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg>Approaching limit</span><span>Warns at 80%</span></div></div>
<dl class="af-capacity__stats"><div><dt>Used</dt><dd>86 GB</dd></div><div><dt>Remaining</dt><dd>14 GB</dd></div><div><dt>Plan limit</dt><dd>100 GB</dd></div></dl>
<p class="af-capacity__note"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg>Estimate: projected from the last 7 days of sample growth. Decimal units (1 GB = 1,000,000,000 B).</p>
</div></div></div>
<div class="af-card"><div class="af-card__body"><div class="af-metric"><span class="af-metric__label">Requests, last 24 hours</span><span class="af-metric__value">48,210</span><p class="af-metric__helper">Total of GET and PUT. Sample data.</p><span class="af-infra-metric__asof"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-clock"/></svg>As of 2026-11-05 09:00 UTC (sample)</span></div></div></div>/* styles/patterns/infrastructure.css · storage-capacity */
/* Capacity card: the usage meter (with threshold and plan limit), three comparable figures, and an ESTIMATE clearly labelled as
* such with how it is computed. Used vs. limit always share a unit. */
.af-capacity { display: flex; flex-direction: column; gap: var(--space-4); }
.af-capacity__stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: var(--space-4); margin: 0; padding-block-start: var(--space-4); border-block-start: 1px solid var(--color-border); }
.af-capacity__stats > div { display: flex; flex-direction: column; gap: var(--space-0-5); }
.af-capacity__stats dt { font: var(--type-label-weight) var(--type-caption-size)/1.3 var(--type-label-family); color: var(--color-muted-text); }
.af-capacity__stats dd { margin: 0; font: var(--font-weight-semibold) var(--type-body-size)/1.3 var(--font-body); font-variant-numeric: tabular-nums; }
.af-capacity__note { margin: 0; display: flex; gap: var(--space-2); font: var(--type-caption-weight) var(--type-caption-size)/1.5 var(--type-caption-family); color: var(--color-muted-text); }
.af-capacity__note > .af-icon { margin-block-start: 1px; inline-size: var(--icon-size-xs); block-size: var(--icon-size-xs); }
/* styles/components/data-display.css · usage-meter */
/* How much of a limit is used. A recessed track with ruler ticks every 10%, a fill whose colour follows data-level, a
* threshold marker (where warnings begin) and a LABEL that says the level in words. role="meter" with aria-valuetext gives
* screen readers "62.4 GB of 100 GB used, 62%". Custom properties --value and --threshold are the ONLY inline styles allowed
* (data, not design): --value: 62.4%; --threshold: 80%.
* data-level: ok (accent) · warning (≥ threshold) · danger (≥ limit) data-kind="estimate" hatches the fill. */
.af-meter { --_fill: var(--color-accent-solid); display: flex; flex-direction: column; gap: var(--space-2); min-inline-size: 0; }
.af-meter[data-level="warning"] { --_fill: var(--status-warning-solid); }
.af-meter[data-level="danger"] { --_fill: var(--status-danger-solid); }
.af-meter__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-1) var(--space-3); }
.af-meter__label { font: var(--type-label-weight) var(--type-label-size)/1.3 var(--type-label-family); }
.af-meter__figures { font: var(--font-weight-regular) var(--type-body-sm-size)/1.3 var(--font-body); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
.af-meter__figures strong { color: var(--color-text); font-weight: var(--font-weight-semibold); }
.af-meter__track { position: relative; block-size: var(--meter-height); border-radius: var(--meter-radius); background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); overflow: hidden; }
.af-meter--lg .af-meter__track { block-size: var(--meter-height-lg); }
.af-meter__fill { position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: var(--value, 0%); max-inline-size: 100%; min-inline-size: var(--meter-height); border-radius: var(--meter-radius); background: var(--_fill); transition: inline-size var(--motion-emphasis) var(--ease-standard); }
.af-meter[data-kind="estimate"] .af-meter__fill { background: repeating-linear-gradient(135deg, var(--_fill) 0 4px, color-mix(in srgb, var(--_fill) 55%, var(--color-surface-inset)) 4px 8px); }
.af-meter__track::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(90deg, transparent 0 calc(10% - 1px), var(--color-surface) calc(10% - 1px) 10%); opacity: 0.85; }
.af-meter__threshold { position: absolute; inset-block: -2px; inset-inline-start: var(--threshold, 80%); inline-size: 2px; background: var(--color-text); z-index: 1; border-radius: 1px; }
.af-meter__foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-1) var(--space-3); font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-meter__state { display: inline-flex; align-items: center; gap: var(--space-1-5); font-weight: var(--font-weight-medium); color: var(--color-muted-text); }
.af-meter[data-level="warning"] .af-meter__state { color: var(--status-warning-text); }
.af-meter[data-level="danger"] .af-meter__state { color: var(--status-danger-text); }
/* Stacked usage breakdown (parts of one whole). Series are told apart by colour AND by texture (see data-series), and by the
* legend that lists names and values. 2px surface gaps separate segments; no borders. */
.af-breakdown { display: flex; gap: 2px; block-size: var(--meter-height-lg); border-radius: var(--meter-radius); overflow: hidden; background: var(--color-surface-inset); box-shadow: var(--shadow-inset-well); }
.af-breakdown__seg { flex: var(--share, 1) 1 0; min-inline-size: 2px; background: var(--color-chart-1); }
.af-breakdown__seg[data-series="2"] { background: var(--color-chart-2); }
.af-breakdown__seg[data-series="3"] { background: var(--color-chart-3); }
.af-breakdown__seg[data-series="other"] { background: var(--color-chart-other); }
.af-breakdown__seg[data-series="2"], .af-breakdown__seg[data-series="3"], .af-breakdown__seg[data-series="other"] { background-image: repeating-linear-gradient(45deg, transparent 0 4px, color-mix(in srgb, var(--color-surface) 38%, transparent) 4px 6px); }
.af-breakdown__seg[data-series="3"] { background-image: repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--color-surface) 38%, transparent) 4px 6px); }
/* styles/patterns/infrastructure.css · metric-card-infra */
/* Infrastructure metric card: a metric (see surfaces.css) with its period, aggregation, an optional sparkline and an "as of"
* time so a stale number can never pass for a live one. */
.af-infra-metric__asof { display: flex; align-items: center; gap: var(--space-1-5); font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); color: var(--color-muted-text); }Design tokens this component reads. Override a token, not the component.
--color-border--color-muted-text--font-body--font-weight-semibold--icon-size-xs--space-0-5--space-2--space-4--type-body-size--type-caption-family--type-caption-size--type-caption-weight--type-label-family--type-label-weight--color-accent-solid--color-chart-1--color-chart-2--color-chart-3--color-chart-other--color-surface--color-surface-inset--color-text--ease-standard--font-weight-medium--font-weight-regular--meter-height--meter-height-lg--meter-radius--motion-emphasis--shadow-inset-well--share--space-1--space-1-5--space-3--status-danger-solid--status-danger-text--status-warning-solid--status-warning-text--threshold--type-body-sm-size--type-label-size--value--type-metadata-family--type-metadata-size--type-metadata-weightThe queue is the live component from Forms: drop a file to see rows move through queued, uploading and complete. Each state has its own text, icon and valid actions. The static rows below show every state at once; their buttons raise a sample toast rather than doing nothing.
ERR_CONNECTION_RESET.<section class="af-queue" aria-label="Upload states"><div class="af-queue__head"><h4 class="af-queue__title">Uploads</h4><div class="af-transfer"><span class="af-transfer__item"><strong>2</strong> of 4 complete</span><span class="af-transfer__item"><strong>~1.2 MB/s</strong></span><span class="af-transfer__item">~12 s left</span></div></div>
<ul class="af-queue__list">
<li class="af-upload-row" data-status="uploading"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-archive"/></svg></span><div><div class="af-upload-row__name">backup-2026-10.zip</div><div class="af-upload-row__meta"><span>48.2 MB</span><span class="af-upload-row__status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-loader-circle"/></svg>Uploading</span></div></div><div class="af-upload-row__actions"><button class="af-button af-button--ghost af-button--sm" type="button" data-toast="info" data-toast-title="Sample: upload would be canceled">Cancel</button></div><div class="af-upload-row__progress"><progress class="af-progress" max="100" value="64" aria-label="Upload progress for backup-2026-10.zip"></progress><span class="af-upload-row__pct" aria-hidden="true">64%</span></div></li>
<li class="af-upload-row" data-status="complete"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-image"/></svg></span><div><div class="af-upload-row__name">logo.png</div><div class="af-upload-row__meta"><span>18.4 kB</span><span class="af-upload-row__status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg>Complete</span></div></div><div class="af-upload-row__actions"><button class="af-button af-button--ghost af-button--sm" type="button" data-toast="info" data-toast-title="Sample: row would be removed">Remove</button></div></li>
<li class="af-upload-row" data-status="failed"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file"/></svg></span><div><div class="af-upload-row__name">report-fail.json</div><div class="af-upload-row__meta"><span>950 B</span><span class="af-upload-row__status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-x"/></svg>Failed</span></div></div><div class="af-upload-row__actions"><button class="af-button af-button--secondary af-button--sm" type="button" data-toast="info" data-toast-title="Sample: upload would retry">Retry</button></div><div class="af-upload-row__error"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-alert"/></svg><span>The connection was reset. Error <code>ERR_CONNECTION_RESET</code>.</span></div></li>
</ul></section>/* styles/patterns/infrastructure.css · upload-row */
/* One upload: icon, name, size, progress or outcome, and the actions valid for its state.
* data-status: queued · uploading · processing · complete · failed · canceled
* queued/uploading → Cancel failed/canceled → Retry + Remove complete → Remove (+ Open)
* A failure ALWAYS shows its reason and an error code beneath the row. */
.af-upload-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-1) var(--space-3); align-items: center; padding: var(--space-3) var(--space-4); }
.af-upload-row + .af-upload-row { border-block-start: 1px solid var(--color-border); }
.af-upload-row__name { font: var(--font-weight-medium) var(--type-body-sm-size)/1.35 var(--font-body); overflow-wrap: anywhere; }
.af-upload-row__meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-0-5) var(--space-3); font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
.af-upload-row__progress { grid-column: 2; display: flex; align-items: center; gap: var(--space-3); }
.af-upload-row__progress > .af-progress { flex: 1; }
.af-upload-row__pct { min-inline-size: 3.5ch; text-align: end; font: var(--type-metadata-weight) var(--type-metadata-size)/1 var(--type-metadata-family); font-variant-numeric: tabular-nums; }
.af-upload-row__actions { display: flex; align-items: center; gap: var(--space-1); grid-row: 1 / span 2; grid-column: 3; }
.af-upload-row__error { grid-column: 2 / -1; display: flex; gap: var(--space-2); align-items: flex-start; margin-block-start: var(--space-1); padding: var(--space-2) var(--space-3); border: var(--border-width-hairline) solid var(--status-danger-border); border-radius: var(--radius-sm); background: var(--status-danger-bg); color: var(--color-text); font: var(--type-caption-weight) var(--type-caption-size)/1.5 var(--type-caption-family); overflow-wrap: anywhere; }
.af-upload-row__error > .af-icon { color: var(--status-danger-solid); inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); margin-block-start: 1px; }
.af-upload-row__error code { background: transparent; border-color: var(--status-danger-border); }
.af-upload-row[data-status="complete"] .af-upload-row__progress { display: none; }
.af-upload-row[data-status="canceled"] .af-upload-row__name { color: var(--color-muted-text); text-decoration: line-through; }
/* styles/patterns/infrastructure.css · upload-queue */
/* A panel listing every upload in the current session. Header: overall summary + Cancel all / Clear completed. Rows are
* .af-upload-row. The summary text lives in a polite live region updated on state CHANGES (start, complete, fail), not on every
* progress tick. */
.af-queue { display: flex; flex-direction: column; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }
.af-queue__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); padding: var(--space-3) var(--space-4); border-block-end: 1px solid var(--color-border); background: var(--color-surface-inset); }
.af-queue__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.3 var(--type-component-title-family); }
.af-queue__list { margin: 0; padding: 0; list-style: none; max-block-size: 22rem; overflow-y: auto; overscroll-behavior: contain; }
.af-queue__empty { padding: var(--space-6) var(--space-4); text-align: center; color: var(--color-muted-text); font-size: var(--type-body-sm-size); }
/* styles/patterns/infrastructure.css · transfer-status */
/* A compact, comparable summary of a multi-file transfer. Figures are tabular. Rate and time remaining are ESTIMATES and say
* "~". */
.af-transfer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-5); font: var(--font-weight-regular) var(--type-body-sm-size)/1.4 var(--font-body); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
.af-transfer strong { color: var(--color-text); font-weight: var(--font-weight-semibold); }
.af-transfer__item { display: inline-flex; align-items: baseline; gap: var(--space-1-5); }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-border--color-muted-text--color-text--font-body--font-weight-medium--icon-size-sm--radius-sm--space-0-5--space-1--space-2--space-3--space-4--status-danger-bg--status-danger-border--status-danger-solid--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--type-metadata-family--type-metadata-size--type-metadata-weight--card-border--card-radius--color-surface--color-surface-inset--space-6--type-component-title-family--type-component-title-size--type-component-title-weight--font-weight-regular--font-weight-semibold--space-1-5--space-5https://storage.example.test/v1/{bucket}
<div class="af-endpoint"><span class="af-endpoint__label">S3-compatible endpoint <span class="af-env" data-env="production">Production</span></span>
<div class="af-endpoint__row"><span class="af-endpoint__method">HTTPS</span><p class="af-endpoint__url" id="in-ep">https://storage.example.test/v1/<span class="af-endpoint__param">{bucket}</span></p><button class="af-button af-button--secondary af-button--md" type="button" data-copy-target="#in-ep" data-copy-label="Endpoint"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg>Copy</button></div></div>/* styles/patterns/infrastructure.css · endpoint-display */
/* An API endpoint: protocol/method badge, the URL in a monospaced well (selectable, wraps), copy control, and the resource it
* belongs to. The URL is the source of truth — never rendered as an image or truncated without a title. */
.af-endpoint { display: flex; flex-direction: column; gap: var(--space-2); }
.af-endpoint__label { font: var(--type-label-weight) var(--type-label-size)/1.3 var(--type-label-family); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.af-endpoint__row { display: flex; align-items: stretch; border: var(--border-width-hairline) solid var(--color-border-control); border-radius: var(--input-radius); background: var(--color-surface-inset); box-shadow: var(--input-shadow); min-inline-size: 0; overflow: hidden; }
.af-endpoint__method { display: inline-flex; align-items: center; padding-inline: var(--space-3); border-inline-end: var(--border-width-hairline) solid var(--color-border-control); background: var(--color-surface); font: var(--font-weight-semibold) var(--type-metadata-size)/1 var(--font-mono); color: var(--color-text); }
.af-endpoint__url { flex: 1; min-inline-size: 0; margin: 0; padding: var(--space-2) var(--space-3); font: var(--type-code-weight) var(--type-code-size)/1.5 var(--type-code-family); font-variant-ligatures: none; overflow-wrap: anywhere; user-select: all; }
.af-endpoint__param { color: var(--color-accent-text); font-weight: var(--font-weight-medium); }
.af-endpoint__row > .af-button { align-self: stretch; block-size: auto; border-radius: 0; border-block: 0; border-inline-end: 0; }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--color-accent-text--color-border-control--color-surface--color-surface-inset--color-text--font-mono--font-weight-medium--font-weight-semibold--input-radius--input-shadow--space-2--space-3--type-code-family--type-code-size--type-code-weight--type-label-family--type-label-size--type-label-weight--type-metadata-sizeA key list never shows a secret: only a prefix and the last four characters. The full secret appears once, in the panel after creation or rotation, and must be acknowledged before it can be dismissed.
<ul class="af-key-list">
<li class="af-key-row" data-status="active"><div><h4 class="af-key-row__name">CI uploader <span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Active</span></h4><span class="af-key-row__secret"><b>afk_s1</b>••••••••••••<span>a9f2</span></span></div><div class="af-cluster af-cluster--2"><button class="af-button af-button--secondary af-button--sm" type="button" data-toast="info" data-toast-title="Sample: a new secret would be created">Rotate</button><button class="af-button af-button--destructive af-button--sm" type="button" data-dialog-open="#in-revoke">Revoke</button></div><div class="af-key-row__perms"><span class="af-badge af-badge--outline">objects:write</span><span class="af-badge af-badge--outline">objects:read</span></div><dl class="af-key-row__meta"><div><dt>Created</dt><dd>2026-09-14</dd></div><div><dt>Last used</dt><dd>2026-11-04 (sample)</dd></div></dl></li>
<li class="af-key-row" data-status="revoked"><div><h4 class="af-key-row__name">Old backup job <span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Revoked</span></h4><span class="af-key-row__secret"><b>afk_s1</b>••••••••••••<span>71c0</span></span></div><div class="af-key-row__perms"><span class="af-badge af-badge--outline">objects:read</span></div><dl class="af-key-row__meta"><div><dt>Created</dt><dd>2026-06-01</dd></div><div><dt>Last used</dt><dd>Never</dd></div></dl></li>
</ul>
<dialog class="af-modal af-modal--sm" id="in-revoke" role="alertdialog" aria-labelledby="in-revoke-t" aria-describedby="in-revoke-d"><form method="dialog" class="af-modal__panel"><div class="af-alertdialog__head"><span class="af-alertdialog__icon" aria-hidden="true"><svg class="af-icon" focusable="false"><use href="#i-triangle-alert"/></svg></span><div><h4 class="af-modal__title" id="in-revoke-t">Revoke “CI uploader”?</h4><p class="af-modal__description" id="in-revoke-d">Anything using this key stops working immediately. This cannot be undone. Sample dialog: nothing is revoked.</p></div></div><div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close autofocus>Cancel</button><button class="af-button af-button--destructive af-button--md" type="button" data-dialog-close="revoke" data-toast="danger" data-toast-title="Sample: key would be revoked">Revoke key</button></div></form></dialog>/* styles/patterns/infrastructure.css · api-key-row */
/* One API key. Secrets are NEVER shown here: only a prefix and the last four characters, masked in between. Permissions are
* listed as badges; "last used" is explicit even when never. Actions: Rotate (creates a new secret, shows the secret panel) and
* Revoke (confirmation dialog). A revoked key stays listed, labelled, with its actions removed. */
.af-key-row { display: grid; gap: var(--space-3) var(--space-4); grid-template-columns: minmax(0, 1fr) auto; align-items: start; padding: var(--space-4); }
.af-key-row + .af-key-row { border-block-start: 1px solid var(--color-border); }
.af-key-row__name { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.35 var(--type-component-title-family); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.af-key-row__secret { display: inline-flex; align-items: center; gap: var(--space-2); margin-block-start: var(--space-1); font: var(--type-code-weight) var(--type-metadata-size)/1.4 var(--font-mono); font-variant-ligatures: none; color: var(--color-muted-text); letter-spacing: 0.02em; overflow-wrap: anywhere; }
.af-key-row__secret > b { font-weight: var(--font-weight-medium); color: var(--color-text); letter-spacing: 0.08em; }
.af-key-row__perms { display: flex; flex-wrap: wrap; gap: var(--space-1-5); grid-column: 1 / -1; }
.af-key-row__meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-5); margin: 0; font: var(--type-caption-weight) var(--type-caption-size)/1.4 var(--type-caption-family); color: var(--color-muted-text); }
.af-key-row__meta > div { display: inline-flex; gap: var(--space-1-5); }
.af-key-row__meta dt { font-weight: var(--font-weight-medium); }
.af-key-row__meta dd { margin: 0; font-variant-numeric: tabular-nums; color: var(--color-text); }
.af-key-row[data-status="revoked"] { background: var(--color-surface-inset); }
.af-key-row[data-status="revoked"] .af-key-row__name, .af-key-row[data-status="revoked"] .af-key-row__secret { color: var(--color-muted-text); }
.af-key-list { margin: 0; padding: 0; list-style: none; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-border--color-muted-text--color-surface--color-surface-inset--color-text--font-mono--font-weight-medium--space-1--space-1-5--space-2--space-3--space-4--space-5--type-caption-family--type-caption-size--type-caption-weight--type-code-weight--type-component-title-family--type-component-title-size--type-component-title-weight--type-metadata-sizeThis is the only time it is shown. After you close this panel it cannot be recovered; create a new key instead.
<div class="af-secret-panel"><div class="af-secret-panel__head"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-key-round"/></svg><div><h4 class="af-secret-panel__title">Copy your secret now</h4><p class="af-secret-panel__text">This is the only time it is shown. After you close this panel it cannot be recovered; create a new key instead.</p></div></div>
<div class="af-field"><label class="af-field__label" for="in-sec">Secret key (sample value)</label><div class="af-input-group af-secret"><input class="af-input" id="in-sec" type="password" value="sample-secret-0000-0000" readonly autocomplete="off"><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-secret-toggle="#in-sec" aria-pressed="false" aria-label="Show secret"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-eye"/></svg></button><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-copy-target="#in-sec" data-copy-label="Secret" aria-label="Copy secret"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button></div></div>
<label class="af-check"><input class="af-check__input" type="checkbox" id="in-ack"><span class="af-check__label">I have stored this secret somewhere safe</span></label>
<div><button class="af-button af-button--primary af-button--md" type="button" aria-disabled="true" id="in-done" data-requires-check="#in-ack" data-toast="success" data-toast-title="Sample: panel would close">Done</button></div></div>/* styles/patterns/infrastructure.css · secret-panel */
/* Shown ONCE, immediately after a secret is created or rotated. The secret is masked until revealed on purpose, copyable, and the
* user must confirm they have stored it before the panel can be dismissed. After dismissal the value is unrecoverable and the
* panel says so. */
.af-secret-panel { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-5); border: var(--border-width-thick) solid var(--status-warning-solid); border-radius: var(--card-radius); background: var(--color-surface); }
.af-secret-panel__head { display: flex; gap: var(--space-3); align-items: flex-start; }
.af-secret-panel__head > .af-icon { color: var(--status-warning-solid); inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); }
.af-secret-panel__title { margin: 0; font: var(--type-component-title-weight) var(--type-component-title-size)/1.35 var(--type-component-title-family); }
.af-secret-panel__text { margin: var(--space-1) 0 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-line-height); color: var(--color-muted-text); }
/* styles/components/forms.css · secret-field */
/* A masked value that can be revealed (toggle button with aria-pressed) and copied. Never auto-reveal. Secrets that cannot be
* recovered (API keys after creation) use the Secret reveal panel in patterns/infrastructure.css instead. */
.af-secret > .af-input { font-family: var(--font-mono); letter-spacing: 0.06em; }
.af-secret > .af-input[type="text"] { letter-spacing: 0; }Design tokens this component reads. Override a token, not the component.
--border-width-thick--card-radius--color-muted-text--color-surface--icon-size-lg--space-1--space-3--space-4--space-5--status-warning-solid--type-body-line-height--type-body-sm-size--type-component-title-family--type-component-title-size--type-component-title-weight--font-mono<div class="af-delivery-list">
<details class="af-delivery"><summary><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>200 OK</span><span class="af-delivery__event">object.created</span><svg class="af-icon af-delivery__chev" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg><span class="af-delivery__meta"><span>https://hooks.example.test/afara</span><span>Attempt 1 of 3</span><span>182 ms</span><time datetime="2026-11-05T08:58:11Z">08:58:11 UTC</time></span></summary><div class="af-delivery__body"><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"event":"object.created","bucket":"acme-assets-prod"}</code></pre></div><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"received":true}</code></pre></div></div></details>
<details class="af-delivery" open><summary><span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>500 Failed</span><span class="af-delivery__event">object.deleted</span><svg class="af-icon af-delivery__chev" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg><span class="af-delivery__meta"><span>https://hooks.example.test/afara</span><span>Attempt 2 of 3</span><span>3,004 ms</span><time datetime="2026-11-05T08:59:40Z">08:59:40 UTC</time></span></summary><div class="af-delivery__body"><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"event":"object.deleted","key":"old.zip"}</code></pre></div><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"error":"upstream_timeout"}</code></pre></div><div class="af-delivery__actions"><button class="af-button af-button--secondary af-button--sm" type="button" data-toast="info" data-toast-title="Sample: delivery would be retried">Redeliver</button></div></div></details>
</div>/* styles/patterns/infrastructure.css · webhook-delivery */
/* One delivery attempt: outcome badge (HTTP status + word + icon), event type, endpoint, attempt n of m, duration, time. Failed
* and retried deliveries expand to show the request and response. Redeliver is always offered for failures. */
.af-delivery { border-block-start: 1px solid var(--color-border); }
.af-delivery:first-child { border-block-start: 0; }
.af-delivery > summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-1) var(--space-3); align-items: center; padding: var(--space-3) var(--space-4); list-style: none; cursor: pointer; }
.af-delivery > summary::-webkit-details-marker { display: none; }
.af-delivery > summary:focus-visible { outline-offset: var(--focus-ring-offset-inset); }
@media (hover: hover) { .af-delivery > summary:hover { background: var(--color-surface-hover); } }
.af-delivery__event { font: var(--font-weight-medium) var(--type-metadata-size)/1.4 var(--font-mono); overflow-wrap: anywhere; }
.af-delivery__meta { grid-column: 2; display: flex; flex-wrap: wrap; gap: var(--space-0-5) var(--space-3); font: var(--type-metadata-weight) var(--type-metadata-size)/1.4 var(--type-metadata-family); color: var(--color-muted-text); font-variant-numeric: tabular-nums; }
.af-delivery__chev { grid-column: 3; grid-row: 1; color: var(--color-muted-text); transition: transform var(--motion-standard) var(--ease-standard); }
.af-delivery[open] > summary .af-delivery__chev { transform: rotate(180deg); }
.af-delivery__body { display: grid; gap: var(--space-4); padding: var(--space-2) var(--space-4) var(--space-4); }
@media (min-width: 768px) { .af-delivery__body { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.af-delivery__actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.af-delivery-list { margin: 0; padding: 0; list-style: none; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }Design tokens this component reads. Override a token, not the component.
--border-width-hairline--card-border--card-radius--color-border--color-muted-text--color-surface--color-surface-hover--ease-standard--focus-ring-offset-inset--font-mono--font-weight-medium--motion-standard--space-0-5--space-1--space-2--space-3--space-4--type-metadata-family--type-metadata-size--type-metadata-weight<ul class="af-log">
<li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:14:02Z" title="Local: 10:14:02 WAT">2026-11-05 09:14:02 UTC</time><span class="af-log__level"><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>OK</span></span><span class="af-log__msg"><span class="af-activity-entry__actor">ci-uploader</span><span class="af-log__op">PutObject</span><span class="af-log__resource">acme-assets-prod/logo.png</span><span class="af-activity-entry__id">req_SAMPLE_0001</span></span></li>
<li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:15:30Z" title="Local: 10:15:30 WAT">2026-11-05 09:15:30 UTC</time><span class="af-log__level"><span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>FAIL</span></span><span class="af-log__msg"><span class="af-activity-entry__actor">backup-job</span><span class="af-log__op">DeleteObject</span><span class="af-log__resource">acme-assets-prod/old.zip</span><span class="af-activity-entry__id">req_SAMPLE_0002</span></span></li>
</ul>/* styles/patterns/infrastructure.css · activity-entry */
/* Infrastructure activity log entry: an event-log row (data-display.css) with an actor, an operation, a resource and request ID.
* Timestamps are shown in UTC with an unambiguous suffix, with local time in the title attribute. */
.af-activity-entry__actor { font-weight: var(--font-weight-medium); }
.af-activity-entry__id { font: var(--type-metadata-weight) var(--type-metadata-size)/1.4 var(--type-metadata-family); color: var(--color-muted-text); }
/* styles/components/data-display.css · event-log */
/* Dense, scannable system events: time · level · operation · resource · identifier. Rows with failures expand to show the
* error code, message and request ID (<details>). Never truncate an identifier a user may need to quote to support — wrap it. */
.af-log { margin: 0; padding: 0; list-style: none; border: var(--border-width-hairline) solid var(--card-border); border-radius: var(--card-radius); background: var(--color-surface); overflow: hidden; }
.af-log__row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-1) var(--space-3); align-items: baseline; padding: var(--space-2) var(--space-4); font-size: var(--type-body-sm-size); }
.af-log__row + .af-log__row, .af-log > details + .af-log__row, .af-log__row + details, .af-log > details + details { border-block-start: 1px solid var(--color-border); }
.af-log > details > summary { list-style: none; cursor: pointer; }
.af-log > details > summary::-webkit-details-marker { display: none; }
.af-log > details > summary:focus-visible { outline-offset: var(--focus-ring-offset-inset); }
@media (hover: hover) { .af-log > details > summary:hover { background: var(--color-surface-hover); } }
.af-log__time { font: var(--type-metadata-weight) var(--type-metadata-size)/1.5 var(--type-metadata-family); color: var(--color-muted-text); white-space: nowrap; }
.af-log__msg { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); min-inline-size: 0; }
.af-log__op { font-family: var(--font-mono); font-size: var(--type-metadata-size); font-weight: var(--font-weight-medium); }
.af-log__resource { font-family: var(--font-mono); font-size: var(--type-metadata-size); color: var(--color-muted-text); overflow-wrap: anywhere; }
.af-log__detail { grid-column: 1 / -1; margin: var(--space-1) 0 var(--space-2); padding: var(--space-3); border-radius: var(--radius-sm); background: var(--status-danger-bg); border: var(--border-width-hairline) solid var(--status-danger-border); color: var(--color-text); font: var(--type-metadata-weight) var(--type-metadata-size)/1.6 var(--type-metadata-family); overflow-wrap: anywhere; }
@media (min-width: 768px) { .af-log__row { grid-template-columns: 9.5rem 5.5rem minmax(0, 1fr); } .af-log__row > .af-log__level { order: 0; } }Design tokens this component reads. Override a token, not the component.
--color-muted-text--font-weight-medium--type-metadata-family--type-metadata-size--type-metadata-weight--border-width-hairline--card-border--card-radius--color-border--color-surface--color-surface-hover--color-text--focus-ring-offset-inset--font-mono--radius-sm--space-1--space-2--space-3--space-4--status-danger-bg--status-danger-border--type-body-sm-sizePeriod: 1 to 5 November (sample). Unit: decimal gigabytes. This is an estimate, not billing data.
<div class="af-usage"><div class="af-usage__head"><div class="af-usage__total"><span class="af-metric__label">Stored this month</span><span class="af-metric__value">90.0<span class="af-metric__unit">GB</span></span></div><span class="af-sample">Sample data</span></div>
<div class="af-breakdown" role="img" aria-label="Standard 62.4 GB, Infrequent 18.1 GB, Archive 9.5 GB"><span class="af-breakdown__seg" data-series="1" style="--share: 62.4"></span><span class="af-breakdown__seg" data-series="2" style="--share: 18.1"></span><span class="af-breakdown__seg" data-series="3" style="--share: 9.5"></span></div>
<ul class="af-usage__rows"><li class="af-usage__row"><span class="af-legend__swatch" data-series="1"></span><span>Standard</span><span>62.4 GB</span><span class="af-usage__share">69%</span></li><li class="af-usage__row"><span class="af-legend__swatch" data-series="2"></span><span>Infrequent</span><span>18.1 GB</span><span class="af-usage__share">20%</span></li><li class="af-usage__row"><span class="af-legend__swatch" data-series="3"></span><span>Archive</span><span>9.5 GB</span><span class="af-usage__share">11%</span></li></ul>
<p class="af-usage__explain">Period: 1 to 5 November (sample). Unit: decimal gigabytes. This is an estimate, not billing data.</p></div>/* styles/patterns/infrastructure.css · usage-breakdown */
/* Consumption split by a dimension (project, bucket, operation, storage class). A stacked bar for the whole, a legend that lists
* each part with its value and share, and the period + unit + estimate status stated in plain words. */
.af-usage { display: flex; flex-direction: column; gap: var(--space-4); }
.af-usage__head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
.af-usage__total { display: flex; flex-direction: column; gap: var(--space-0-5); }
.af-usage__rows { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
.af-usage__row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; border-block-start: 1px solid var(--color-border); font-size: var(--type-body-sm-size); font-variant-numeric: tabular-nums; }
.af-usage__row > :nth-child(3), .af-usage__row > :nth-child(4) { text-align: end; }
.af-usage__share { min-inline-size: 4ch; color: var(--color-muted-text); }
.af-usage__explain { margin: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-sm); background: var(--color-surface-inset); font: var(--type-caption-weight) var(--type-caption-size)/1.55 var(--type-caption-family); color: var(--color-muted-text); }
/* styles/components/data-display.css · legend */
.af-legend { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); margin: 0; padding: 0; list-style: none; font: var(--font-weight-regular) var(--type-caption-size)/1.4 var(--font-body); color: var(--color-muted-text); }
.af-legend__item { display: inline-flex; align-items: center; gap: var(--space-1-5); }
.af-legend__swatch { inline-size: var(--space-3); block-size: var(--space-3); flex: none; border-radius: var(--radius-xs); background: var(--color-chart-1); }
.af-legend__swatch[data-series="2"] { background: var(--color-chart-2); }
.af-legend__swatch[data-series="3"] { background: var(--color-chart-3); }
.af-legend__swatch[data-series="other"] { background: var(--color-chart-other); }
.af-chart[data-texture] .af-legend__swatch[data-series="2"], .af-chart[data-texture] .af-legend__swatch[data-series="other"], .af-legend[data-texture] .af-legend__swatch[data-series="2"], .af-legend[data-texture] .af-legend__swatch[data-series="other"] { background-image: repeating-linear-gradient(45deg, transparent 0 3px, color-mix(in srgb, var(--color-surface) 38%, transparent) 3px 5px); }
.af-chart[data-texture] .af-legend__swatch[data-series="3"], .af-legend[data-texture] .af-legend__swatch[data-series="3"] { background-image: repeating-linear-gradient(135deg, transparent 0 3px, color-mix(in srgb, var(--color-surface) 38%, transparent) 3px 5px); }
.af-legend__value { color: var(--color-text); font-variant-numeric: tabular-nums; font-weight: var(--font-weight-medium); }
.af-legend button.af-legend__item { border-radius: var(--radius-sm); padding: var(--space-0-5) var(--space-1); }
.af-legend button.af-legend__item[aria-pressed="false"] { opacity: 0.45; text-decoration: line-through; }Design tokens this component reads. Override a token, not the component.
--color-border--color-muted-text--color-surface-inset--radius-sm--space-0-5--space-2--space-3--space-4--type-body-sm-size--type-caption-family--type-caption-size--type-caption-weight--color-chart-1--color-chart-2--color-chart-3--color-chart-other--color-surface--color-text--font-body--font-weight-medium--font-weight-regular--radius-xs--space-1--space-1-5Patterns arrange the components into whole views. Each is built in one of the example pages, using only documented components and flagged sample data.
| Pattern | Composes | Example |
|---|---|---|
| Resource hierarchy | Resource path, switchers, environment badge, resource card | Buckets |
| Storage overview | Metric cards, capacity card, usage breakdown, chart, activity | Storage overview |
| File management | Object explorer, file rows, inspector, bulk bar, pagination | Files |
| API management | Endpoint display, key rows, secret panel, webhook deliveries | API keys and webhooks |
| Activity | Event log, activity entries, timeline, filters | Dashboard |
| Usage and billing | Meters, breakdown, estimate notes, key-value lists | Storage overview |
| Upload workflow | Dropzone, upload queue and rows, transfer status, error states | Files |