v0.3 Phase 4i: counterlines as interaction-affordance identity

Round-7 redirect (Option D): move counterlines from notices (where
they're invisible) to interactive controls (where they add identity).

Three-level model:

  Level 0  no counterline   panels, notices (default), content, disabled
  Level 1  quiet            controls, chips, active nav
                            1 px, 48% opacity dark / 34% light
  Level 2  focus / strong   keyboard focus, recording state
                            2 px, 85% opacity

Notices retire from default counterline rendering. Notice already
carries enough semantic language: bar + icon + border + bg + label.
A complementary inset on top was adding nothing. Notice can opt in
via <LumotiaNotice counterline ... /> when it deliberately mimics
control identity.

Token deltas vs Phase 4h.

  --counterline-width: 1px               unchanged
  --counterline-width-focus: 2px         new (Level-2 width)
  --counterline-opacity-dark: 0.48       was 0.32 (raised so Level-1
                                          actually reads on controls)
  --counterline-opacity-light: 0.34      was 0.22
  --counterline-opacity-focus: 0.85      new
  --role-brand-counterline: #3A6BFF      new (blue counter for the
                                          orange brand accent so
                                          primary buttons get an
                                          identity edge like the
                                          other roles)
  --button-{role}-counterline            new (per-role component tokens
                                          subscribing to role-counterline
                                          via color-mix)
  --focus-ring-color                     new (defaults to brand accent;
                                          HC overrides to #005FCC)

LumotiaButton applies counterlines to primary + destructive variants
only. Secondary + tertiary stay clean. All variants get a Level-2
focus-visible ring at --focus-ring-color * opacity-focus, with the
existing ring-offset-2 ring-offset-bg pattern.

LumotiaNotice gains a `counterline` boolean prop, default false. When
set, the previous Phase 4h inset shadow re-applies.

HC contract: counterline-width 0 + all -counterline tokens transparent.
Focus width stays 2 px because the HC focus ring IS the tactile
detail in that mode. focus-ring-color forces to #005FCC.

Honest evaluation of the visible result.

  Notices: visibly cleaner without the default inset shadow. Reads
  with more confidence. Good change.

  Buttons: counterline applied at 1 px / 48% dark / 34% light is
  perceptible at standard viewing but screenshot-zoom subtle. Focus
  state implemented but not visible in static screenshots; requires
  manual tab-test in the dev server.

  Recommend keeping the architecture even if the visual is restrained,
  because the focus-state work is genuinely valuable accessibility
  scaffolding regardless of how the Level-1 reads.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

Phase 5c (Dictation layout migration) still queued for a focused
fresh sitting. This commit does not block it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:07:26 +01:00
parent 74bcc293bb
commit 001d8fe83d
4 changed files with 124 additions and 27 deletions

View File

@@ -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