diff --git a/docs/assets/VENDORED.md b/docs/assets/VENDORED.md deleted file mode 100644 index 6cda9aa..0000000 --- a/docs/assets/VENDORED.md +++ /dev/null @@ -1,61 +0,0 @@ -# Vendored assets - -Everything in this directory is copied from another repository. It is -machine-owned: do not edit any of it in place. To update, re-vendor from a tag -and record the new version here and in the header of each file. - -## Instrument Workshop - -| Item | Value | -|---|---| -| Source | [`trsdn/design-system`](https://github.com/trsdn/design-system) | -| Version | **v1.5.1** | -| Vendored on | 2026-09-17 | - -| File here | Source path | -|---|---| -| `core.tokens.css` | `tokens/generated/web/core.tokens.css` | -| `instrument-workshop.css` | `src/web/styles/instrument-workshop.css` | -| `instrument-workshop-fonts.css` | `assets/instrument-workshop-fonts.css` | -| `fonts/ibm-plex/*.woff2` | `assets/fonts/ibm-plex/*.woff2` | - -Copying rather than depending is deliberate and is expressly permitted by the -[Repository Quality Standard](https://github.com/trsdn/.github/blob/main/docs/repository-quality-standard.md#design-language): -the design system is maintained privately, so a public site cannot resolve it at -build time, and a vendored copy keeps rendering when the source moves. - -### Re-vendoring - -```bash -VERSION=v1.5.1 -for f in tokens/generated/web/core.tokens.css \ - src/web/styles/instrument-workshop.css \ - assets/instrument-workshop-fonts.css; do - gh api "repos/trsdn/design-system/contents/$f?ref=$VERSION" \ - -H "Accept: application/vnd.github.raw" > "docs/assets/$(basename "$f")" -done -for f in IBMPlexSans-Regular IBMPlexSans-Medium IBMPlexSans-SemiBold \ - IBMPlexMono-Regular IBMPlexMono-SemiBold; do - gh api "repos/trsdn/design-system/contents/assets/fonts/ibm-plex/$f.woff2?ref=$VERSION" \ - -H "Accept: application/vnd.github.raw" > "docs/assets/fonts/ibm-plex/$f.woff2" -done -``` - -Then restore the vendoring header at the top of each CSS file and update the -version in this document. - -### Recorded deviations - -None. The site uses the design language as published. - -## IBM Plex - -`fonts/ibm-plex/*.woff2` are IBM Plex Sans and IBM Plex Mono, licensed under the -SIL Open Font License 1.1. The licence text is at -[`fonts/ibm-plex/OFL.txt`](fonts/ibm-plex/OFL.txt) and applies to the font files -only, not to OpenPromptr itself, which is MIT-licensed. - -They are self-hosted rather than loaded from a font CDN. A remote font host -observes every visitor's address and referrer on a page this project controls, -which criterion `W07` forbids and criterion `Y02` would otherwise require this -project to disclose. diff --git a/docs/assets/core.tokens.css b/docs/assets/core.tokens.css deleted file mode 100644 index dc9cb93..0000000 --- a/docs/assets/core.tokens.css +++ /dev/null @@ -1,275 +0,0 @@ -/* Vendored from trsdn/design-system (Instrument Workshop) v1.5.1. Source: core.tokens.css. Vendored on: 2026-09-17. Do not edit in place; re-vendor from a tag; see docs/assets/VENDORED.md. */ -/* - * DO NOT EDIT — generated file - * Source: tokens/core.tokens.json - * Source SHA-256: c2c2e11413d4ea8a8a3f23ef80fdfebf50750231850e942f7d2555dcecbd6143 - * Generator: scripts/build-tokens.mjs@1.2.0 - * Lifecycle: Generated / non-normative - * Decision: decisions/token-transform-architecture.md - * Regenerate: node scripts/build-tokens.mjs - */ - -:root { - color-scheme: light; - --canvas: #d8d5ce; - --surface-primary: #ece9e2; - --surface-grouped: #c8c7c1; - --surface-hover: #b9bbb5; - --text-primary: #202522; - --text-secondary: #5d625e; - --border-essential: #656864; - --rule-subtle: #9a9d98; - --identity: #29483c; - --identity-ink: #ffffff; - --warning: #8a5400; - --danger: #a4381a; - --success: #315f4b; - --focus: #111111; - --surface-raised: #ece9e2; - --surface-recessed: #d8d5ce; - --surface-sunken: #d8d5ce; - --surface-selected: #b9bbb5; - --text-quiet: #5d625e; - --text-attention: #8a5400; - --text-critical: #a4381a; - --border-subtle: #9a9d98; - --border-quiet: #9a9d98; - --status-critical: #a4381a; - --status-caution: #8a5400; - --status-affirmative: #315f4b; - --focus-ring: #111111; - --syntax-plain: #202522; - --syntax-quiet: #5d625e; - --syntax-keyword: #29483c; - --syntax-string: #8a5400; - --syntax-literal: #a4381a; - --syntax-invalid: #a4381a; - --syntax-added: #315f4b; - --syntax-removed: #a4381a; - --syntax-modified: #8a5400; - --chart-categorical-1: #122f50; - --chart-categorical-2: #643212; - --chart-categorical-3: #24635c; - --chart-categorical-4: #6e7a36; - --chart-sequential-1: #a5c9ba; - --chart-sequential-2: #7ea092; - --chart-sequential-3: #58796b; - --chart-sequential-4: #355448; - --chart-sequential-5: #133226; - --term-background: #ece9e2; - --term-foreground: #202522; - --term-cursor: #111111; - --term-black: #1b201d; - --term-bright-black: #3f4642; - --term-red: #7a1800; - --term-bright-red: #a63a1c; - --term-green: #174633; - --term-bright-green: #396753; - --term-yellow: #5a3500; - --term-bright-yellow: #885200; - --term-blue: #243e63; - --term-bright-blue: #485f86; - --term-magenta: #6c2246; - --term-bright-magenta: #924567; - --term-cyan: #004452; - --term-bright-cyan: #226678; - --term-white: #4e534f; - --term-bright-white: #6c716d; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { - color-scheme: dark; - --canvas: #171b19; - --surface-primary: #252a27; - --surface-grouped: #343936; - --surface-hover: #404641; - --text-primary: #f0ede5; - --text-secondary: #b8b6ae; - --border-essential: #858a83; - --rule-subtle: #555b56; - --identity: #7fa38f; - --identity-ink: #0d1a14; - --warning: #ef9d36; - --danger: #ee6b43; - --success: #83b599; - --focus: #ffffff; - --surface-raised: #252a27; - --surface-recessed: #171b19; - --surface-sunken: #171b19; - --surface-selected: #404641; - --text-quiet: #b8b6ae; - --text-attention: #ef9d36; - --text-critical: #ee6b43; - --border-subtle: #555b56; - --border-quiet: #555b56; - --status-critical: #ee6b43; - --status-caution: #ef9d36; - --status-affirmative: #83b599; - --focus-ring: #ffffff; - --syntax-plain: #f0ede5; - --syntax-quiet: #b8b6ae; - --syntax-keyword: #7fa38f; - --syntax-string: #ef9d36; - --syntax-literal: #ee6b43; - --syntax-invalid: #ee6b43; - --syntax-added: #83b599; - --syntax-removed: #ee6b43; - --syntax-modified: #ef9d36; - --chart-categorical-1: #3878c3; - --chart-categorical-2: #de7834; - --chart-categorical-3: #4fc2b6; - --chart-categorical-4: #c4d865; - --chart-sequential-1: #2d4d3c; - --chart-sequential-2: #4d6f5d; - --chart-sequential-3: #709380; - --chart-sequential-4: #94b9a4; - --chart-sequential-5: #bae0cb; - --term-background: #252a27; - --term-foreground: #f0ede5; - --term-cursor: #ffffff; - --term-black: #101512; - --term-bright-black: #495049; - --term-red: #e46e4a; - --term-bright-red: #ff9c7d; - --term-green: #6b9b84; - --term-bright-green: #8fc1a9; - --term-yellow: #c28437; - --term-bright-yellow: #eca85a; - --term-blue: #7b91bb; - --term-bright-blue: #a1b6e3; - --term-magenta: #ca779a; - --term-bright-magenta: #f29dc0; - --term-cyan: #5b9aac; - --term-bright-cyan: #81bfd2; - --term-white: #9aa09b; - --term-bright-white: #c6ccc7; - } -} - -html[data-theme="dark"] { - color-scheme: dark; - --canvas: #171b19; - --surface-primary: #252a27; - --surface-grouped: #343936; - --surface-hover: #404641; - --text-primary: #f0ede5; - --text-secondary: #b8b6ae; - --border-essential: #858a83; - --rule-subtle: #555b56; - --identity: #7fa38f; - --identity-ink: #0d1a14; - --warning: #ef9d36; - --danger: #ee6b43; - --success: #83b599; - --focus: #ffffff; - --surface-raised: #252a27; - --surface-recessed: #171b19; - --surface-sunken: #171b19; - --surface-selected: #404641; - --text-quiet: #b8b6ae; - --text-attention: #ef9d36; - --text-critical: #ee6b43; - --border-subtle: #555b56; - --border-quiet: #555b56; - --status-critical: #ee6b43; - --status-caution: #ef9d36; - --status-affirmative: #83b599; - --focus-ring: #ffffff; - --syntax-plain: #f0ede5; - --syntax-quiet: #b8b6ae; - --syntax-keyword: #7fa38f; - --syntax-string: #ef9d36; - --syntax-literal: #ee6b43; - --syntax-invalid: #ee6b43; - --syntax-added: #83b599; - --syntax-removed: #ee6b43; - --syntax-modified: #ef9d36; - --chart-categorical-1: #3878c3; - --chart-categorical-2: #de7834; - --chart-categorical-3: #4fc2b6; - --chart-categorical-4: #c4d865; - --chart-sequential-1: #2d4d3c; - --chart-sequential-2: #4d6f5d; - --chart-sequential-3: #709380; - --chart-sequential-4: #94b9a4; - --chart-sequential-5: #bae0cb; - --term-background: #252a27; - --term-foreground: #f0ede5; - --term-cursor: #ffffff; - --term-black: #101512; - --term-bright-black: #495049; - --term-red: #e46e4a; - --term-bright-red: #ff9c7d; - --term-green: #6b9b84; - --term-bright-green: #8fc1a9; - --term-yellow: #c28437; - --term-bright-yellow: #eca85a; - --term-blue: #7b91bb; - --term-bright-blue: #a1b6e3; - --term-magenta: #ca779a; - --term-bright-magenta: #f29dc0; - --term-cyan: #5b9aac; - --term-bright-cyan: #81bfd2; - --term-white: #9aa09b; - --term-bright-white: #c6ccc7; -} - -:root { - --space-detail: 4px; - --space-compact: 8px; - --space-dense: 12px; - --space-standard: 16px; - --space-group: 24px; - --space-section: 32px; - --target-minimum: 24px; - --target-compact-row: 32px; - --target-standard-control: 40px; - --target-touch-control: 44px; - --target-pointer: 24px; - --icon-size-inline: 16px; - --icon-size-standard: 20px; - --icon-size-prominent: 24px; - --layout-page-inset-compact: 16px; - --layout-page-inset-standard: 24px; - --layout-page-inset-wide: 32px; - --layout-work-width-maximum: 1440px; - --edge-boundary-essential: 1px; - --edge-state-emphasis: 3px; - --edge-focus-width: 3px; - --edge-focus-offset: 3px; - --edge-focus-ring: 3px; - --edge-radius-authored: 3px; - --edge-clip-major: 10px; - --edge-boundary-subtle: 1px; - --edge-boundary-strong: 3px; - --edge-signature: 3px; - --edge-selection: 3px; - --font-size-page-title: 26px; - --font-size-section-title: 20px; - --font-size-body: 16px; - --font-size-body-compact: 15px; - --font-size-label-dense: 13px; - --font-size-caption: 12px; - --font-size-heading: 20px; - --font-size-display: 26px; - --line-height-page-title: 1.2; - --line-height-section-title: 1.25; - --line-height-body: 1.5; - --line-height-label-dense: 1.35; - --font-weight-body: 400; - --font-weight-emphasis: 500; - --font-weight-title: 600; - --font-weight-label-dense: 600; - --letter-spacing-interface: 0em; - --letter-spacing-eyebrow: 0.08em; - --motion-duration-press-fast: 80ms; - --motion-duration-press-standard: 160ms; - --motion-duration-local-reveal: 200ms; - --motion-duration-panel-transition: 240ms; - --motion-duration-reduced: 100ms; - --motion-easing-direct: cubic-bezier(0, 0, 0.58, 1); - --font-interface: "IBM Plex Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --font-data: "IBM Plex Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; - --font-numeric: "IBM Plex Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; -} diff --git a/docs/assets/fonts/ibm-plex/IBMPlexMono-Regular.woff2 b/docs/assets/fonts/ibm-plex/IBMPlexMono-Regular.woff2 deleted file mode 100644 index 4297ee3..0000000 Binary files a/docs/assets/fonts/ibm-plex/IBMPlexMono-Regular.woff2 and /dev/null differ diff --git a/docs/assets/fonts/ibm-plex/IBMPlexMono-SemiBold.woff2 b/docs/assets/fonts/ibm-plex/IBMPlexMono-SemiBold.woff2 deleted file mode 100644 index 86e2d90..0000000 Binary files a/docs/assets/fonts/ibm-plex/IBMPlexMono-SemiBold.woff2 and /dev/null differ diff --git a/docs/assets/fonts/ibm-plex/IBMPlexSans-Medium.woff2 b/docs/assets/fonts/ibm-plex/IBMPlexSans-Medium.woff2 deleted file mode 100644 index b142a66..0000000 Binary files a/docs/assets/fonts/ibm-plex/IBMPlexSans-Medium.woff2 and /dev/null differ diff --git a/docs/assets/fonts/ibm-plex/IBMPlexSans-Regular.woff2 b/docs/assets/fonts/ibm-plex/IBMPlexSans-Regular.woff2 deleted file mode 100644 index 231a1ad..0000000 Binary files a/docs/assets/fonts/ibm-plex/IBMPlexSans-Regular.woff2 and /dev/null differ diff --git a/docs/assets/fonts/ibm-plex/IBMPlexSans-SemiBold.woff2 b/docs/assets/fonts/ibm-plex/IBMPlexSans-SemiBold.woff2 deleted file mode 100644 index b96a19c..0000000 Binary files a/docs/assets/fonts/ibm-plex/IBMPlexSans-SemiBold.woff2 and /dev/null differ diff --git a/docs/assets/fonts/ibm-plex/OFL.txt b/docs/assets/fonts/ibm-plex/OFL.txt deleted file mode 100644 index c35c4c6..0000000 --- a/docs/assets/fonts/ibm-plex/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/assets/instrument-workshop-fonts.css b/docs/assets/instrument-workshop-fonts.css deleted file mode 100644 index d46d097..0000000 --- a/docs/assets/instrument-workshop-fonts.css +++ /dev/null @@ -1,63 +0,0 @@ -/* Vendored from trsdn/design-system (Instrument Workshop) v1.5.1. Source: instrument-workshop-fonts.css. Vendored on: 2026-09-17. Do not edit in place; re-vendor from a tag; see docs/assets/VENDORED.md. */ -/* - * Generated by scripts/build-font-stylesheet.mjs@1 — do not edit. - * Source: src/web/styles/instrument-workshop-fonts.css - * Regenerate: node scripts/build-font-stylesheet.mjs - * - * Identical to the source except that the font URLs are relative to assets/ - * rather than to an installed package, so anything inside assets/ can link - * this sheet and get the accepted faces in a checkout, in the published - * package, and on the portal alike. - */ -/* - * Instrument Workshop — bundled typefaces (optional layer) - * - * IBM Plex Sans + IBM Plex Mono, SIL Open Font License 1.1. - * Source: @ibm/plex 6.4.1 (github.com/IBM/plex); license text ships alongside - * the font files in fonts/ibm-plex/OFL.txt. Owner acceptance and the - * verification evidence are recorded in foundations/design-language.md §5.1. - * - * Load this before the token stylesheet or not at all: the font-family tokens - * fall back to the platform system stack when these faces are absent, so this - * file is an enhancement, never a requirement. - */ - -@font-face { - font-family: "IBM Plex Sans"; - src: url("fonts/ibm-plex/IBMPlexSans-Regular.woff2") format("woff2"); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: "IBM Plex Sans"; - src: url("fonts/ibm-plex/IBMPlexSans-Medium.woff2") format("woff2"); - font-weight: 500; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: "IBM Plex Sans"; - src: url("fonts/ibm-plex/IBMPlexSans-SemiBold.woff2") format("woff2"); - font-weight: 600; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: "IBM Plex Mono"; - src: url("fonts/ibm-plex/IBMPlexMono-Regular.woff2") format("woff2"); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: "IBM Plex Mono"; - src: url("fonts/ibm-plex/IBMPlexMono-SemiBold.woff2") format("woff2"); - font-weight: 600; - font-style: normal; - font-display: swap; -} diff --git a/docs/assets/instrument-workshop.css b/docs/assets/instrument-workshop.css deleted file mode 100644 index 9d6011a..0000000 --- a/docs/assets/instrument-workshop.css +++ /dev/null @@ -1,2699 +0,0 @@ -/* Vendored from trsdn/design-system (Instrument Workshop) v1.5.1. Source: instrument-workshop.css. Vendored on: 2026-09-17. Do not edit in place; re-vendor from a tag; see docs/assets/VENDORED.md. */ -/* - * Instrument Workshop — consumable component stylesheet - * - * Lifecycle: Candidate / consumable - * Requires: tokens/generated/web/core.tokens.css (load first) - * Source of truth for values: tokens/core.tokens.json - * Foundations: foundations/design-language.md - * - * Usage: - * - * - * … - * - * This file styles real application markup. It adds no JavaScript, no build - * step, and no framework. Every control is native HTML so platform behaviour, - * keyboard support, and assistive technology semantics are preserved. - * - * Density: set data-density="compact | standard | comfortable" on any container. - * Theme: set data-theme="dark" on (see core.tokens.css). - * - * Cascade: everything here lives in the `instrument-workshop` layer, so any - * unlayered rule in a consuming app overrides these styles without specificity - * escalation or !important. - */ - -@layer instrument-workshop { - - -/* ========================================================================== - 1. Base - ========================================================================== */ - -.iw-root { - /* Web-only layout knobs; retune per app without forking the stylesheet. - ch/rem measures have no cross-platform token equivalent. */ - --measure-prose: 66ch; - --iw-sidebar-min: 200px; - --iw-sidebar-max: 240px; - --iw-metric-min: 14rem; - --iw-avatar-size: 2rem; - margin: 0; - background: var(--canvas); - color: var(--text-primary); - font-family: var(--font-interface); - font-size: var(--font-size-body); - line-height: var(--line-height-body); - font-weight: var(--font-weight-body); - -webkit-text-size-adjust: 100%; -} - -.iw-root *, -.iw-root *::before, -.iw-root *::after { - box-sizing: border-box; -} - -/* Density scale. Standard is the default for repeated work. */ -.iw-root, -[data-density="standard"] { - --iw-control-height: var(--target-standard-control); - --iw-row-height: var(--target-standard-control); - --iw-region-padding: var(--space-standard); -} - -[data-density="compact"] { - --iw-control-height: var(--target-compact-row); - --iw-row-height: var(--target-compact-row); - --iw-region-padding: var(--space-dense); -} - -[data-density="comfortable"] { - --iw-control-height: var(--target-touch-control); - --iw-row-height: var(--target-touch-control); - --iw-region-padding: var(--space-group); -} - -/* Touch-capable input returns to comfortable targets regardless of density. */ -@media (pointer: coarse) { - .iw-root, - [data-density] { - --iw-control-height: var(--target-touch-control); - --iw-row-height: var(--target-touch-control); - } -} - -/* One focus indicator for every interactive surface. */ -.iw-root :focus-visible { - outline: var(--edge-focus-width) solid var(--focus); - outline-offset: var(--edge-focus-offset); - border-radius: var(--edge-radius-authored); -} - -.iw-root :focus:not(:focus-visible) { - outline: none; -} - -/* ========================================================================== - 2. Typography roles (foundations section 5.2) - ========================================================================== */ - -.iw-page-title { - margin: 0; - font-size: var(--font-size-page-title); - line-height: var(--line-height-page-title); - font-weight: var(--font-weight-title); - text-wrap: balance; -} - -.iw-section-title { - margin: 0; - font-size: var(--font-size-section-title); - line-height: var(--line-height-section-title); - font-weight: var(--font-weight-title); -} - -.iw-body { - margin: 0; - font-size: var(--font-size-body); - line-height: var(--line-height-body); -} - -.iw-body--compact { - font-size: var(--font-size-body-compact); -} - -.iw-secondary { - color: var(--text-secondary); -} - -.iw-caption { - margin: 0; - font-size: var(--font-size-caption); - line-height: var(--line-height-label-dense); - color: var(--text-secondary); -} - -/* - * Eyebrow — an indexed, uppercase label. Foundations limit uppercase to short - * codes and eyebrows of 16 characters or fewer, with added tracking. - */ -.iw-eyebrow { - margin: 0; - font-family: var(--font-data); - font-size: var(--font-size-label-dense); - line-height: var(--line-height-label-dense); - font-weight: var(--font-weight-label-dense); - letter-spacing: var(--letter-spacing-eyebrow); - text-transform: uppercase; - color: var(--text-secondary); -} - -/* Signature: calibrated numerics. Tabular figures, stable alignment. */ -.iw-numeric { - font-variant-numeric: tabular-nums; - font-feature-settings: "tnum" 1; - text-align: right; - white-space: nowrap; -} - -/* Codes, IDs, and exact values. Same optical size as surrounding text. */ -.iw-code { - font-family: var(--font-data); - font-size: 0.9375em; - font-variant-numeric: tabular-nums; -} - -/* ========================================================================== - 3. Layout - ========================================================================== */ - -.iw-page { - max-width: var(--layout-work-width-maximum); - margin-inline: auto; - padding-inline: var(--layout-page-inset-compact); - padding-block: var(--space-group); -} - -@media (min-width: 768px) { - .iw-page { - padding-inline: var(--layout-page-inset-standard); - } -} - -@media (min-width: 1200px) { - .iw-page { - padding-inline: var(--layout-page-inset-wide); - } -} - -/* Application shell: navigation column plus work column. */ -.iw-shell { - display: grid; - gap: var(--space-group); - grid-template-columns: 1fr; - align-items: start; -} - -@media (min-width: 900px) { - .iw-shell { - grid-template-columns: minmax(var(--iw-sidebar-min, 200px), var(--iw-sidebar-max, 240px)) minmax(0, 1fr); - } -} - -.iw-page-header { - display: flex; - flex-wrap: wrap; - gap: var(--space-standard); - align-items: flex-end; - justify-content: space-between; - padding-block-end: var(--space-standard); - border-block-end: var(--edge-boundary-essential) solid var(--border-essential); -} - -.iw-page-header > div { - display: grid; - gap: var(--space-detail); - min-width: 0; -} - -.iw-stack { - display: grid; - gap: var(--space-standard); -} - -.iw-stack--dense { - gap: var(--space-dense); -} - -.iw-stack--section { - gap: var(--space-section); -} - -.iw-row { - display: flex; - flex-wrap: wrap; - gap: var(--space-compact); - align-items: center; -} - -.iw-spacer { - flex: 1 1 auto; -} - -/* - * Signature: visible construction. Sections are separated by rules and shared - * alignment rather than by giving every region its own floating card. - */ -.iw-section { - display: grid; - gap: var(--space-dense); - padding-block: var(--space-group); - border-block-start: var(--edge-boundary-essential) solid var(--rule-subtle); -} - -.iw-section:first-child { - border-block-start: none; - padding-block-start: 0; -} - -/* ========================================================================== - 4. Signatures - ========================================================================== */ - -/* - * Signature: clipped geometry. A restrained clipped corner marks one major - * authored region per view. Not for individual controls. - */ -.iw-region { - position: relative; - align-content: start; - padding: var(--iw-region-padding); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -.iw-region--authored { - border-radius: 0; - clip-path: polygon( - 0 0, - calc(100% - var(--edge-clip-major)) 0, - 100% var(--edge-clip-major), - 100% 100%, - 0 100% - ); -} - -/* The clip removes the border on the bevel, so restate the seam explicitly. */ -.iw-region--authored::after { - content: ""; - position: absolute; - inset-block-start: 0; - inset-inline-end: 0; - width: var(--edge-clip-major); - height: var(--edge-clip-major); - background: linear-gradient( - to bottom left, - var(--border-essential) 0 var(--edge-boundary-essential), - transparent var(--edge-boundary-essential) - ); - pointer-events: none; -} - -/* Identity-led region: current position or branded authored area. */ -.iw-region--identity { - border-inline-start: var(--edge-state-emphasis) solid var(--identity); -} - -/* - * Signature: calibrated indexing. A labelled scale tied to real quantity, - * position, or sequence. Anti-rule: never decorative — each index must state a - * real position, so it lives in markup as data-index rather than as a separate - * decorative rail that can drift out of alignment with its rows. - */ -.iw-index-list { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: var(--space-dense); -} - -.iw-index-list > li { - display: grid; - grid-template-columns: auto minmax(0, 1fr); - gap: var(--space-dense); - align-items: baseline; -} - -.iw-index-list > li::before { - content: attr(data-index); - align-self: stretch; - padding-inline-end: var(--space-compact); - border-inline-end: var(--edge-boundary-essential) solid var(--rule-subtle); - font-family: var(--font-data); - font-size: var(--font-size-caption); - font-variant-numeric: tabular-nums; - color: var(--text-secondary); -} - -/* Current and completed positions are marked by weight and edge, not colour. */ -.iw-index-list > li[aria-current] { - font-weight: var(--font-weight-title); -} - -.iw-index-list > li[aria-current]::before { - border-inline-end-color: var(--identity); - border-inline-end-width: var(--edge-state-emphasis); - color: var(--text-primary); -} - -.iw-index-list > li[data-complete]::before { - border-inline-end-color: var(--success); - border-inline-end-width: var(--edge-state-emphasis); -} - -/* - * Signature: joined assemblies. Related controls share an outer boundary and - * an aligned seam while remaining individually operable. - */ -.iw-assembly { - display: inline-flex; - align-items: stretch; - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - overflow: hidden; - max-width: 100%; -} - -.iw-assembly > * { - border-radius: 0; - border-block: none; - border-inline-start: none; - border-inline-end: var(--edge-boundary-essential) solid var(--border-essential); - margin: 0; -} - -.iw-assembly > *:last-child { - border-inline-end: none; -} - -/* Keep the focus ring readable above the shared seam. */ -.iw-assembly > *:focus-visible { - outline-offset: calc(var(--edge-focus-offset) * -1); - position: relative; - z-index: 1; -} - -/* ========================================================================== - 5. Buttons - ========================================================================== */ - -.iw-button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-compact); - min-height: var(--iw-control-height); - min-width: var(--target-minimum); - padding-inline: var(--space-standard); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-primary); - color: var(--text-primary); - font: inherit; - font-size: var(--font-size-body-compact); - font-weight: var(--font-weight-emphasis); - text-align: center; - text-decoration: none; - cursor: pointer; - /* Signature: settled state change. Direct easing, no bounce or overshoot. */ - transition: - background-color var(--motion-duration-press-fast) var(--motion-easing-direct), - border-color var(--motion-duration-press-fast) var(--motion-easing-direct); -} - -.iw-button:hover { - background: var(--surface-hover); -} - -.iw-button:active { - background: var(--surface-grouped); -} - -.iw-button[disabled], -.iw-button[aria-disabled="true"] { - cursor: not-allowed; - opacity: 0.55; -} - -/* One primary action leads a normal task region. */ -.iw-button--primary { - background: var(--identity); - border-color: var(--identity); - color: var(--identity-ink); - font-weight: var(--font-weight-title); -} - -.iw-button--primary:hover, -.iw-button--primary:active { - background: var(--identity); - border-color: var(--text-primary); -} - -/* - * Destructive actions carry a text label plus a colour role. Colour alone never - * communicates the destructive meaning. - */ -.iw-button--danger { - color: var(--danger); - border-color: var(--danger); -} - -.iw-button--danger:hover { - background: var(--surface-hover); -} - -.iw-button--quiet { - border-color: transparent; - background: transparent; -} - -.iw-button--quiet:hover { - background: var(--surface-hover); - border-color: var(--border-essential); -} - -.iw-button--compact { - min-height: var(--target-compact-row); - padding-inline: var(--space-dense); - font-size: var(--font-size-label-dense); -} - -/* Toolbar of joined actions. */ -.iw-toolbar { - display: flex; - flex-wrap: wrap; - gap: var(--space-compact); - align-items: center; - padding-block: var(--space-compact); -} - -/* ========================================================================== - 6. Fields and validation - ========================================================================== */ - -.iw-field { - display: grid; - gap: var(--space-detail); - min-width: 0; -} - -.iw-label { - font-size: var(--font-size-label-dense); - line-height: var(--line-height-label-dense); - font-weight: var(--font-weight-label-dense); -} - -.iw-input, -.iw-select, -.iw-textarea { - min-height: var(--iw-control-height); - width: 100%; - padding: var(--space-compact) var(--space-dense); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-primary); - color: var(--text-primary); - font: inherit; - font-size: var(--font-size-body-compact); -} - -.iw-textarea { - min-height: calc(var(--iw-control-height) * 2.5); - line-height: var(--line-height-body); - resize: vertical; -} - -.iw-input::placeholder, -.iw-textarea::placeholder { - color: var(--text-secondary); -} - -.iw-input:disabled, -.iw-select:disabled, -.iw-textarea:disabled { - background: var(--surface-grouped); - cursor: not-allowed; -} - -/* - * Invalid state uses a thickened edge plus a text message, never colour alone. - * Pair with aria-invalid and aria-describedby on the control. - */ -.iw-input[aria-invalid="true"], -.iw-select[aria-invalid="true"], -.iw-textarea[aria-invalid="true"] { - border-color: var(--danger); - border-inline-start-width: var(--edge-state-emphasis); -} - -/* Helper text. Never carries errors or required instructions alone. */ -.iw-help { - margin: 0; - font-size: var(--font-size-caption); - line-height: var(--line-height-label-dense); - color: var(--text-secondary); -} - -/* Inline validation message. Colour is on Surface 1 only (4.5:1 verified). */ -.iw-message { - display: flex; - gap: var(--space-detail); - align-items: baseline; - margin: 0; - font-size: var(--font-size-label-dense); - line-height: var(--line-height-label-dense); - font-weight: var(--font-weight-emphasis); -} - -.iw-message::before { - font-family: var(--font-data); - font-weight: var(--font-weight-title); -} - -/* - * Markers follow the state vocabulary in foundations/design-language.md §9.2, - * the same one .iw-status uses: a warning is a triangle, an error an - * exclamation, a success a check. Drawing a warning with the error's glyph - * would leave colour as the only thing separating the two. - */ -.iw-message--error { - color: var(--danger); -} - -.iw-message--error::before { - content: "!"; -} - -.iw-message--warning { - color: var(--warning); -} - -.iw-message--warning::before { - content: "\25B2"; -} - -.iw-message--success { - color: var(--success); -} - -.iw-message--success::before { - content: "\2713"; -} - -/* Checkbox, radio, and their labels. */ -.iw-choice { - display: flex; - gap: var(--space-compact); - align-items: center; - min-height: var(--iw-control-height); - cursor: pointer; -} - -.iw-checkbox, -.iw-radio { - inline-size: var(--space-standard); - block-size: var(--space-standard); - accent-color: var(--identity); - margin: 0; - flex: none; - cursor: pointer; -} - -.iw-fieldset { - margin: 0; - padding: var(--iw-region-padding); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -.iw-fieldset > legend { - padding-inline: var(--space-compact); - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-label-dense); -} - -/* C36 Radio group. One choice from a short, fully visible set. The group is a - fieldset so the legend names the choice for assistive technology; the - indicator aligns to the first line of a wrapped label rather than to the - centre of a two-line block. */ -.iw-radio-group { - margin: 0; - padding: 0; - border: 0; - display: flex; - flex-direction: column; - gap: var(--space-compact); -} - -.iw-radio-group > legend { - padding: 0; - margin-block-end: var(--space-compact); - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-label-dense); -} - -.iw-radio-group .iw-choice { - align-items: flex-start; - min-height: 0; - padding-block: calc(var(--space-compact) / 2); -} - -.iw-radio-group .iw-radio { - /* Centre the control on the first line of the label rather than on the - whole label box, so a wrapped option stays aligned at 200% zoom. */ - margin-block-start: calc((1lh - var(--space-standard)) / 2); -} - -.iw-radio-group[data-invalid="true"] > legend { - color: var(--text-critical, currentColor); -} - -/* Two-column form grid that reflows complete groups, never split fields. */ -.iw-form-grid { - display: grid; - gap: var(--space-standard); - grid-template-columns: 1fr; -} - -@media (min-width: 768px) { - .iw-form-grid--two { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -/* ========================================================================== - 7. Table - ========================================================================== */ - -/* - * The table may scroll internally when a stacked representation would destroy - * comparison. A scrollable region must be reachable and announced, so mark it up - * as
. - */ -.iw-table-wrap { - overflow-x: auto; - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -.iw-table { - width: 100%; - border-collapse: collapse; - font-size: var(--font-size-body-compact); -} - -.iw-table caption { - padding: var(--space-dense); - text-align: start; - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-label-dense); - color: var(--text-secondary); -} - -.iw-table th, -.iw-table td { - padding: var(--space-compact) var(--space-dense); - text-align: start; - vertical-align: baseline; - border-block-end: var(--edge-boundary-essential) solid var(--rule-subtle); -} - -.iw-table thead th { - height: var(--target-compact-row); - background: var(--surface-grouped); - border-block-end: var(--edge-boundary-essential) solid var(--border-essential); - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-label-dense); - white-space: nowrap; - position: sticky; - top: 0; - z-index: 1; -} - -.iw-table tbody tr { - height: var(--iw-row-height); -} - -.iw-table tbody tr:hover { - background: var(--surface-hover); -} - -.iw-table tbody tr:last-child td { - border-block-end: none; -} - -/* Numeric and date columns keep stable alignment across rows. */ -.iw-table [data-numeric] { - font-variant-numeric: tabular-nums; - text-align: right; - white-space: nowrap; -} - -/* - * Selected row: identity edge plus a checked control. The row is never - * identified by background colour alone. - */ -.iw-table tbody tr[aria-selected="true"] { - background: var(--surface-grouped); - box-shadow: inset var(--edge-state-emphasis) 0 0 0 var(--identity); -} - -/* Sortable header button sits inside the th and keeps native semantics. */ -.iw-sort { - display: inline-flex; - align-items: center; - gap: var(--space-detail); - padding: var(--space-detail) 0; - border: none; - background: none; - color: inherit; - font: inherit; - font-weight: var(--font-weight-label-dense); - cursor: pointer; -} - -.iw-sort::after { - content: "\2195"; - font-family: var(--font-data); - color: var(--text-secondary); -} - -th[aria-sort="ascending"] .iw-sort::after { - content: "\2191"; - color: var(--text-primary); -} - -th[aria-sort="descending"] .iw-sort::after { - content: "\2193"; - color: var(--text-primary); -} - -/* ========================================================================== - 8. Status, priority, and badges - ========================================================================== */ - -/* - * State is carried by the text label. The marker adds a second, non-colour - * channel so status survives greyscale and forced-colors. - */ -.iw-status { - display: inline-flex; - align-items: baseline; - gap: var(--space-detail); - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-label-dense); - white-space: nowrap; -} - -.iw-status::before { - content: "\25A1"; - font-family: var(--font-data); -} - -.iw-status[data-state="ready"]::before { - content: "\25A0"; - color: var(--success); -} - -.iw-status[data-state="in-progress"]::before { - content: "\25D0"; - color: var(--identity); -} - -.iw-status[data-state="at-risk"]::before { - content: "\25B2"; - color: var(--warning); -} - -.iw-status[data-state="blocked"]::before { - content: "\2715"; - color: var(--danger); -} - -.iw-status[data-state="complete"]::before { - content: "\2713"; - color: var(--success); -} - -/* Priority uses a monospaced code so P0…P3 align down a column. */ -.iw-priority { - display: inline-flex; - align-items: baseline; - gap: var(--space-detail); - font-family: var(--font-data); - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-label-dense); - white-space: nowrap; -} - -.iw-priority::before { - content: "\2014\2014\2014"; - letter-spacing: -0.15em; - color: var(--rule-subtle); -} - -.iw-priority[data-priority="p0"]::before { - content: "\2588\2588\2588"; - letter-spacing: 0; - color: var(--danger); -} - -.iw-priority[data-priority="p1"]::before { - content: "\2588\2588\2014"; - letter-spacing: 0; - color: var(--warning); -} - -.iw-priority[data-priority="p2"]::before { - content: "\2588\2014\2014"; - letter-spacing: 0; - color: var(--text-secondary); -} - -.iw-badge { - display: inline-flex; - align-items: center; - gap: var(--space-detail); - padding: 0 var(--space-compact); - min-height: var(--target-minimum); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-grouped); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-label-dense); - white-space: nowrap; -} - -.iw-badge--identity { - border-color: var(--identity); - border-inline-start-width: var(--edge-state-emphasis); -} - -/* ========================================================================== - 9. Callouts - ========================================================================== */ - -/* - * Callouts sit on Surface 1 so the coloured title text keeps its verified - * contrast ratio. The left edge and the title word carry the meaning together. - */ -.iw-callout { - display: grid; - align-content: start; - gap: var(--space-detail); - padding: var(--space-dense) var(--space-standard); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-inline-start-width: var(--edge-state-emphasis); - border-radius: var(--edge-radius-authored); -} - -.iw-callout > p { - margin: 0; -} - -.iw-callout-title { - margin: 0; - font-size: var(--font-size-label-dense); - line-height: var(--line-height-label-dense); - font-weight: var(--font-weight-title); -} - -.iw-callout--warning { - border-inline-start-color: var(--warning); -} - -.iw-callout--warning .iw-callout-title { - color: var(--warning); -} - -.iw-callout--danger { - border-inline-start-color: var(--danger); -} - -.iw-callout--danger .iw-callout-title { - color: var(--danger); -} - -.iw-callout--success { - border-inline-start-color: var(--success); -} - -.iw-callout--success .iw-callout-title { - color: var(--success); -} - -.iw-callout--identity { - border-inline-start-color: var(--identity); -} - -/* ========================================================================== - 10. Navigation - ========================================================================== */ - -.iw-nav { - display: grid; - gap: var(--space-detail); -} - -.iw-nav ul { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: var(--space-detail); -} - -.iw-nav a { - display: flex; - align-items: center; - min-height: var(--iw-control-height); - padding: var(--space-compact) var(--space-dense); - border-inline-start: var(--edge-state-emphasis) solid transparent; - color: var(--text-primary); - text-decoration: none; -} - -.iw-nav a:hover { - background: var(--surface-hover); -} - -/* Current position: identity edge plus a weight change, never colour alone. */ -.iw-nav a[aria-current] { - border-inline-start-color: var(--identity); - background: var(--surface-grouped); - font-weight: var(--font-weight-title); -} - -.iw-breadcrumb { - font-size: var(--font-size-label-dense); - color: var(--text-secondary); -} - -.iw-breadcrumb ol { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: var(--space-compact); - margin: 0; - padding: 0; -} - -.iw-breadcrumb li + li::before { - content: "/"; - margin-inline-end: var(--space-compact); - color: var(--rule-subtle); -} - -.iw-breadcrumb a { - color: inherit; -} - -.iw-breadcrumb [aria-current] { - color: var(--text-primary); - font-weight: var(--font-weight-emphasis); -} - -/* Tabs. Use with role="tablist" and native buttons. */ -.iw-tabs { - display: flex; - flex-wrap: wrap; - gap: 0; - border-block-end: var(--edge-boundary-essential) solid var(--border-essential); -} - -.iw-tab { - min-height: var(--iw-control-height); - padding: var(--space-compact) var(--space-standard); - border: none; - border-block-end: var(--edge-state-emphasis) solid transparent; - background: none; - color: var(--text-secondary); - font: inherit; - font-size: var(--font-size-body-compact); - font-weight: var(--font-weight-emphasis); - cursor: pointer; -} - -.iw-tab:hover { - color: var(--text-primary); - background: var(--surface-hover); -} - -.iw-tab[aria-selected="true"] { - color: var(--text-primary); - border-block-end-color: var(--identity); - font-weight: var(--font-weight-title); -} - -/* ========================================================================== - 11. Dialog - ========================================================================== */ - -.iw-dialog { - padding: 0; - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-primary); - color: var(--text-primary); - max-width: min(560px, calc(100vw - var(--space-section))); - font-family: var(--font-interface); -} - -.iw-dialog::backdrop { - background: rgb(0 0 0 / 0.55); -} - -.iw-dialog-header, -.iw-dialog-footer { - padding: var(--space-standard); -} - -.iw-dialog-header { - border-block-end: var(--edge-boundary-essential) solid var(--rule-subtle); -} - -.iw-dialog-body { - padding: var(--space-standard); - display: grid; - gap: var(--space-standard); -} - -.iw-dialog-footer { - display: flex; - flex-wrap: wrap; - gap: var(--space-compact); - justify-content: flex-end; - border-block-start: var(--edge-boundary-essential) solid var(--rule-subtle); -} - -/* ========================================================================== - 12. Feedback and progression - ========================================================================== */ - -/* Live status region. Pair with role="status" and aria-live="polite". */ -.iw-live { - margin: 0; - padding: var(--space-compact) var(--space-dense); - background: var(--surface-grouped); - border-inline-start: var(--edge-state-emphasis) solid var(--identity); - border-radius: var(--edge-radius-authored); - font-size: var(--font-size-body-compact); -} - -.iw-empty { - display: grid; - gap: var(--space-compact); - justify-items: start; - padding: var(--space-section) var(--space-standard); - border: var(--edge-boundary-essential) dashed var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -/* Bulk action bar for a filtered result. */ -.iw-bulk-bar { - display: flex; - flex-wrap: wrap; - gap: var(--space-compact); - align-items: center; - padding: var(--space-compact) var(--space-dense); - background: var(--surface-grouped); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - font-size: var(--font-size-body-compact); -} - -/* - * Signature: calibrated indexing applied to progression. Each step shows a real - * position in a real sequence. - */ -.iw-steps { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: var(--space-standard); - margin: 0; - padding: 0; - font-size: var(--font-size-label-dense); -} - -.iw-steps li { - display: flex; - align-items: baseline; - gap: var(--space-compact); - padding-block-end: var(--space-compact); - border-block-end: var(--edge-state-emphasis) solid var(--rule-subtle); - color: var(--text-secondary); -} - -.iw-steps li::before { - content: attr(data-step); - font-family: var(--font-data); - font-variant-numeric: tabular-nums; -} - -.iw-steps li[aria-current] { - color: var(--text-primary); - border-block-end-color: var(--identity); - font-weight: var(--font-weight-title); -} - -.iw-steps li[data-complete] { - color: var(--text-primary); - border-block-end-color: var(--success); -} - -/* Determinate meter tied to a stated quantity. */ -.iw-meter { - display: grid; - gap: var(--space-detail); -} - -.iw-meter-track { - height: var(--space-compact); - background: var(--surface-grouped); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - overflow: hidden; -} - -.iw-meter-fill { - height: 100%; - background: var(--identity); -} - -/* ========================================================================== - 13. Catalog components (C36-C79) - ========================================================================== - The classes below cover the components admitted on platform convention. - Each one styles a native control or a plain container around one: the - behaviour stays with the platform, only the appearance is ours. - ========================================================================== */ - -/* C37 Divider with an optional label. A rule that says what it separates. */ -.iw-divider { - display: grid; - grid-template-columns: auto 1fr; - align-items: center; - gap: var(--space-compact); - margin-block: var(--space-standard); - font-size: var(--font-size-label-dense); - color: var(--text-secondary); -} - -.iw-divider::after { - content: ""; - height: var(--edge-boundary-essential); - background: var(--rule-subtle); -} - -/* C38 Card. A surface that carries one subject and its actions. */ -.iw-card { - display: grid; - gap: var(--space-compact); - padding: var(--space-standard); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -.iw-card-title { - margin: 0; - font-size: var(--font-size-section-title); - font-weight: var(--font-weight-title); -} - -/* C39 Avatar. Initials by default; an image only when one exists. */ -.iw-avatar { - display: inline-flex; - align-items: center; - justify-content: center; - inline-size: var(--iw-avatar-size, 2rem); - block-size: var(--iw-avatar-size, 2rem); - border-radius: 50%; - border: var(--edge-boundary-essential) solid var(--border-essential); - background: var(--surface-grouped); - font-size: var(--font-size-label-dense); - font-weight: var(--font-weight-title); -} - -.iw-identity-row { - display: flex; - align-items: center; - gap: var(--space-compact); -} - -/* C40 Skeleton. A placeholder with the shape of the content it waits for. */ -.iw-skeleton { - display: block; - block-size: var(--space-standard); - background: var(--surface-grouped); - border: var(--edge-boundary-essential) solid var(--rule-subtle); - border-radius: var(--edge-radius-authored); -} - -.iw-skeleton + .iw-skeleton { - margin-block-start: var(--space-compact); -} - -/* C41 Tag. A user-applied label, removable where the user applied it. */ -.iw-tag { - display: inline-flex; - align-items: center; - gap: var(--space-detail); - padding: 0 var(--space-compact); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-grouped); - font-size: var(--font-size-label-dense); -} - -/* C42 Tooltip and C66 shortcut hint. Supporting text, never the only route. */ -.iw-kbd { - font-family: var(--font-data); - font-size: var(--font-size-label-dense); - padding: 0 var(--space-detail); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-grouped); -} - -.iw-shortcut-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: var(--space-compact); - align-items: baseline; -} - -/* C67 Icon. Sized from the icon scale rather than from the text around it, and - coloured by inheritance so it follows every contrast decision already made for - that text. The inline size is a floor: dense contexts keep it instead of - shrinking, because the drawings stop resolving before the type does. */ -.iw-icon { - inline-size: var(--icon-size-inline); - block-size: var(--icon-size-inline); - vertical-align: -0.125em; - flex: none; -} - -.iw-icon--standard { - inline-size: var(--icon-size-standard); - block-size: var(--icon-size-standard); -} - -.iw-icon--prominent { - inline-size: var(--icon-size-prominent); - block-size: var(--icon-size-prominent); -} - -/* C68 Image or thumbnail. The frame holds its ratio before the image arrives. */ -.iw-figure { - margin: 0; - display: grid; - gap: var(--space-detail); - max-inline-size: 30rem; -} - -.iw-figure-frame { - aspect-ratio: 16 / 9; - border: var(--edge-boundary-essential) solid var(--border-essential); - background: var(--surface-grouped); - overflow: hidden; -} - -.iw-figure-frame > img { - inline-size: 100%; - block-size: 100%; - object-fit: cover; - display: block; -} - -/* C69 Count badge. The number is decoration here; the host carries it in words. */ -.iw-count-host { - display: inline-flex; - align-items: center; - gap: var(--space-detail); -} - -.iw-count { - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: 1em; - padding: 0 0.4em; - font-family: var(--font-data); - font-size: var(--font-size-label-dense); -} - -/* C70 Metric tile. One quantity, its period, and how it moved. */ -.iw-metrics { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--iw-metric-min, 14rem)), 1fr)); - gap: var(--space-compact); -} - -.iw-metric { - margin: 0; - display: grid; - gap: var(--space-detail); - padding: var(--space-compact); - border: var(--edge-boundary-essential) solid var(--border-essential); - min-inline-size: 0; -} - -.iw-metric-label { - color: var(--text-secondary); - font-size: var(--font-size-label-dense); -} - -.iw-metric-value { - margin: 0; - font-family: var(--font-data); - font-size: var(--font-size-heading); -} - -.iw-metric-period { - margin: 0; - color: var(--text-secondary); - font-size: var(--font-size-label-dense); -} - -/* C71 Sparkline. One ink, no axes, and the numbers live in the text beside it. */ -.iw-sparkline { - inline-size: 100%; - block-size: 2rem; - color: var(--text-primary); -} - -/* C72 Bar comparison. A table of labelled numbers; the bars are length only. */ -.iw-comparison { - inline-size: 100%; - border-collapse: collapse; -} - -.iw-comparison caption { - text-align: start; - color: var(--text-secondary); - padding-block-end: var(--space-detail); -} - -.iw-comparison th, -.iw-comparison td { - padding: var(--space-detail) var(--space-compact); - text-align: start; - font-weight: inherit; -} - -.iw-comparison td { - font-family: var(--font-data); -} - -.iw-bar { - display: block; - block-size: 0.5rem; - inline-size: var(--iw-bar-length, 0%); - background: var(--text-primary); - margin-block-end: var(--space-detail); -} - -/* C43 Slider. The native range plus the value it stands for, as text. */ -.iw-range { - inline-size: 100%; - max-inline-size: 22rem; - accent-color: var(--identity); -} - -/* C47/C48 Combo box and multi-select. Native list semantics, our spacing. */ -.iw-option-list { - list-style: none; - margin: 0; - padding: var(--space-compact); - display: grid; - gap: var(--space-compact); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - background: var(--surface-primary); -} - -/* C50 Segmented control. One row of radios that behaves as one control. */ -.iw-segmented { - display: inline-flex; - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - overflow: hidden; -} - -.iw-segmented label { - padding: var(--space-compact) var(--space-dense); - font-size: var(--font-size-label-dense); - border-inline-start: var(--edge-boundary-essential) solid var(--border-essential); -} - -.iw-segmented label:first-of-type { - border-inline-start: 0; -} - -.iw-segmented input { - position: absolute; - opacity: 0; - pointer-events: none; -} - -.iw-segmented label:has(input:checked) { - background: var(--identity); - color: var(--identity-ink); - font-weight: var(--font-weight-title); -} - -.iw-segmented label:has(input:focus-visible) { - outline: var(--edge-focus-ring) solid var(--focus-ring); - outline-offset: calc(var(--edge-focus-ring) * -1); -} - -/* C52 Tree view. Structure carried by indentation and expanded state. */ -.iw-tree, -.iw-tree ul { - list-style: none; - margin: 0; - padding: 0; -} - -.iw-tree ul { - padding-inline-start: var(--space-standard); - border-inline-start: var(--edge-boundary-essential) solid var(--rule-subtle); - margin-inline-start: var(--space-compact); -} - -.iw-tree [role="treeitem"] > span { - display: inline-block; - padding: var(--space-detail) 0; -} - -/* C55/C56 Progress and activity. Every wait names what it is waiting for. */ -.iw-progress { - inline-size: 100%; - max-inline-size: 22rem; - block-size: var(--space-compact); - accent-color: var(--identity); -} - -.iw-activity { - display: flex; - align-items: center; - gap: var(--space-compact); -} - -.iw-activity-mark { - inline-size: 1rem; - block-size: 1rem; - border: var(--edge-state-emphasis) solid currentColor; - border-right-color: transparent; - border-radius: 50%; - animation: iw-spin 1s linear infinite; -} - -@keyframes iw-spin { - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: reduce) { - .iw-activity-mark { - animation: none; - border-right-color: currentColor; - } -} - -/* C57/C59/C60 Popover, drawer, and coach mark: three surfaces, one edge. */ -.iw-surface { - padding: var(--space-dense); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - display: grid; - gap: var(--space-compact); - justify-items: start; -} - -.iw-surface--coach { - border-inline-start-width: var(--edge-state-emphasis); - border-inline-start-color: var(--identity); -} - -/* C61 Accordion. Sibling disclosures presented as one list of headings. */ -.iw-accordion { - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - overflow: hidden; -} - -.iw-accordion-heading { - margin: 0; - font-size: inherit; -} - -.iw-accordion-trigger { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: var(--space-compact); - inline-size: 100%; - text-align: start; - padding: var(--space-dense); - background: none; - border: 0; - border-block-end: var(--edge-boundary-essential) solid var(--rule-subtle); - color: inherit; - font: inherit; - font-weight: var(--font-weight-title); - cursor: pointer; -} - -.iw-accordion-panel { - padding: var(--space-dense); - border-block-end: var(--edge-boundary-essential) solid var(--rule-subtle); -} - -/* C62 Context menu. The same actions the visible control already offers. */ -.iw-menu { - list-style: none; - margin: 0; - padding: var(--space-compact); - display: grid; - gap: var(--space-detail); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - min-inline-size: 12rem; -} - -.iw-menu [role="menuitem"] { - display: block; - inline-size: 100%; - text-align: start; - padding: var(--space-compact); - background: none; - border: 0; - color: inherit; - font: inherit; - cursor: pointer; -} - -.iw-menu [role="menuitem"][aria-disabled="true"] { - color: var(--text-secondary); - cursor: default; -} - -.iw-menu [role="menuitem"]:hover:not([aria-disabled="true"]) { - background: var(--surface-grouped); -} - -/* C63 Split view. A divider that is a control, not a decoration. */ -.iw-split { - display: grid; - grid-template-columns: minmax(0, 2fr) auto minmax(0, 3fr); - gap: var(--space-dense); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); - padding: var(--space-dense); -} - -.iw-split-divider { - inline-size: var(--edge-state-emphasis); - background: var(--border-essential); - border: 0; - padding: 0; - cursor: col-resize; -} - -@media (max-width: 47.9375rem) { - .iw-split { - grid-template-columns: minmax(0, 1fr); - } - - .iw-split-divider { - display: none; - } -} - -/* C65 Colour picker. The swatch is decorative; the value is the text. */ -.iw-swatch { - inline-size: 1.5rem; - block-size: 1.5rem; - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -/* ========================================================================== - 14. Utilities - ========================================================================== */ - -.iw-visually-hidden { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; - border: 0; -} - -.iw-skip-link { - position: absolute; - inset-inline-start: var(--space-standard); - inset-block-start: calc(var(--space-standard) * -4); - z-index: 10; - padding: var(--space-compact) var(--space-standard); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - color: var(--text-primary); -} - -.iw-skip-link:focus { - inset-block-start: var(--space-standard); -} - -.iw-rule { - border: none; - border-block-start: var(--edge-boundary-essential) solid var(--rule-subtle); - margin-block: var(--space-standard); -} - -/* A field that spans every column of an iw-form-grid. */ -.iw-field--full { - grid-column: 1 / -1; -} - -/* ========================================================================== - 15. Environment adaptation - ========================================================================== */ - -@media (prefers-reduced-motion: reduce) { - .iw-root *, - .iw-root *::before, - .iw-root *::after { - transition-duration: var(--motion-duration-reduced) !important; - animation-duration: var(--motion-duration-reduced) !important; - scroll-behavior: auto !important; - } -} - -/* - * Forced colors: hand every colour decision back to the user's palette and keep - * the non-colour channels (markers, edges, weight) doing the work. - */ -/* -------------------------------------------------------------------------- - C73-C79 Commands. Dialog, toggle button, icon button, button group, split - button, menu bar, and inline message. Each one wraps a native control; the - modality, the pressed state, and the menu semantics stay with the platform. - -------------------------------------------------------------------------- */ - -/* C73 Dialog. `` owns modality, focus, and Escape. */ -.iw-dialog { - max-inline-size: min(32rem, calc(100vw - 2rem)); - padding: var(--space-section); - background: var(--surface-primary); - color: var(--text-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); - border-radius: var(--edge-radius-authored); -} - -.iw-dialog::backdrop { - background: rgb(0 0 0 / 45%); -} - -.iw-dialog-title { - margin: 0 0 var(--space-detail); - font-size: var(--font-size-section-title); - line-height: var(--line-height-section-title); -} - -.iw-dialog-description { - margin: 0 0 var(--space-group); - max-inline-size: var(--measure-prose); - color: var(--text-secondary); -} - -.iw-dialog-actions { - display: flex; - flex-wrap: wrap; - gap: var(--space-dense); - justify-content: flex-end; - margin-block-start: var(--space-group); -} - -/* C74 Toggle button. The label names the mode; `aria-pressed` carries state, - and the inset rule repeats it without relying on colour. */ -.iw-toggle-button[aria-pressed="true"] { - background: var(--surface-grouped); - border-color: var(--border-essential); - box-shadow: inset var(--edge-signature) 0 0 0 var(--identity); -} - -/* C75 Icon button. Square and never below the pointer target size. */ -.iw-icon-button { - display: inline-grid; - place-items: center; - min-inline-size: var(--target-pointer); - min-block-size: var(--target-pointer); - padding: var(--space-detail); -} - -.iw-icon-button > svg { - inline-size: var(--icon-size-inline); - block-size: var(--icon-size-inline); - display: block; -} - -/* C76 Button group. Related actions share one border and one accessible name. */ -.iw-button-group { - display: flex; - flex-wrap: wrap; -} - -.iw-button-group > * + * { - margin-inline-start: -1px; -} - -/* C77 Split button. A default action and a menu of variants, joined. */ -.iw-split-button { - display: inline-flex; - position: relative; - inline-size: fit-content; -} - -.iw-split-button > .iw-split-button-menu { - margin-inline-start: -1px; -} - -.iw-split-button > .iw-menu { - position: absolute; - inset-block-start: 100%; - inset-inline-end: 0; - z-index: 5; -} - -/* C78 Menu bar. One tab stop; arrow keys move between titles. */ -.iw-menubar { - display: flex; - flex-wrap: wrap; - position: relative; - gap: var(--space-detail); - border-block-end: var(--edge-boundary-subtle) solid var(--border-subtle); - padding-block-end: var(--space-detail); -} - -.iw-menubar > [role="menuitem"] { - padding: var(--space-detail) var(--space-dense); - background: none; - border: var(--edge-boundary-subtle) solid transparent; - color: inherit; - font: inherit; - cursor: pointer; -} - -.iw-menubar > [role="menuitem"]:hover, -.iw-menubar > [role="menuitem"][aria-expanded="true"] { - border-color: var(--border-essential); - background: var(--surface-grouped); -} - -.iw-menubar .iw-menu { - position: absolute; - inset-block-start: 100%; - inset-inline-start: 0; - z-index: 5; -} - -.iw-menubar .iw-menu[hidden], -.iw-split-button .iw-menu[hidden] { - display: none; -} - -/* C79 Inline message. The severity is a word before it is a colour. */ -.iw-inline-message { - display: grid; - gap: var(--space-detail); - justify-items: start; - padding: var(--space-dense); - background: var(--surface-grouped); - border: var(--edge-boundary-subtle) solid var(--border-subtle); - border-inline-start: var(--edge-signature) solid var(--identity); -} - -.iw-inline-message[data-severity="error"] { - border-inline-start-color: var(--status-critical); -} - -.iw-inline-message[data-severity="warning"] { - border-inline-start-color: var(--status-caution); -} - -.iw-inline-message[data-severity="success"] { - border-inline-start-color: var(--status-affirmative); -} - -.iw-inline-message-severity { - margin: 0; - font-weight: var(--font-weight-emphasis); -} - -.iw-inline-message-body { - margin: 0; - max-inline-size: var(--measure-prose); -} - -/* -------------------------------------------------------------------------- - C80-C86 Capacity, periods, hierarchy, and stepping. A meter, a date range, - a month grid, a tree grid, an inline edit, a skip link, and a stepper. - -------------------------------------------------------------------------- */ - -/* C80 Meter. A level within a fixed capacity; the number beside it is the - statement, the bar only the aid. */ -.iw-meter { - inline-size: 12rem; - block-size: 0.5rem; -} - -.iw-meter-row { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-dense); - margin: 0; -} - -/* C81 Date range. Two fields, one legend, one shared error. */ -.iw-daterange { - display: grid; - gap: var(--space-detail); - padding: var(--space-dense); - border: var(--edge-boundary-subtle) solid var(--border-subtle); - border-radius: var(--edge-radius-authored); -} - -.iw-daterange > legend { - padding-inline: var(--space-detail); - font-weight: var(--font-weight-emphasis); -} - -/* C82 Calendar grid. One month, seven columns, one tab stop. */ -.iw-calendar { - display: grid; - gap: var(--space-detail); - justify-items: start; -} - -.iw-calendar-grid { - border-collapse: collapse; -} - -.iw-calendar-grid th, -.iw-calendar-grid td { - inline-size: var(--target-standard-control); - block-size: var(--target-standard-control); - padding: 0; - text-align: center; -} - -.iw-calendar-grid td > button { - inline-size: 100%; - block-size: 100%; - background: none; - border: var(--edge-boundary-subtle) solid transparent; - color: inherit; - font: inherit; - cursor: pointer; -} - -.iw-calendar-grid td > button:hover { - border-color: var(--border-essential); -} - -.iw-calendar-grid td[aria-selected="true"] > button { - background: var(--surface-grouped); - box-shadow: inset 0 calc(-1 * var(--edge-signature)) 0 0 var(--identity); -} - -.iw-calendar-grid td > button[aria-disabled="true"] { - color: var(--text-secondary); - text-decoration: line-through; - cursor: default; -} - -/* C83 Tree grid. Indentation and a marker carry the level; colour never does. */ -.iw-treegrid { - inline-size: 100%; - border-collapse: collapse; -} - -.iw-treegrid th, -.iw-treegrid td { - text-align: start; - padding: var(--space-detail) var(--space-dense); - border-block-end: var(--edge-boundary-subtle) solid var(--border-subtle); -} - -.iw-treegrid tbody tr[aria-level="2"] > th { - padding-inline-start: calc(var(--space-dense) * 2); -} - -.iw-treegrid tbody tr[aria-level="3"] > th { - padding-inline-start: calc(var(--space-dense) * 3); -} - -.iw-treegrid tbody tr[aria-expanded="true"] > th::before { - content: "▾ "; -} - -.iw-treegrid tbody tr[aria-expanded="false"] > th::before { - content: "▸ "; -} - -/* C84 Inline edit. The trigger is a control, never a hover-revealed underline. */ -.iw-inline-edit, -.iw-stepper { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-dense); - margin: 0; -} - -.iw-inline-edit-label { - color: var(--text-secondary); -} - -/* C85 Skip link. Present in the accessibility tree at all times, visible on - focus. The hiding technique is the one C04 already uses. */ -.iw-skip-link { - position: absolute; - inline-size: 1px; - block-size: 1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; -} - -.iw-skip-link:focus-visible { - position: static; - inline-size: auto; - block-size: auto; - overflow: visible; - clip-path: none; - display: inline-block; - padding: var(--space-detail) var(--space-dense); - background: var(--surface-primary); - border: var(--edge-boundary-essential) solid var(--border-essential); -} - -/* C86 Stepper. Both controls stay at the platform target size. */ -.iw-stepper > button { - min-inline-size: var(--target-pointer); - min-block-size: var(--target-pointer); -} - -.iw-stepper > input { - inline-size: 5rem; -} - -@media (forced-colors: active) { - .iw-button, - .iw-input, - .iw-select, - .iw-textarea, - .iw-region, - .iw-callout, - .iw-badge, - .iw-table-wrap, - .iw-card, - .iw-avatar, - .iw-tag, - .iw-kbd, - .iw-accordion, - .iw-menu, - .iw-menubar .iw-menu, - .iw-dialog, - .iw-inline-message, - .iw-daterange, - .iw-treegrid th, - .iw-treegrid td, - .iw-skip-link:focus-visible, - .iw-split, - .iw-surface, - .iw-option-list, - .iw-segmented, - .iw-swatch, - .iw-figure-frame, - .iw-count, - .iw-metric, - .iw-skeleton { - border-color: CanvasText; - } - - .iw-split-divider, - .iw-divider::after { - background: CanvasText; - } - - /* The bar and the line are drawn in one ink, so the system colour is enough; - both are redundant to the numbers beside them in any case. */ - .iw-bar { - background: CanvasText; - outline: 1px solid CanvasText; - } - - .iw-sparkline { - color: CanvasText; - } - - .iw-segmented label:has(input:checked) { - forced-color-adjust: none; - background: Highlight; - color: HighlightText; - } - - .iw-root :focus-visible { - outline-color: Highlight; - } - - .iw-nav a[aria-current], - .iw-tab[aria-selected="true"], - .iw-table tbody tr[aria-selected="true"] { - forced-color-adjust: none; - background: Highlight; - color: HighlightText; - } - - .iw-meter-fill { - background: Highlight; - } - - .iw-calendar-grid td[aria-selected="true"] > button { - forced-color-adjust: none; - background: Highlight; - color: HighlightText; - } - - .iw-toggle-button[aria-pressed="true"] { - forced-color-adjust: none; - background: Highlight; - color: HighlightText; - } - - /* The native radio indicator is the only selection cue here, so let the - system draw it rather than the accent colour. */ - .iw-radio-group .iw-radio { - forced-color-adjust: auto; - } -} - -/* 14. Extended catalog, C87-C97. Consumable classes for real applications: - tag input, listbox, avatar group, presence, code block, drop zone, scroll - area, command palette, inline confirmation, character count, inset callout. */ - -.iw-tag-list { - display: flex; - flex-wrap: wrap; - gap: var(--space-compact); - margin: 0; - padding: 0; - list-style: none; -} - -.iw-tag { - display: inline-flex; - align-items: center; - gap: var(--space-detail); - border: 1px solid var(--border-essential); - background: none; - color: inherit; - font: inherit; - padding: 2px var(--space-compact); - min-block-size: var(--target-pointer); -} - -.iw-listbox { - margin: 0; - padding: 0; - list-style: none; - border: 1px solid var(--border-essential); - max-block-size: 16rem; - overflow-y: auto; -} - -.iw-listbox > [role="option"] { - padding: var(--space-detail) var(--space-dense); - min-block-size: var(--target-pointer); -} - -.iw-listbox > [role="option"][aria-selected="true"]::before { - content: "✓ "; -} - -.iw-avatar-group { - display: flex; - align-items: center; - gap: var(--space-compact); -} - -.iw-avatar-group > ul { - display: flex; - margin: 0; - padding: 0; - list-style: none; -} - -.iw-avatar-group li { - inline-size: 32px; - block-size: 32px; - border-radius: 50%; - border: 2px solid var(--surface-primary); - background: var(--surface-grouped); - display: grid; - place-items: center; - font-size: var(--font-size-caption); - margin-inline-start: -8px; -} - -.iw-avatar-group li:first-child { - margin-inline-start: 0; -} - -.iw-presence::before { - display: inline-block; - inline-size: 1.25em; -} - -.iw-presence[data-state="available"]::before { - content: "●"; -} - -.iw-presence[data-state="busy"]::before { - content: "⊖"; -} - -.iw-presence[data-state="away"]::before { - content: "○"; -} - -.iw-presence[data-state="offline"]::before { - content: "⊘"; -} - -.iw-presence[data-state="unknown"]::before { - content: "?"; -} - -.iw-code-block { - display: grid; - gap: var(--space-detail); - justify-items: start; -} - -.iw-code-block pre { - margin: 0; - padding: var(--space-dense); - border: 1px solid var(--border-essential); - background: var(--surface-grouped); - font-family: var(--font-numeric, ui-monospace, monospace); - white-space: pre; - overflow-x: auto; -} - -.iw-drop-zone { - display: grid; - gap: var(--space-detail); - padding: var(--space-standard); - border: 1px dashed var(--border-essential); - min-block-size: 96px; -} - -.iw-drop-zone[data-dragging="true"] { - border-style: solid; - border-width: var(--edge-signature); -} - -.iw-scroll-area { - overflow: auto; - overscroll-behavior: contain; - scrollbar-gutter: stable; - border: 1px solid var(--border-essential); - padding: var(--space-dense); -} - -.iw-scroll-area:focus-visible { - outline: var(--edge-focus-width, 3px) solid var(--focus); - outline-offset: var(--edge-focus-offset, 3px); -} - -.iw-palette { - inline-size: min(32rem, calc(100vw - 32px)); - padding: var(--space-standard); - border: 1px solid var(--border-essential); - background: var(--surface-primary); - color: var(--text-primary); -} - -.iw-palette ul { - margin: var(--space-dense) 0; - padding: 0; - list-style: none; - max-block-size: 16rem; - overflow-y: auto; -} - -.iw-inline-confirm { - display: grid; - gap: var(--space-compact); - max-inline-size: 24rem; - padding: var(--space-dense); - border: 1px solid var(--border-essential); - background: var(--surface-primary); -} - -.iw-character-count { - display: block; - font-size: var(--font-size-caption); - color: var(--text-secondary); - min-block-size: 1.5em; -} - -.iw-character-count[data-over="true"] { - color: var(--status-critical); - font-weight: var(--font-weight-emphasis); -} - -.iw-inset { - border-inline-start: var(--edge-signature) solid var(--identity); - padding-inline-start: var(--space-standard); - padding-block: var(--space-dense); - background: var(--surface-grouped); - max-inline-size: var(--measure-prose); -} - -@media (forced-colors: active) { - .iw-tag, - .iw-listbox, - .iw-scroll-area, - .iw-palette, - .iw-inline-confirm, - .iw-drop-zone, - .iw-code-block pre { - border-color: CanvasText; - } - - .iw-avatar-group li { - border-color: Canvas; - outline: 1px solid CanvasText; - } - - .iw-inset { - border-inline-start-color: CanvasText; - } -} - -/* --------------------------------------------------------------------------- - C98-C111. The last of the catalogue. Two groups, one rule: the state must - survive greyscale, the operation must survive the absence of a gesture, and - the position must survive not being able to count shapes. - --------------------------------------------------------------------------- */ - -/* C98 Rating */ -.iw-rating { - display: grid; - gap: var(--space-detail, 4px); - justify-items: start; - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); - padding: var(--space-dense, 12px); - margin: 0; -} - -.iw-rating-scale { - display: flex; - gap: var(--space-detail, 4px); - color: var(--identity); - font-size: 1.25em; -} - -.iw-rating-scale label { - display: grid; - place-items: center; - min-inline-size: var(--target-pointer, 24px); - min-block-size: var(--target-pointer, 24px); -} - -.iw-rating-empty { - color: var(--text-quiet); -} - -/* C99 Transfer list */ -.iw-transfer { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--space-dense, 12px); - align-items: start; -} - -@media (min-width: 900px) { - .iw-transfer { - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); - } -} - -.iw-transfer-list { - margin: 0; - padding: 0; - list-style: none; - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); - min-block-size: 6rem; - max-block-size: 12rem; - overflow-y: auto; -} - -.iw-transfer-list [role="option"] { - padding: var(--space-detail, 4px) var(--space-dense, 12px); - min-block-size: var(--target-pointer, 24px); -} - -.iw-transfer-list [role="option"][aria-selected="true"]::before { - content: "✓ "; -} - -.iw-transfer-list [role="option"][aria-selected="false"]::before { - content: " "; - white-space: pre; -} - -.iw-transfer-moves { - display: grid; - gap: var(--space-compact, 8px); - align-content: center; -} - -/* C100 Notification centre */ -.iw-notifications { - display: grid; - gap: var(--space-compact, 8px); - justify-items: start; -} - -.iw-notifications-panel { - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); - background: var(--surface-primary); - padding: var(--space-dense, 12px); - max-inline-size: 24rem; -} - -.iw-notifications-list { - margin: var(--space-compact, 8px) 0; - padding: 0; - list-style: none; - display: grid; - gap: var(--space-detail, 4px); -} - -.iw-notifications-list .is-unread { - font-weight: 600; -} - -.iw-notifications-list .is-unread::before { - content: "● "; -} - -.iw-notifications-list .is-read::before { - content: " "; - white-space: pre; -} - -/* C101 Selection card grid */ -.iw-cardgrid { - display: grid; - gap: var(--space-compact, 8px); -} - -.iw-cardgrid [role="row"] { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); - gap: var(--space-compact, 8px); -} - -.iw-card { - display: grid; - gap: var(--space-detail, 4px); - padding: var(--space-dense, 12px); - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); - min-block-size: 4.5rem; -} - -.iw-card[aria-selected="true"] { - border-width: var(--edge-signature, 3px); -} - -.iw-card[aria-selected="true"] .iw-card-name::before { - content: "✓ "; -} - -.iw-card-fact { - color: var(--text-quiet); - font-size: var(--font-size-caption, 12px); -} - -/* C102 Virtualised list */ -.iw-virtual-block { - display: grid; - gap: var(--space-detail, 4px); -} - -.iw-virtual { - margin: 0; - padding: 0; - list-style: none; - block-size: 6rem; - overflow-y: auto; - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); -} - -.iw-virtual li { - padding-inline: var(--space-dense, 12px); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* C103 One-time code */ -.iw-otp { - font-family: var(--font-numeric, ui-monospace, monospace); - letter-spacing: 0.5em; - inline-size: 9rem; -} - -/* C104 Bottom sheet */ -.iw-sheet { - display: grid; - gap: var(--space-compact, 8px); - justify-items: start; - inline-size: min(100%, 32rem); - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); - background: var(--surface-primary); - padding: var(--space-standard, 16px); - padding-block-end: calc(var(--space-standard, 16px) + env(safe-area-inset-bottom, 0px)); -} - -.iw-sheet-grabber { - inline-size: 2.5rem; - block-size: 4px; - justify-self: center; - background: var(--border-essential); -} - -/* C105 Tab bar */ -.iw-tabbar ul { - display: flex; - margin: 0; - padding: var(--space-compact, 8px) 0; - padding-block-end: calc(var(--space-compact, 8px) + env(safe-area-inset-bottom, 0px)); - list-style: none; - border-block-start: var(--edge-boundary-essential, 1px) solid var(--border-essential); -} - -.iw-tabbar li { - flex: 1 1 0; - min-inline-size: 0; -} - -.iw-tabbar a { - display: grid; - /* Without an explicit track the auto column takes the max-content width of a - long label and pushes the row past a narrow viewport. */ - grid-template-columns: minmax(0, 1fr); - justify-items: center; - gap: 2px; - padding: var(--space-detail, 4px); - min-block-size: var(--target-pointer, 24px); - color: var(--text-secondary); - text-decoration: none; -} - -.iw-tabbar a[aria-current="page"] { - color: var(--identity); - font-weight: 600; - box-shadow: inset 0 3px 0 0 currentColor; -} - -.iw-tabbar-label { - font-size: var(--font-size-caption, 12px); - text-align: center; - min-inline-size: 0; - overflow-wrap: anywhere; -} - -/* C106 Navigation rail */ -.iw-rail ul { - display: grid; - gap: var(--space-detail, 4px); - margin: 0; - padding: var(--space-compact, 8px); - list-style: none; - inline-size: max-content; - border-inline-end: var(--edge-boundary-essential, 1px) solid var(--border-essential); -} - -.iw-rail a { - display: grid; - justify-items: center; - gap: 2px; - padding: var(--space-compact, 8px); - min-inline-size: 4.5rem; - min-block-size: var(--target-standard-control, 40px); - color: var(--text-secondary); - text-decoration: none; -} - -.iw-rail a[aria-current="page"] { - color: var(--identity); - font-weight: 600; - box-shadow: inset 3px 0 0 0 currentColor; -} - -.iw-rail-label { - font-size: var(--font-size-caption, 12px); - text-align: center; -} - -/* C107 Floating action button */ -.iw-fab-block { - display: grid; - gap: var(--space-compact, 8px); - justify-items: start; -} - -.iw-fab { - display: inline-flex; - align-items: center; - gap: var(--space-compact, 8px); - min-block-size: 48px; - padding-inline: var(--space-standard, 16px); - border: var(--edge-boundary-essential, 1px) solid var(--identity); - background: var(--identity); - color: var(--identity-ink); - font: inherit; - cursor: pointer; -} - -/* C108 Swipe actions */ -.iw-swipe-list { - margin: 0; - padding: 0; - list-style: none; - display: grid; - gap: var(--space-detail, 4px); -} - -.iw-swipe-list li { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: var(--space-compact, 8px); - padding: var(--space-compact, 8px); - border-block-end: var(--edge-boundary-subtle, 1px) solid var(--border-quiet); -} - -.iw-swipe-actions { - display: flex; - gap: var(--space-compact, 8px); -} - -/* C109 Pull to refresh */ -.iw-refresh { - display: grid; - gap: var(--space-detail, 4px); - justify-items: start; -} - -/* C110 Page indicator */ -.iw-pages { - display: grid; - gap: var(--space-detail, 4px); - justify-items: start; -} - -.iw-pages-dots { - display: flex; - gap: var(--space-compact, 8px); -} - -.iw-page-dot { - inline-size: var(--target-pointer, 24px); - block-size: var(--target-pointer, 24px); - border: none; - background: none; - padding: 0; - cursor: pointer; - display: grid; - place-items: center; -} - -.iw-page-dot::before { - content: ""; - inline-size: 8px; - block-size: 8px; - border-radius: 50%; - border: var(--edge-boundary-essential, 1px) solid var(--border-essential); -} - -.iw-page-dot[aria-current="true"]::before { - inline-size: 12px; - block-size: 12px; - background: var(--identity); - border-color: var(--identity); -} - -/* C111 Share action */ -.iw-share { - display: grid; - gap: var(--space-detail, 4px); - justify-items: start; -} - -@media (forced-colors: active) { - .iw-card[aria-selected="true"], - .iw-transfer-list [role="option"][aria-selected="true"] { - forced-color-adjust: none; - background: Highlight; - color: HighlightText; - } - - .iw-fab { - background: ButtonFace; - color: ButtonText; - border-color: ButtonText; - } - - .iw-page-dot[aria-current="true"]::before { - background: CanvasText; - border-color: CanvasText; - } - - .iw-tabbar a[aria-current="page"], - .iw-rail a[aria-current="page"] { - color: Highlight; - } -} - -} /* end @layer instrument-workshop */ diff --git a/docs/assets/screenshots/control-panel.png b/docs/assets/screenshots/control-panel.png new file mode 100644 index 0000000..b5f882b Binary files /dev/null and b/docs/assets/screenshots/control-panel.png differ diff --git a/docs/assets/screenshots/orientation-demo.gif b/docs/assets/screenshots/orientation-demo.gif new file mode 100644 index 0000000..6fe3a66 Binary files /dev/null and b/docs/assets/screenshots/orientation-demo.gif differ diff --git a/docs/index.html b/docs/index.html index fa1b8be..25cbf94 100644 --- a/docs/index.html +++ b/docs/index.html @@ -14,176 +14,449 @@ - - - + + - + -Skip to content + -
+
-
-
-

Open source · native macOS app

-

- - OpenPromptr -

-

- OpenPromptr mirrors a display, a window, or a private virtual display - onto a physical teleprompter monitor — rotated and mirrored so it - reads correctly through a mirror-glass rig. -

-

- Maintained - Version 1.1.0 - macOS 13+ - MIT +

+
+
+ +

OpenPromptr

+
+

+ Mirrors a display, a window, or a private virtual display onto a + physical teleprompter monitor — rotated and mirrored so it reads + correctly through the glass.

+
+ Maintained + v1.2.0 + macOS 13+ + MIT licence +
+ +
+ +
+
+ The OpenPromptr control panel: a Virtual display source, an AAA 1920×1080 target display, orientation set to 0° with flip horizontal on, and Output running shown in the top corner. +
+
+ + the actual app, not a mockup +
- - Build from source -
- -
-

What it does

-

- A teleprompter or mirror-glass rig needs one thing: whatever is on - the presenter-view window, mirrored and correctly oriented, on a - second physical display. OpenPromptr captures a source with +


+ +
+

How it works

+

+ A mirror-glass rig needs one thing: whatever is on the presenter-view + window, mirrored and correctly oriented, full-screen on a second + physical display. OpenPromptr captures a source with ScreenCaptureKit, applies rotation and mirroring, and shows the - result full-screen on the target display — nothing else runs, and - unused screen area stays black. + result on the target — nothing else runs, and unused screen area + stays black.

-
-
-

Three source types

-

- A single window (simplest — everything stays usable), a visible - physical display, or a private virtual display for when the - presenter view shouldn't appear anywhere else. -

-
-
-

Rotation and mirroring

-

- 0°/90°/180°/270° rotation plus independent horizontal and - vertical mirroring, with a live preview of the result. -

-
-
-

No feedback loop

-

- Source and target are always separate displays, so the - full-screen output never covers the thing being captured. -

-
-
-

Automatic recovery

-

- Optional automatic retry after a capture or rendering failure, - and automatic resume when a disconnected display reappears. -

-
+
+
+

Source

+

A single window — simplest, everything else stays usable.

+

A visible physical display.

+

A private virtual display, for when the presenter view + shouldn't appear anywhere else.

+
+ +
+

Rotate & mirror

+
+ The orientation control cycling through 0°, 90°, 180°, and 270°, with a live preview glyph that flips and rotates to match. +
0° / 90° / 180° / 270°, plus independent + horizontal and vertical mirroring — live preview included.
+
+
+ +
+

Target display

+

Full-screen output, always on a display separate from the + source — so it never covers the thing being captured.

+

Optional automatic retry after a capture failure, and + automatic resume when a disconnected display reappears.

+
-
+
- -
-

How to get it

- -
-

No signed release yet

-

- Distributable, notarized builds are in progress (tracked in - issue #7). - For now, build it yourself — the app is a small SwiftPM project. - Once a release exists, the app checks for updates itself. -

-
+
+

Get it

+

+ Notarized, signed builds are on the + releases + page. The app checks for updates itself once installed. +

-
    -
  1. - Clone and build. - git clone https://github.com/trsdn/OpenPromptr.git -&& cd OpenPromptr && ./build-app.sh - creates dist/OpenPromptr.app. Full steps - are in the - README. +
      +
    1. +
      + Download and open the disk image. +

      + OpenPromptr-v1.2.0-macOS-arm64.dmg + — verify against its + published checksum + if you'd like — then drag OpenPromptr into Applications. +

      +
    2. -
    3. - Grant Screen Recording permission. - Under System Settings → Privacy & Security → Screen Recording, - for the built app bundle. +
    4. +
      + Grant Screen Recording permission. +

      System Settings → Privacy & Security → Screen Recording, for OpenPromptr.

      +
    5. -
    6. - Choose a source and a target display, then start - output — see - Operation - in the README. +
    7. +
      + Choose a source and a target display, then start output. +

      See Operation in the README for the full walkthrough.

      +
    -

    - See the - changelog - for release history. +

    + Prefer to build it yourself? It's a small SwiftPM project — see + building + and testing. Release history is in the + changelog.

-
- - -
-

Privacy

-
-

OpenPromptr collects nothing and sends nothing

-

- Captured images are processed locally and shown on a display only, - with no telemetry and no storage of image content on disk. The only - outbound network access is a daily check against this repository's - GitHub Releases (can be turned off), used solely to offer app - updates. An optional, off-by-default local API (127.0.0.1 only, - token-authenticated) lets a script or a Stream Deck control output - from outside the app — never reachable from the network. Settings - are kept in the app's own - UserDefaults domain. Full detail is in - the - security policy. -

+
+ +
+

Privacy

+
+ +
+ OpenPromptr collects nothing and sends nothing. +

+ Captured images are processed locally and shown on a display + only — no telemetry, no storage of image content on disk. The + only outbound network access is a daily check against this + repository's GitHub Releases (can be turned off), used solely + to offer app updates. An optional, off-by-default local API + (127.0.0.1 only, token-authenticated) lets a script or a Stream + Deck control output from outside the app — never reachable from + the network. Full detail is in the + security policy. +

+
-

- This page behaves the same way. Every stylesheet, font, and image is +

+ This page behaves the same way: every stylesheet, font, and image is served from this repository, so nothing here loads from a third party, sets a cookie, or records your visit.

-
+
- -
-

Accessibility

-

+

+

Accessibility

+

Every control is a standard SwiftUI button, toggle, or picker, with - standard keyboard focus and an accessibility label. This has not been - tested end-to-end with VoiceOver. Full detail is in the + standard keyboard focus and an accessibility label. This has not + been tested end-to-end with VoiceOver. Full detail is in the README's accessibility note.

@@ -191,11 +464,11 @@

Accessibility

-
+
-