v0.3 Phase 4d/4e: palette revision + HC rendering contract

Round-4 feedback (2026-05-15) pulled the system away from the
color.adobe.com primaries toward muted-Material values, separated
brand accent from semantic caution, and reframed high-contrast as a
behavioural rendering contract rather than a palette override.

Two phases land together because the palette revision and the HC
contract are inherently linked — HC mode needs both palette overrides
AND behavioural-token overrides to deliver "different rendering
contract" semantics.

Phase 4d palette revision.

  Dark mode (was wine-aubergine #2A1620, now brown-charcoal #12100E):
    --color-bg            #12100E
    --color-bg-elevated   #1A1713
    --color-bg-card       #211D18
    --color-bg-input      #181510
    --color-sidebar       #0F0E0C
    --color-border        #4A4035
    --color-border-subtle #2C261F

  Brand accent (new — was unset, fell through to v0.2 amber that
  competed with semantic yellow):
    Dark:  --color-accent #C97845 (copper)
    Light: --color-accent #9D5F32 (deeper copper)

  Semantic colours (muted-Material, both modes):
    Dark:  danger #FF8A8A  caution #F2C94C  success #79D59B  info #8AB4F8
    Light: danger #B3261E  caution #7A5D00  success #1B6B3A  info #2457A6

  Light mode work surface (slight paper tone, not pure white):
    --color-bg-card #FFFDF8 (was #FFFFFF)
    --color-bg-elevated #F6F1EA

  Caution fill-only-on-cream rule retires. Ochre #7A5D00 passes AA
  as foreground text on cream under the muted palette, so the single-
  token-per-role convention applies across all four semantic colours
  in both modes.

  LumotiaNotice opacity rebalance:
    Background tint  10% -> 8%
    Outer border     neutral subtle -> role at 40%
    Icon             --color-text -> role colour
    Left bar         role at 100% (unchanged)
    Title / body     --color-text (unchanged)

Phase 4e HC rendering contract.

  Behavioural tokens published alongside palette:
    --grain-opacity            0.06 dark / 0.07 light / 0 in HC
    --shadow-strength          1 default / 0 in HC
    --border-width-control     1px default / 2px in HC
    --focus-ring-width         2px default / 3px in HC
    --panel-radius             8px (reserved for future tightening)

  --quietware-texture-opacity retained as a legacy alias pointing at
  --grain-opacity so Phase 4c slider keeps working through the rename.

  HC palette: pure-black or pure-white base, the other for borders,
  single strong-focus blue #005FCC replaces accent. Brand atmosphere
  steps aside. Selector splits via :not([data-theme="light"]) so
  HC-light and HC-dark variants diverge cleanly.

Verified.

  - npm run check: 0 errors, 0 warnings across 5706 files.
  - Plan doc updated with full before/after tables, behavioural-token
    contract table, and Phase 4c / 4d / 4e log sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 13:50:11 +01:00
parent 4dca802f0b
commit dab9c7b5a6
3 changed files with 237 additions and 106 deletions

View File

@@ -236,6 +236,77 @@ Architectural finding: the v0.2 `SettingsPage.svelte` (2 891 LOC after Phase 4a)
Rationale: phase 4a already delivers "one section at a time" under quietware. Renaming the eight tabs to match the canonical plan (Output / Vocabulary carved out as top-level) is labelling polish, not user-value. Page-skeleton sweep (Phase 5) delivers more visible improvement per LOC. Phase 4b will return after Phase 5 lands.
### Phase 4d — Palette revision per 2026-05-15 round 4 feedback. Landed 2026-05-15.
The color.adobe.com primaries (#FF0700 / #000AFF / #00FF56 / #FFCD00) earned vividness at the cost of brand identity and accent-vs-caution discipline. Round-4 feedback pulled the system back toward muted-Material values with a clear architectural split:
```
Atmosphere = warm tactile surface
Status = red / yellow / green / blue
Brand = copper-clay amber accent (--color-accent, distinct from caution)
Accessibility = high-contrast override is a different rendering contract
```
Dark mode (revised, brown-charcoal instead of wine-aubergine):
| Token | Before (4c) | After (4d) |
|---|---|---|
| `--color-bg` | `#2A1620` | `#12100E` |
| `--color-bg-elevated` | `#34222B` | `#1A1713` |
| `--color-bg-card` | `#3E2A33` | `#211D18` |
| `--color-sidebar` | `#1F0F18` | `#0F0E0C` |
| `--color-accent` | (unset, fell through to v0.2) | `#C97845` |
| `--color-danger` | `#FF2A24` | `#FF8A8A` |
| `--color-caution` | `#FFCD00` | `#F2C94C` |
| `--color-success` | `#00FF56` | `#79D59B` |
| `--color-info` | `#7076FF` | `#8AB4F8` |
Light mode (revised, semantic values back to Material AA-friendly hues):
| Token | Before (4c) | After (4d) |
|---|---|---|
| `--color-bg-card` | `#FFFFFF` | `#FFFDF8` (warm white) |
| `--color-bg-elevated` | `#F3EEE6` | `#F6F1EA` |
| `--color-accent` | (unset) | `#9D5F32` |
| `--color-danger` | `#C54B1F` | `#B3261E` |
| `--color-caution` | `#FFCD00` (fill-only) | `#7A5D00` (passes as text) |
| `--color-success` | `#00852D` | `#1B6B3A` |
| `--color-info` | `#0047AB` | `#2457A6` |
The "caution fill-only on cream" rule retires under the muted palette. Ochre `#7A5D00` passes AA as foreground text against cream, so the same single-token-per-role convention applies across all four semantic colours in light mode.
LumotiaNotice opacity rebalance:
| Surface | Before | After |
|---|---|---|
| Background tint | role at 10% | role at 8% |
| Outer border | neutral subtle | role at 40% |
| Left bar | role full | role full (unchanged) |
| Icon | `--color-text` | role colour |
| Title / body | `--color-text` | `--color-text` (unchanged) |
### Phase 4e — High-contrast rendering contract. Landed 2026-05-15.
High-contrast is not a colour variant — it's a different rendering contract that the entire UI subscribes to. The tokens block now publishes behavioural variables alongside the palette overrides, so components can opt into the contract by reading them.
Behavioural tokens (defaults + HC values):
| Token | Default | HC |
|---|---|---|
| `--grain-opacity` | 0.06 (dark) / 0.07 (light) | 0 |
| `--shadow-strength` | 1 | 0 |
| `--border-width-control` | 1px | 2px |
| `--focus-ring-width` | 2px | 3px |
| `--panel-radius` | 8px | 8px (unchanged for now) |
`--quietware-texture-opacity` retained as a legacy alias pointing at `--grain-opacity` so Phase 4c controls keep working through the rename.
HC palette tweaks (round-4 feedback):
- **HC light** is now true pure-white background (`#FFFFFF`) + true pure-black text/borders (`#000000`). Brand atmosphere steps aside completely. Single strong-focus blue `#005FCC` replaces the accent.
- **HC dark** stays pure black `#000000` with white borders, semantic muted hues that read clearly. Matches the v0.3 dark feel but at maximum legibility.
- Hue-based selectors: `[data-design="quietware"][data-contrast="high"]` matches both base themes; the dark-base variant is scoped via `:not([data-theme="light"])` so light-base HC and dark-base HC can diverge.
### Phase 4c — Texture-opacity slider + high-contrast toggle. Landed 2026-05-15.
- New static grain overlay defined in `src/design-system/v0.3-quietware-tokens.css`. Fixed-position `body::after`, `mix-blend-mode: overlay`, opacity driven by `--quietware-texture-opacity`. Pattern generated inline via SVG `feTurbulence` (`baseFrequency=0.85`, `numOctaves=2`, `stitchTiles=stitch`). Tile size 240×240, repeated across viewport. `pointer-events: none` so the overlay never intercepts clicks. Active only under `html[data-design="quietware"]` and only on the dark and light modes — high-contrast forces opacity to 0.

View File

@@ -10,29 +10,26 @@
Activation paths:
1. Manual: add data-design="quietware" to <html> via dev tools.
2. Build flag (Phase 2): VITE_LUMOTIA_QUIETWARE=1 will wire the
2. Build flag (Phase 2): VITE_LUMOTIA_QUIETWARE=1 wires the
attribute through +layout.svelte automatically.
--------------------------------------------------------------
Palette source — Jake's color.adobe.com primaries, locked 2026-05-15:
Palette — Phase 4d revision (2026-05-15 round 4 feedback).
#FF0700 red (--danger)
#000AFF blue (--info)
#00FF56 green (--success)
#FFCD00 yellow (--caution)
Atmosphere is warm brown-charcoal. Semantic colours are
muted-Material hues, NOT max-saturation primaries. The brand
accent (copper-clay) is intentionally distinct from caution
(yellow) so the two warm attention colours do not collide.
Each primary is used literally where it clears WCAG AA on the
relevant background. Where it does not (typically yellow and green
on cream paper, blue on dark Coffee Bean) a text-safe sibling at
the same hue and saturation is derived by adjusting lightness
until 4.5:1 clears.
Light-mode caution returns to ochre #7A5D00; this passes AA as
text on cream and works with the left-bar Notice pattern. The
earlier "fill-only on cream" rule applied to the bright #FFCD00
primary; under muted-Material values that constraint dissolves.
Caution stays a fill-only role on cream paper. Any darkened yellow
reads as olive or mustard ("looks like poop", per Jake), which we
refuse to ship. Pill backgrounds, dots, left-border accents and
icons take --color-caution. Foreground text inside caution-coloured
pills stays --color-text. This matches Material, GOV.UK and IBM
Carbon practice.
High contrast is a rendering CONTRACT, not just a palette. Phase
4e behavioural tokens (--grain-opacity, --shadow-strength,
--border-width-control, --focus-ring-width, --panel-radius)
accompany the palette override.
Typography stack (V4 pairing, locked 2026-05-15):
Work Sans -> body, UI, controls, headers
@@ -70,55 +67,69 @@
/* ============================================================
Quietware — dark mode default.
Lifted Coffee Bean base. Jake's primaries used directly where
they clear AA on the dark surface; lifted siblings for the rest.
Warm brown-charcoal atmosphere. Reads as warm when you notice
it, not coloured at first glance. Semantic and brand-accent
palettes are deliberately separate.
============================================================ */
:root[data-design="quietware"] {
--color-bg: #2A1620;
--color-bg-elevated: #34222B;
--color-bg-card: #3E2A33;
--color-bg-input: #281620;
--color-border: #6B4D58;
--color-border-subtle: #4E3742;
--color-bg: #12100E;
--color-bg-elevated: #1A1713;
--color-bg-card: #211D18;
--color-bg-input: #181510;
--color-border: #4A4035;
--color-border-subtle: #2C261F;
--color-text: #F3EFE7;
--color-text-secondary: #BDB6A9;
--color-text-tertiary: #8D8577;
--color-text-tertiary: #908779;
/* --info and --danger lifted from primaries; pure red/blue too dark on dark bg. */
--color-info: #7076FF; /* lifted from #000AFF, 4.64 AA */
--color-danger: #FF2A24; /* lifted from #FF0700, 4.54 AA */
/* --success and --caution use Jake's primaries directly; both already AAA on dark bg. */
--color-success: #00FF56; /* Jake literal, 12.51 AAA */
--color-caution: #FFCD00; /* Jake literal, 11.33 AAA */
/* Brand accent. Copper-clay, distinct from semantic yellow. */
--color-accent: #C97845;
--color-accent-hover: #B96638;
--color-accent-subtle: #C9784514;
--color-accent-glow: #C9784525;
/* Semantic palette. Muted-Material hues. */
--color-danger: #FF8A8A;
--color-caution: #F2C94C;
--color-warning: var(--color-caution);
--color-success: #79D59B;
--color-info: #8AB4F8;
--color-sidebar: #1F0F18;
--color-nav-active: #3E2A33;
--color-hover: #34222B;
--color-overlay-dim: rgba(31, 8, 18, 0.7);
--color-sidebar: #0F0E0C;
--color-nav-active: #211D18;
--color-hover: #1A1713;
--color-overlay-dim: rgba(15, 14, 12, 0.7);
--font-family-body: 'Work Sans', system-ui, sans-serif;
--font-family-display: 'Young Serif', serif;
--font-family-mono: 'JetBrains Mono', monospace;
/* Texture intensity. 0.06 baseline, slider range 0.05 to 0.08 in advanced. */
--quietware-texture-opacity: 0.06;
/* Phase 4e behavioural tokens — components opt into the rendering
contract by reading these. Defaults preserve existing v0.3 feel. */
--grain-opacity: 0.06;
--shadow-strength: 1;
--border-width-control: 1px;
--focus-ring-width: 2px;
--panel-radius: 8px;
/* Legacy alias — earlier Phase 4c name. Kept for transition. */
--quietware-texture-opacity: var(--grain-opacity);
--quietware-texture-color: rgba(243, 239, 231, 1);
}
/* ============================================================
Quietware — light mode.
Cream paper base. Jake's blue passes AAA on cream as text and is
used literally. The other three roles use text-safe siblings
derived at the same hue and saturation (lightness shifted for AA).
Caution is fill-only on cream by convention.
Warm paper base. Slight tone on the main work surface so the
page does not read as a generic web app. Caution returns to
ochre #7A5D00 — passes AA as text on cream under the muted
palette.
============================================================ */
:root[data-design="quietware"][data-theme="light"] {
--color-bg: #FBF8F2;
--color-bg-elevated: #F3EEE6;
--color-bg-card: #FFFFFF;
--color-bg-input: #F3EEE6;
--color-bg-elevated: #F6F1EA;
--color-bg-card: #FFFDF8;
--color-bg-input: #F4EFE7;
--color-border: #897D70;
--color-border-subtle: #D8D0C4;
@@ -126,70 +137,106 @@
--color-text-secondary: #5E574C;
--color-text-tertiary: #74685B;
--color-info: #000AFF; /* Jake literal, 7.96 AAA on cream */
--color-danger: #E60600; /* text-safe derived from #FF0700, 4.51 AA */
--color-success: #00852D; /* text-safe derived from #00FF56, 4.51 AA */
/* Caution is FILL ONLY on cream. Text inside caution pills uses --color-text. */
--color-caution: #FFCD00; /* Jake literal, fill-only by convention */
--color-accent: #9D5F32;
--color-accent-hover: #854E29;
--color-accent-subtle: #9D5F3214;
--color-accent-glow: #9D5F3225;
--color-danger: #B3261E;
--color-caution: #7A5D00;
--color-warning: var(--color-caution);
--color-success: #1B6B3A;
--color-info: #2457A6;
--color-sidebar: #F3EEE6;
--color-nav-active: #E8E2D6;
--color-hover: #EAE4D8;
--color-overlay-dim: rgba(29, 27, 24, 0.55);
--quietware-texture-opacity: 0.07;
--grain-opacity: 0.07;
--quietware-texture-opacity: var(--grain-opacity);
--quietware-texture-color: rgba(29, 27, 24, 1);
}
/* ============================================================
Quietware — high-contrast mode.
Same four hues retained so role recognition does not flip between
modes. Saturation 80%, lightness 70%. Zero texture, no translucent
overlays.
Quietware — high-contrast rendering contract.
Different rendering CONTRACT, not just a colour override. The
tactile brand steps aside entirely. No grain, no shadows,
thicker borders, sharper focus rings. Pure black/white with a
single strong-blue focus colour.
============================================================ */
:root[data-design="quietware"][data-contrast="high"] {
--color-bg: #000000;
--color-bg-elevated: #0A0A0A;
--color-bg-card: #111111;
--color-bg-input: #050505;
--color-border: #FFFFFF;
--color-border-subtle: #888888;
:root[data-design="quietware"][data-contrast="high"],
:root[data-design="quietware"][data-theme="light"][data-contrast="high"] {
--color-bg: #FFFFFF;
--color-bg-elevated: #FFFFFF;
--color-bg-card: #FFFFFF;
--color-bg-input: #FFFFFF;
--color-border: #000000;
--color-border-subtle: #000000;
--color-text: #FFFFFF;
--color-text-secondary: #DADADA;
--color-text-tertiary: #C8C8C8;
--color-text: #000000;
--color-text-secondary: #000000;
--color-text-tertiary: #000000;
--color-info: #757AF0; /* 5.80 AA on black */
--color-danger: #F07975; /* 7.70 AAA on black */
--color-success: #75F09F; /* 14.72 AAA on black */
--color-caution: #F0D875; /* 14.78 AAA on black */
/* Accent collapses to the strong focus blue in HC. Brand
identity steps aside. */
--color-accent: #005FCC;
--color-accent-hover: #004BA0;
--color-accent-subtle: transparent;
--color-accent-glow: transparent;
--color-danger: #B3261E;
--color-caution: #7A5D00;
--color-warning: var(--color-caution);
--color-success: #1B6B3A;
--color-info: #005FCC;
--color-sidebar: #000000;
--color-nav-active: #1A1A1A;
--color-hover: #1A1A1A;
--color-sidebar: #FFFFFF;
--color-nav-active: #000000;
--color-hover: #F0F0F0;
--color-overlay-dim: rgba(0, 0, 0, 0.85);
/* No texture in high contrast. Clarity over atmosphere. */
/* Behavioural overrides. The contract bites here. */
--grain-opacity: 0;
--shadow-strength: 0;
--border-width-control: 2px;
--focus-ring-width: 3px;
--panel-radius: 8px;
--quietware-texture-opacity: 0;
--quietware-texture-color: rgba(0, 0, 0, 0);
}
/* Dark-base high-contrast — black background instead of white.
Used when the user explicitly wants HC inside the dark theme. */
:root[data-design="quietware"][data-contrast="high"]:not([data-theme="light"]) {
--color-bg: #000000;
--color-bg-elevated: #000000;
--color-bg-card: #000000;
--color-bg-input: #000000;
--color-border: #FFFFFF;
--color-border-subtle: #FFFFFF;
--color-text: #FFFFFF;
--color-text-secondary: #FFFFFF;
--color-text-tertiary: #FFFFFF;
--color-accent: #75A8F0;
--color-accent-hover: #5C92E0;
--color-danger: #FF8A8A;
--color-caution: #F2C94C;
--color-success: #79D59B;
--color-info: #8AB4F8;
--color-sidebar: #000000;
--color-nav-active: #FFFFFF;
--color-hover: #1A1A1A;
}
/* ============================================================
Static grain overlay. Atmospheric texture across the whole viewport.
Fixed position so it does not scroll. Tile pattern generated inline
via SVG feTurbulence. Opacity driven by --quietware-texture-opacity
(default 0.06 dark / 0.07 light, slider range 0.05 to 0.08 in
advanced settings). mix-blend-mode overlay darkens shadows and
brightens highlights, producing the soft notebook-paper effect.
pointer-events: none so clicks pass through. z-index 9999 places it
above all content, but content remains fully interactive thanks to
the pointer-events override.
No texture in high-contrast mode — the [data-contrast="high"] block
above sets --quietware-texture-opacity to 0.
Static grain overlay. Reads --grain-opacity. Zero in HC mode by
contract. Fixed-position so it does not scroll. mix-blend-mode
overlay produces the soft notebook-paper effect.
============================================================ */
:root[data-design="quietware"] body::after {
content: "";
@@ -198,7 +245,7 @@
pointer-events: none;
z-index: 9999;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
opacity: var(--quietware-texture-opacity);
opacity: var(--grain-opacity);
mix-blend-mode: overlay;
}

View File

@@ -4,19 +4,24 @@
// scattered across pages. Four tones map to the semantic tokens.
//
// v0.3 Phase 3 update — left-bar accent pattern. The visible status
// signal now lives on a 4-px solid left border in the tone colour
// (100% opacity, fill-grade chroma); the surrounding container uses
// a soft tone-tinted background and a subtle outer border. Icon,
// title and body text all use --color-text so they remain legible
// across every theme combination, including quietware light mode
// where --color-caution is the bright #FFCD00 fill (would fail as
// text on cream). Same accessibility role semantics retained.
// signal lives on a 4-px solid left border in the tone colour, full
// opacity. The surround uses a soft tone-tinted background + outer
// border in the tone colour. Title and body text use --color-text;
// icon uses the tone colour. Same accessibility role semantics
// retained.
//
// v0.3 Phase 4d opacity rebalance (2026-05-15 round 4 feedback):
// Background role colour at 8% (was 10%, less heavy)
// Outer border role colour at 40% (was neutral subtle)
// Left bar role colour at 100% (unchanged)
// Icon role colour (reverted from --color-text once the
// muted-Material palette landed; previous fill-only-
// on-cream concern dissolved with the new ochre
// #7A5D00 light-mode caution).
// Title + body --color-text (unchanged)
//
// Pattern matches Material Design "banner with leading accent" and
// IBM Carbon "inline notification" guidance: status colour on a
// large fill surface (the bar), neutral foreground text. Works for
// any tone in any theme regardless of WCAG-text contrast on the
// role colour itself.
// IBM Carbon "inline notification" guidance.
import { Info, AlertTriangle, AlertOctagon, CheckCircle, X } from "lucide-svelte";
import type { Snippet } from "svelte";
@@ -40,25 +45,33 @@
classes = "",
}: Props = $props();
const palette: Record<Tone, { bar: string; bg: string; icon: typeof Info }> = {
const palette: Record<Tone, { bar: string; bg: string; border: string; iconColor: string; icon: typeof Info }> = {
info: {
bar: "border-l-info",
bg: "bg-info/10",
bg: "bg-info/8",
border: "border-info/40",
iconColor: "text-info",
icon: Info,
},
caution: {
bar: "border-l-caution",
bg: "bg-caution/10",
bg: "bg-caution/8",
border: "border-caution/40",
iconColor: "text-caution",
icon: AlertTriangle,
},
danger: {
bar: "border-l-danger",
bg: "bg-danger/10",
bg: "bg-danger/8",
border: "border-danger/40",
iconColor: "text-danger",
icon: AlertOctagon,
},
success: {
bar: "border-l-success",
bg: "bg-success/10",
bg: "bg-success/8",
border: "border-success/40",
iconColor: "text-success",
icon: CheckCircle,
},
};
@@ -73,10 +86,10 @@
<div
role={ariaRole}
class="px-4 py-3 rounded-r-lg rounded-l-sm border border-l-4
border-border-subtle text-[13px] flex items-start gap-2.5
{tonePal.bar} {tonePal.bg} {classes}"
text-[13px] flex items-start gap-2.5
{tonePal.bar} {tonePal.border} {tonePal.bg} {classes}"
>
<ToneIcon size={16} class="mt-0.5 shrink-0 text-text" aria-hidden="true" />
<ToneIcon size={16} class="mt-0.5 shrink-0 {tonePal.iconColor}" aria-hidden="true" />
<div class="flex-1 min-w-0 leading-relaxed text-text">
{#if title}
<p class="font-semibold text-text mb-1">{title}</p>