diff --git a/docs/release/v0.3-tactile-quietware.md b/docs/release/v0.3-tactile-quietware.md
index b559fe0..b725b77 100644
--- a/docs/release/v0.3-tactile-quietware.md
+++ b/docs/release/v0.3-tactile-quietware.md
@@ -351,7 +351,41 @@ LumotiaNotice now reads these via Tailwind arbitrary-value classes (`bg-[var(--n
Per Jake's round-5 spec: yellow stays yellow (visible signal), ochre only appears as small-text ink, and brown/khaki never represents the caution role's primary identity again.
-### Phase 4h — Counterlines test (TBD: keep or remove). Landed 2026-05-15.
+### Phase 4i — Counterlines as interaction-affordance identity. Landed 2026-05-15.
+
+Round-7 redirect (Option D): move counterlines from notices (where they're invisible) to interactive controls (where they add identity). Three-level model:
+
+| Level | Surface | Width | Opacity |
+|---|---|---|---|
+| 0 — none | panels, notices (default), content, disabled | — | — |
+| 1 — quiet | controls, chips, active nav | 1 px | 48% dark / 34% light |
+| 2 — focus | keyboard focus, recording state | 2 px | 85% |
+
+Notice retires from default counterline rendering. Opt-in via `` when a specific notice deliberately mimics control identity.
+
+LumotiaButton primary + destructive variants gain a Level-1 inset counterline:
+
+- primary: `--button-brand-counterline` (softened blue, complement of brand amber)
+- destructive: `--button-danger-counterline` (softened blue, complement of red)
+- secondary / tertiary: no counterline (clean neutral)
+- All variants on `:focus-visible`: 2-px ring at `--focus-ring-color` (default `--color-accent`) at 85% opacity. HC overrides to a strong blue `#005FCC`.
+
+Token deltas vs Phase 4h:
+
+```css
+--counterline-width: 1px /* unchanged */
+--counterline-width-focus: 2px /* new — Level-2 width */
+--counterline-opacity-dark: 0.48 /* was 0.32 */
+--counterline-opacity-light: 0.34 /* was 0.22 */
+--counterline-opacity-focus: 0.85 /* new */
+--role-brand-counterline: #3A6BFF /* new — blue counter for orange brand */
+--button-{role}-counterline /* new — per-role component tokens */
+--focus-ring-color /* new — context-overridable focus tint */
+```
+
+HC contract: every counterline token forced transparent, `--counterline-width: 0`, focus width kept at 2 px (HC focus ring carries the tactile detail in that mode), `--focus-ring-color: #005FCC`.
+
+### Phase 4h — Counterlines test (RETIRED 2026-05-15, superseded by Phase 4i). Landed 2026-05-15.
Test commit per Jake's round-6 spec. Subtle 1-px inner-shadow detail in a softened complementary hue, applied to LumotiaNotice only. Other test surfaces (Button.destructive, status pills, record ring, active nav) reserved for after evaluation.
diff --git a/src/design-system/v0.3-quietware-tokens.css b/src/design-system/v0.3-quietware-tokens.css
index df9d68b..c97e305 100644
--- a/src/design-system/v0.3-quietware-tokens.css
+++ b/src/design-system/v0.3-quietware-tokens.css
@@ -219,29 +219,53 @@
--quietware-texture-color: rgba(243, 239, 231, 1);
/* ----------------------------------------------------------
- Phase 4h — counterlines (test).
- Tactile-only 1-px inner-shadow detail using a softened
- complementary hue. NOT a second semantic signal — meaning is
- still carried by signal + icon + label. HC contract zeroes
- the width.
+ Phase 4i — counterlines as interactive-affordance identity
+ (per round-7 redirect). Three-level model:
+
+ Level 0 no counterline (panels, notices, content, disabled)
+ Level 1 quiet (controls, chips, active nav)
+ Level 2 strong / focus (keyboard focus, recording state)
+
+ Notices retire from default counterline rendering (they have
+ enough semantic language already: bar + icon + border + bg +
+ label). Notice can still opt in via the `counterline` prop.
+
+ HC contract zeroes the normal width but keeps the focus width
+ because the HC focus ring IS the tactile detail in that mode.
---------------------------------------------------------- */
--counterline-width: 1px;
+ --counterline-width-focus: 2px;
+ --counterline-opacity-dark: 0.48;
+ --counterline-opacity-light: 0.34;
+ --counterline-opacity-focus: 0.85;
- /* Raw complementary hues per Jake's spec. */
+ /* Raw complementary hues. Brand orange gets its own counterline
+ (soft blue) so primary buttons land in the same identity as the
+ other roles. */
--role-info-counterline: var(--color-accent);
--role-danger-counterline: #6EA8FF;
--role-success-counterline: #B891FF;
--role-caution-counterline: #3A6BFF;
+ --role-brand-counterline: #3A6BFF;
- /* Tinted per theme. Dark mode uses 32% opacity (~slightly stronger
- so it reads against the dark surface). Per-usage aliases so
- components subscribe semantically. */
- --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) 32%, transparent);
- --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) 32%, transparent);
- --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) 32%, transparent);
- --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) 32%, transparent);
+ /* Component usage tokens. Buttons + chips + nav subscribe to these.
+ Default opacity is dark; light-mode override below replaces. */
+ --button-brand-counterline: color-mix(in srgb, var(--role-brand-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --button-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --button-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --button-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --button-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
- --button-danger-counterline: var(--notice-danger-counterline);
+ /* Notice-level counterline tokens kept available for opt-in only.
+ LumotiaNotice no longer applies them by default per round-7. */
+ --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+ --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent);
+
+ /* Focus ring colour — defaults to brand accent. Components can
+ override per-context (eg. info focus = info-counterline). */
+ --focus-ring-color: var(--color-accent);
}
/* ============================================================
@@ -325,14 +349,18 @@
--quietware-texture-opacity: var(--grain-opacity);
--quietware-texture-color: rgba(29, 27, 24, 1);
- /* Phase 4h light-mode counterlines. Lower opacity (22%) so the
- detail stays whisper-quiet against the cream surface. */
- --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) 22%, transparent);
- --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) 22%, transparent);
- --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) 22%, transparent);
- --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) 22%, transparent);
+ /* Phase 4i light-mode counterlines. Lower opacity per Jake's spec
+ so the detail stays restrained against the cream surface. */
+ --button-brand-counterline: color-mix(in srgb, var(--role-brand-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --button-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --button-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --button-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --button-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
- --button-danger-counterline: var(--notice-danger-counterline);
+ --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
+ --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-light) * 100%), transparent);
}
/* ============================================================
@@ -418,14 +446,28 @@
--quietware-texture-opacity: 0;
--quietware-texture-color: rgba(0, 0, 0, 0);
- /* HC contract: counterlines disabled (the 2-px HC border IS the
- tactile detail in this mode). */
+ /* HC contract: counterlines disabled for normal states. The HC
+ focus ring (2 px, full strong-blue) IS the tactile detail in
+ this mode. */
--counterline-width: 0;
+ --counterline-width-focus: 2px;
+ --counterline-opacity-dark: 0;
+ --counterline-opacity-light: 0;
+ --counterline-opacity-focus: 1;
+
--notice-info-counterline: transparent;
--notice-danger-counterline: transparent;
--notice-success-counterline: transparent;
--notice-caution-counterline: transparent;
+
+ --button-brand-counterline: transparent;
--button-danger-counterline: transparent;
+ --button-info-counterline: transparent;
+ --button-success-counterline: transparent;
+ --button-caution-counterline: transparent;
+
+ /* HC focus ring is the brand-strong blue, not a complement. */
+ --focus-ring-color: #005FCC;
}
/* Dark-base high-contrast — black background, white borders. Used
diff --git a/src/lib/ui/LumotiaButton.svelte b/src/lib/ui/LumotiaButton.svelte
index b393359..0250b79 100644
--- a/src/lib/ui/LumotiaButton.svelte
+++ b/src/lib/ui/LumotiaButton.svelte
@@ -46,15 +46,26 @@
// Variant palette uses semantic tokens; primary fills with --color-accent
// (amber/copper) and pairs with .btn-filled-text for AA. Destructive
// mirrors primary on --color-danger.
+ //
+ // v0.3 Phase 4i — interactive counterlines. primary + destructive
+ // gain a Level-1 inset counterline (softened complement) under
+ // quietware. Secondary + tertiary stay clean. HC contract zeroes
+ // --counterline-width so the effect vanishes in HC.
+ //
+ // Focus state uses Level-2 width (2 px) + --focus-ring-color via a
+ // focus-visible ring. Tailwind's ring utility writes to a separate
+ // box-shadow slot so it composes cleanly with the inset counterline.
const variantClass: Record = {
primary:
- "bg-accent btn-filled-text font-medium hover:bg-accent-hover shadow-[var(--shadow-accent-pill)]",
+ "bg-accent btn-filled-text font-medium hover:bg-accent-hover " +
+ "shadow-[var(--shadow-accent-pill),inset_0_0_0_var(--counterline-width,0)_var(--button-brand-counterline,transparent)]",
secondary:
"bg-bg-elevated text-text border border-border-subtle hover:bg-hover",
tertiary:
"bg-transparent text-text-secondary hover:bg-hover hover:text-text",
destructive:
- "bg-danger btn-filled-text font-medium hover:bg-danger/90",
+ "bg-danger btn-filled-text font-medium hover:bg-danger/90 " +
+ "shadow-[inset_0_0_0_var(--counterline-width,0)_var(--button-danger-counterline,transparent)]",
};
const busy = $derived(loading || disabled);
@@ -63,6 +74,10 @@