Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Involute</title>
<script>
// No-flash theme boot: set data-theme before first paint.
// Atelier light is the default (Lumen "warm light" identity).
(function () {
try {
var stored = localStorage.getItem('involute.theme');
document.documentElement.setAttribute(
'data-theme',
stored === 'dark' || stored === 'light' ? stored : 'light',
);
} catch (error) {
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ function getStoredTheme(): ThemeMode {
return storedTheme;
}
} catch {
// Ignore localStorage failures and fall back to dark.
// Ignore localStorage failures and fall back to the Atelier light default.
}

return 'dark';
return 'light';
}

function persistTheme(nextTheme: ThemeMode) {
Expand Down
209 changes: 116 additions & 93 deletions packages/web/src/styles/app.css
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap');
/* ============================================================
Involute UI — aligned to the Lumen Design System.
Warm "Atelier" light (default) + "Vault" dark, IBM Plex, and the
quiet-utility ethos: flat backgrounds, 1px hairline borders doing
the structural work, shadows only on the outer shell + popovers,
no gradients / blur / emoji, hover = tint not movement.
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Sans+SC:wght@400;500;600;700&display=swap');

:root {
--font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;

--r-1: 3px;
--r-2: 4px;
--r-3: 6px;
--r-4: 8px;
--r-5: 8px;
--r-6: 8px;
--font-sans: "IBM Plex Sans", "IBM Plex Sans SC", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--font-serif: "Iowan Old Style", "Songti SC", "Source Han Serif SC", Georgia, "Times New Roman", serif;

/* radius scale — Lumen: controls 10, panels 12, cards 16, shell 20, pill 999.
Micro elements (code, keycaps) stay tighter. */
--r-1: 6px;
--r-2: 8px;
--r-3: 10px;
--r-4: 12px;
--r-5: 16px;
--r-6: 20px;

--sidebar-w: 224px;
--content-gutter: 16px;
--pad-x: 16px;
--pad-y: 8px;
--row-h: 32px;

--ease: cubic-bezier(0.2, 0, 0, 1);
--dur-1: 80ms;
--dur-2: 120ms;
/* motion — restrained; 120–180ms, color/opacity/width only */
--ease: cubic-bezier(0.4, 0, 0.2, 1);
--dur-1: 120ms;
--dur-2: 150ms;
--dur-3: 180ms;
}

Expand All @@ -44,89 +55,96 @@
--row-h: 36px;
}

/* ===== LIGHT (default) — "Atelier": warm parchment + terracotta ===== */
:root,
[data-theme="light"] {
color-scheme: light;

--bg: #f7f6f2;
--bg-raised: #fffdfa;
--bg-hover: #f2ede4;
--bg-active: #eae3d7;
--bg-sunken: #efece5;
--bg-overlay: rgba(31, 26, 23, 0.24);

--border: #e7e1d8;
--border-strong: #cdc4b6;
--border-subtle: #efece5;

--fg: #1f1a17;
--fg-muted: #443a32;
--fg-dim: #71675d;
--fg-faint: #b0a89d;

--accent: #9f4f24;
--accent-fg: #fffdfa;
--accent-weak: rgba(159, 79, 36, 0.1);
--accent-border: rgba(159, 79, 36, 0.24);

--danger: #b04545;
--danger-weak: rgba(176, 69, 69, 0.1);
--success: #2f7d52;
--success-weak: rgba(47, 125, 82, 0.1);
--warn: #a85e10;
--info: #2563bb;

/* elevation — only shell + pop; inline cards carry none */
--shadow-sm: none;
--shadow-md: 0 12px 36px rgba(31, 26, 23, 0.06);
--shadow-lg: 0 16px 48px rgba(31, 26, 23, 0.12);

--selection: rgba(159, 79, 36, 0.14);

--prio-urgent: #b04545;
--prio-high: #a85e10;
--prio-med: #71675d;
--prio-low: #a99f93;
--prio-none: #cdc4b6;
}

/* ===== DARK — "Vault": near-black + deep blue + cyan ===== */
[data-theme="dark"] {
--bg: #1a1a2e;
--bg-raised: #1e1e34;
--bg-hover: #25253d;
--bg-active: #2c2c46;
--bg-sunken: #151528;
--bg-overlay: rgba(0, 0, 0, 0.55);

--border: #334155;
--border-strong: #3e4c63;
--border-subtle: #252540;

--fg: #e2e8f0;
--fg-muted: #94a3b8;
--fg-dim: #64748b;
--fg-faint: #475569;

--accent: #d4c5a0;
--accent-fg: #0f0f1a;
--accent-weak: rgba(212, 197, 160, 0.1);
--accent-border: rgba(212, 197, 160, 0.2);

--danger: #e5484d;
--danger-weak: rgba(229, 72, 77, 0.12);
--success: #65b881;
--success-weak: rgba(101, 184, 129, 0.12);
--warn: #f5a524;
--info: #7c88ff;

--shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3), 0 0 0 1px var(--border);
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--border);

--selection: rgba(212, 197, 160, 0.2);

--prio-urgent: #ef4444;
--prio-high: #f97316;
--prio-med: #eab308;
--prio-low: #3b82f6;
--prio-none: #475569;
}
color-scheme: dark;

[data-theme="light"] {
--bg: #fbfbfa;
--bg-raised: #ffffff;
--bg-hover: #f3f3f1;
--bg-active: #ebeae7;
--bg-sunken: #f6f5f3;
--bg-overlay: rgba(18, 18, 18, 0.24);

--border: #e5e3df;
--border-strong: #d4d2cd;
--border-subtle: #efeeea;

--fg: #1a1a1a;
--fg-muted: #5a5a5a;
--fg-dim: #8a8a8a;
--fg-faint: #bcbcbc;

--accent: #8b7a52;
--bg: #0a0e16;
--bg-raised: #11161f;
--bg-hover: #1c2432;
--bg-active: #161c27;
--bg-sunken: #0c1119;
--bg-overlay: rgba(0, 0, 0, 0.58);

--border: #1f2937;
--border-strong: #2c3a4f;
--border-subtle: #161c27;

--fg: #e6edf5;
--fg-muted: #c2cfde;
--fg-dim: #7d8aa0;
--fg-faint: #55606f;

--accent: #3b82f6;
--accent-fg: #ffffff;
--accent-weak: rgba(139, 122, 82, 0.08);
--accent-border: rgba(139, 122, 82, 0.25);
--accent-weak: rgba(59, 130, 246, 0.16);
--accent-border: rgba(59, 130, 246, 0.3);

--danger: #d42d35;
--danger-weak: rgba(212, 45, 53, 0.1);
--success: #3f8d5a;
--success-weak: rgba(63, 141, 90, 0.12);
--warn: #c47609;
--info: #4f5bd5;
--danger: #f472b6;
--danger-weak: rgba(244, 114, 182, 0.14);
--success: #22c55e;
--success-weak: rgba(34, 197, 94, 0.15);
--warn: #fbbf24;
--info: #06b6d4;

--shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06), 0 0 0 1px var(--border);
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.08), 0 0 0 1px var(--border);
--shadow-sm: none;
--shadow-md: 0 12px 36px rgba(0, 0, 0, 0.55);
--shadow-lg: 0 18px 52px rgba(0, 0, 0, 0.7);

--selection: rgba(139, 122, 82, 0.18);
--selection: rgba(59, 130, 246, 0.22);

--prio-urgent: #d42d35;
--prio-high: #c47609;
--prio-med: #5a5a5a;
--prio-low: #8a8a8a;
--prio-none: #bcbcbc;
--prio-urgent: #f472b6;
--prio-high: #fbbf24;
--prio-med: #7d8aa0;
--prio-low: #55606f;
--prio-none: #2c3a4f;
}

* {
Expand All @@ -151,13 +169,20 @@ body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-feature-settings: "ss01", "cv11";
/* Flat warm background — no gradients, no imagery, no grid texture. */
}

a {
color: inherit;
text-decoration: none;
}

/* Focus — 3px accent outline at 2px offset (Lumen :focus-visible). */
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--accent) 32%, transparent);
outline-offset: 2px;
}
Comment on lines +180 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
def lin(c):
    c/=255
    return c/12.92 if c<=0.03928 else ((c+0.055)/1.055)**2.4
def L(r,g,b):
    return 0.2126*lin(r)+0.7152*lin(g)+0.0722*lin(b)
def ratio(a,b):
    l1,l2=L(*a),L(*b)
    hi,lo=max(l1,l2),min(l1,l2)
    return (hi+0.05)/(lo+0.05)
accent=(159,79,36); alpha=0.32
for name,bg in [("bg",(247,246,242)),("bg-raised",(255,253,250)),("bg-sunken",(239,236,229))]:
    eff=tuple(round(alpha*a+(1-alpha)*b) for a,b in zip(accent,bg))
    print(f"over {name}: outline≈{eff} contrast={ratio(eff,bg):.2f}:1 (need >=3:1)")
PY

Repository: fakechris/Involute

Length of output: 356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate app.css =="
fd -a 'app.css' . | sed 's#^\./##'

echo
echo "== relevant CSS variables and focus rules =="
if [ -f packages/web/src/styles/app.css ]; then
  nl -ba packages/web/src/styles/app.css | sed -n '1,230p'
fi

echo
echo "== search for Atelier/light palette usage =="
rg -n "Atelier|atelier|accent|bg-raised|bg-sunken|--bg" -S . -g '!node_modules' -g '!dist' -g '!build' | head -n 200

Repository: fakechris/Involute

Length of output: 315


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant CSS variables and focus rules =="
[ -f packages/web/src/styles/app.css ] && sed -n '1,230p' packages/web/src/styles/app.css

echo
echo "== search for Atelier/light palette usage =="
rg -n "Atelier|atelier|accent|bg-raised|bg-sunken|--bg" -S . -g '!node_modules' -g '!dist' -g '!build' | head -n 200

Repository: fakechris/Involute

Length of output: 13771


🌐 Web query:

WCAG 2.4.11 Focus Appearance level AA contrast outline outline offset specification

💡 Result:

There is a common confusion regarding the numbering of the WCAG Focus Appearance criteria. Success Criterion 2.4.11 is titled Focus Not Obscured (Minimum) (Level AA) [1][2], while the criteria detailing specific requirements for focus indicator size and contrast are found under Success Criterion 2.4.13 Focus Appearance (Level AAA) [3][4][5]. WCAG 2.4.11 (Level AA) does not specify requirements for outline thickness, contrast, or offset [1][2]. Its purpose is to ensure that when a component receives keyboard focus, it is not entirely hidden by author-created content (such as sticky headers or footers) [1][2]. Requirements for focus indicator size and contrast are specified in WCAG 2.4.13 (Level AAA) [3][4]. While 2.4.13 is the relevant standard for appearance, it does not mandate specific CSS properties like outline or outline-offset, but rather defines functional requirements for the indicator's area and contrast change [3][6][5]: 1. Size (Minimum Area): The focus indicator must be at least as large as the area of a 2 CSS pixel thick perimeter of the unfocused component [3][6][4]. 2. Contrast (State Change): There must be a contrast ratio of at least 3:1 between the pixels of the indicator in the focused state versus the same pixels in the unfocused state [3][6][5]. Although not required by the standard, using the CSS outline and outline-offset properties is a widely accepted technique to meet these criteria [6][7][5]. A common, compliant implementation is to use an outline at least 2px thick, often combined with an outline-offset to separate the indicator from the component's edge, ensuring it remains distinct [6][4][5]. Top Results: [1], [2], [3], [6], [4], [5]

Citations:


Increase the Atelier focus-ring contrast

The 32% mixed light accent produces ~1.6:1 contrast against the parchment backgrounds, below WCAG 2.4.13 Focus Appearance’s 3:1 state-change requirement. Use var(--accent) or a stronger contrast shade for :focus-visible so keyboard focus remains reliably visible in the light theme.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/styles/app.css` around lines 171 - 175, Increase the
contrast of the :focus-visible outline in app.css by replacing the 32% mixed
accent color with var(--accent) or another stronger-contrast accent shade.
Preserve the existing 3px outline and 2px offset while ensuring the light-theme
focus ring meets the required visibility.


button,
input,
select,
Expand Down Expand Up @@ -254,7 +279,7 @@ kbd {
height: 100vh;
flex-direction: column;
justify-content: space-between;
border-right: 1px solid var(--border-subtle);
border-right: 1px solid var(--border);
background: var(--bg-sunken);
padding: 10px 8px 8px;
}
Expand Down Expand Up @@ -1427,7 +1452,6 @@ kbd {
box-shadow: none;
cursor: grab;
transition:
transform var(--dur-2) var(--ease),
border-color var(--dur-2) var(--ease),
background var(--dur-2) var(--ease);
touch-action: none;
Expand Down Expand Up @@ -2743,7 +2767,7 @@ kbd {
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--border-subtle);
border-bottom: 1px solid var(--border);
background: var(--bg-sunken);
padding: 10px var(--content-gutter);
}
Expand Down Expand Up @@ -3181,6 +3205,5 @@ kbd {
}

.dialog-modal::backdrop {
background: rgba(0,0,0,0.5);
backdrop-filter: blur(2px);
background: var(--bg-overlay);
}
Loading