/* ── Theme presets (MIN-696) ────────────────────────────────────────────── One palette per mode, chosen on the account (`lib/theme-presets.ts`). The pre-paint script writes both choices onto before the first paint: and the mode class decides which one is read. A light palette is keyed on `html:not(.dark)`, a dark one on `html.dark`, so switching modes (or the OS switching under "system") lands on the palette saved for that mode. Specificity is deliberate: `html:not(.dark)[…]` and `html.dark[…]` weigh (0,2,1), above every mode-level override in globals.css — `:root`, `.dark` and the app shell's `.dark[data-app-shell]` (0,2,0). That is why the presets live in their own file without having to come last. The minddy presets have NO rule here: an absent or default attribute leaves app/tokens.css untouched, pixel for pixel, with its lavender accent (MIN-840). An unknown id, such as the retired `violet-*`, falls back to it. Only neutral surfaces, text, focus rings and the primary action move. Borders are not set here: app/tokens.css draws them as a light veil of `--foreground`, so each palette tints them with its own text color at the same weight. Only the high-contrast presets draw them heavier. Status, priority, destructive, project, mention and rich-text colors keep their meaning in every palette. Values are plain hexadecimal so `lib/theme-presets.test.ts` can measure each pair's WCAG contrast: normal text 4.5:1, focus ring 3:1, and 7:1 text with 3:1 borders for the high-contrast presets. */ /* ── Accent presets ── only the accent color changes: primary actions and the focus ring. Every surface — content, sidebar, top and bottom bars, tabs, selection — keeps minddy's own neutral values, and `--brand` stays lavender. "Noir" (light) and "Blanc" (dark) are the ink actions minddy had before lavender became its default: near-black buttons in light mode, near-white ones in dark mode, with a neutral focus ring. */ html:not(.dark)[data-theme-light="ink-light"] { --primary: #16181e; --primary-foreground: #ffffff; --ring: #555555; --sidebar-primary: #16181e; --sidebar-primary-foreground: #ffffff; --sidebar-ring: #555555; } html.dark[data-theme-dark="ink-dark"] { --primary: #f4f5f8; --primary-foreground: #0f1216; --ring: #b7b7b7; --sidebar-primary: #f4f5f8; --sidebar-primary-foreground: #0f1216; --sidebar-ring: #b7b7b7; } html:not(.dark)[data-theme-light="blue-light"] { --primary: #2563eb; --primary-foreground: #ffffff; --ring: #2563eb; --sidebar-primary: #2563eb; --sidebar-primary-foreground: #ffffff; --sidebar-ring: #2563eb; } html.dark[data-theme-dark="blue-dark"] { --primary: #60a5fa; --primary-foreground: #0a0a0a; --ring: #60a5fa; --sidebar-primary: #60a5fa; --sidebar-primary-foreground: #0a0a0a; --sidebar-ring: #60a5fa; } html:not(.dark)[data-theme-light="green-light"] { --primary: #15803d; --primary-foreground: #ffffff; --ring: #15803d; --sidebar-primary: #15803d; --sidebar-primary-foreground: #ffffff; --sidebar-ring: #15803d; } html.dark[data-theme-dark="green-dark"] { --primary: #4ade80; --primary-foreground: #0a0a0a; --ring: #4ade80; --sidebar-primary: #4ade80; --sidebar-primary-foreground: #0a0a0a; --sidebar-ring: #4ade80; } html:not(.dark)[data-theme-light="orange-light"] { --primary: #c2410c; --primary-foreground: #ffffff; --ring: #c2410c; --sidebar-primary: #c2410c; --sidebar-primary-foreground: #ffffff; --sidebar-ring: #c2410c; } html.dark[data-theme-dark="orange-dark"] { --primary: #fb923c; --primary-foreground: #0a0a0a; --ring: #fb923c; --sidebar-primary: #fb923c; --sidebar-primary-foreground: #0a0a0a; --sidebar-ring: #fb923c; } html:not(.dark)[data-theme-light="rose-light"] { --primary: #e11d48; --primary-foreground: #ffffff; --ring: #e11d48; --sidebar-primary: #e11d48; --sidebar-primary-foreground: #ffffff; --sidebar-ring: #e11d48; } html.dark[data-theme-dark="rose-dark"] { --primary: #fb7185; --primary-foreground: #0a0a0a; --ring: #fb7185; --sidebar-primary: #fb7185; --sidebar-primary-foreground: #0a0a0a; --sidebar-ring: #fb7185; } /* ── Solarized Light ── base3 / base2 surfaces, base02 ink, blue focus. */ html:not(.dark)[data-theme-light="solarized-light"] { --background: #fdf6e3; --foreground: #073642; --card: #fdf6e3; --card-foreground: #073642; --popover: #fdf6e3; --popover-foreground: #073642; --primary: #073642; --primary-foreground: #fdf6e3; --secondary: #eee8d5; --secondary-foreground: #073642; --muted: #eee8d5; --muted-foreground: #4f646b; --accent: #e9e2cc; --accent-foreground: #073642; --input: #e2dbc4; --ring: #268bd2; --sidebar: #eee8d5; --sidebar-foreground: #073642; --sidebar-primary: #073642; --sidebar-primary-foreground: #fdf6e3; --sidebar-accent: #e4ddc6; --sidebar-accent-foreground: #073642; --sidebar-border: #e2dbc4; --sidebar-ring: #268bd2; --control: #f4eddb; --control-hover: #e9e2cc; --surface-raised: #fdf6e3; --surface-sunken: #eee8d5; --surface-glass: rgb(238 232 213 / 60%); --text-tertiary: #4f646b; } /* ── Catppuccin Latte ── base / mantle / crust, text, mauve action. */ html:not(.dark)[data-theme-light="catppuccin-latte"] { --background: #eff1f5; --foreground: #4c4f69; --card: #eff1f5; --card-foreground: #4c4f69; --popover: #eff1f5; --popover-foreground: #4c4f69; --primary: #8839ef; --primary-foreground: #ffffff; --secondary: #e6e9ef; --secondary-foreground: #4c4f69; --muted: #e6e9ef; --muted-foreground: #5c5f77; --accent: #dce0e8; --accent-foreground: #4c4f69; --input: #ccd0da; --ring: #1e66f5; --sidebar: #e6e9ef; --sidebar-foreground: #4c4f69; --sidebar-primary: #8839ef; --sidebar-primary-foreground: #ffffff; --sidebar-accent: #dce0e8; --sidebar-accent-foreground: #4c4f69; --sidebar-border: #ccd0da; --sidebar-ring: #1e66f5; --control: #e6e9ef; --control-hover: #dce0e8; --surface-raised: #eff1f5; --surface-sunken: #e6e9ef; --surface-glass: rgb(230 233 239 / 60%); --text-tertiary: #5c5f77; } /* ── Nord Light ── Snow Storm surfaces, Polar Night ink, Frost action. */ html:not(.dark)[data-theme-light="nord-light"] { --background: #eceff4; --foreground: #2e3440; --card: #eceff4; --card-foreground: #2e3440; --popover: #eceff4; --popover-foreground: #2e3440; --primary: #4a6b94; --primary-foreground: #ffffff; --secondary: #e5e9f0; --secondary-foreground: #2e3440; --muted: #e5e9f0; --muted-foreground: #4c566a; --accent: #d8dee9; --accent-foreground: #2e3440; --input: #d8dee9; --ring: #5e81ac; --sidebar: #e5e9f0; --sidebar-foreground: #2e3440; --sidebar-primary: #4a6b94; --sidebar-primary-foreground: #ffffff; --sidebar-accent: #d8dee9; --sidebar-accent-foreground: #2e3440; --sidebar-border: #d8dee9; --sidebar-ring: #5e81ac; --control: #e5e9f0; --control-hover: #d8dee9; --surface-raised: #eceff4; --surface-sunken: #e5e9f0; --surface-glass: rgb(229 233 240 / 60%); --text-tertiary: #4c566a; } /* ── High contrast (light) ── pure black on white, borders that read. */ html:not(.dark)[data-theme-light="high-contrast-light"] { --background: #ffffff; --foreground: #000000; --card: #ffffff; --card-foreground: #000000; --popover: #ffffff; --popover-foreground: #000000; --primary: #000000; --primary-foreground: #ffffff; --secondary: #ebebeb; --secondary-foreground: #000000; --muted: #ebebeb; --muted-foreground: #333333; --accent: #e0e0e0; --accent-foreground: #000000; --border: #767676; --input: #767676; --ring: #0047b3; --sidebar: #f0f0f0; --sidebar-foreground: #000000; --sidebar-primary: #000000; --sidebar-primary-foreground: #ffffff; --sidebar-accent: #dcdcdc; --sidebar-accent-foreground: #000000; --sidebar-border: #767676; --sidebar-ring: #0047b3; --control: #ebebeb; --control-hover: #dcdcdc; --surface-raised: #ffffff; --surface-sunken: #f0f0f0; --surface-glass: rgb(240 240 240 / 95%); --text-tertiary: #333333; --scrollbar-thumb: rgb(0 0 0 / 45%); --scrollbar-thumb-hover: rgb(0 0 0 / 65%); } /* ── Solarized Dark ── base03 content, base02 chrome, base2 ink. */ html.dark[data-theme-dark="solarized-dark"] { --background: #002b36; --foreground: #eee8d5; --card: #002b36; --card-foreground: #eee8d5; --popover: #002b36; --popover-foreground: #eee8d5; --primary: #eee8d5; --primary-foreground: #002b36; --secondary: #0a3d4a; --secondary-foreground: #eee8d5; --muted: #073642; --muted-foreground: #93a1a1; --accent: #0d4351; --accent-foreground: #eee8d5; --input: #1a4c58; --ring: #268bd2; --sidebar: #073642; --sidebar-foreground: #eee8d5; --sidebar-primary: #eee8d5; --sidebar-primary-foreground: #002b36; --sidebar-accent: #0d4351; --sidebar-accent-foreground: #eee8d5; --sidebar-border: #164651; --sidebar-ring: #268bd2; --control: #0a3d4a; --control-hover: #114957; --surface-raised: #073642; --surface-sunken: #00222b; --surface-glass: rgb(7 54 66 / 60%); --text-tertiary: #93a1a1; } /* ── Catppuccin Mocha ── base / mantle / crust, text, mauve action. */ html.dark[data-theme-dark="catppuccin-mocha"] { --background: #1e1e2e; --foreground: #cdd6f4; --card: #1e1e2e; --card-foreground: #cdd6f4; --popover: #181825; --popover-foreground: #cdd6f4; --primary: #cba6f7; --primary-foreground: #11111b; --secondary: #313244; --secondary-foreground: #cdd6f4; --muted: #313244; --muted-foreground: #a6adc8; --accent: #313244; --accent-foreground: #cdd6f4; --input: #45475a; --ring: #b4befe; --sidebar: #181825; --sidebar-foreground: #cdd6f4; --sidebar-primary: #cba6f7; --sidebar-primary-foreground: #11111b; --sidebar-accent: #313244; --sidebar-accent-foreground: #cdd6f4; --sidebar-border: #313244; --sidebar-ring: #b4befe; --control: #313244; --control-hover: #45475a; --surface-raised: #1e1e2e; --surface-sunken: #11111b; --surface-glass: rgb(24 24 37 / 60%); --text-tertiary: #9399b2; } /* ── Nord ── Polar Night surfaces, Snow Storm ink, Frost action. */ html.dark[data-theme-dark="nord-dark"] { --background: #2e3440; --foreground: #eceff4; --card: #2e3440; --card-foreground: #eceff4; --popover: #2e3440; --popover-foreground: #eceff4; --primary: #88c0d0; --primary-foreground: #2e3440; --secondary: #3b4252; --secondary-foreground: #eceff4; --muted: #3b4252; --muted-foreground: #b4bccb; --accent: #434c5e; --accent-foreground: #eceff4; --input: #434c5e; --ring: #88c0d0; --sidebar: #272c36; --sidebar-foreground: #eceff4; --sidebar-primary: #88c0d0; --sidebar-primary-foreground: #2e3440; --sidebar-accent: #3b4252; --sidebar-accent-foreground: #eceff4; --sidebar-border: #3b4252; --sidebar-ring: #88c0d0; --control: #3b4252; --control-hover: #434c5e; --surface-raised: #3b4252; --surface-sunken: #242933; --surface-glass: rgb(46 52 64 / 60%); --text-tertiary: #b4bccb; } /* ── High contrast (dark) ── pure white on black, borders that read. */ html.dark[data-theme-dark="high-contrast-dark"] { --background: #000000; --foreground: #ffffff; --card: #000000; --card-foreground: #ffffff; --popover: #0d0d0d; --popover-foreground: #ffffff; --primary: #ffffff; --primary-foreground: #000000; --secondary: #1f1f1f; --secondary-foreground: #ffffff; --muted: #1f1f1f; --muted-foreground: #d4d4d4; --accent: #2b2b2b; --accent-foreground: #ffffff; --border: #8c8c8c; --input: #8c8c8c; --ring: #66b3ff; --sidebar: #0d0d0d; --sidebar-foreground: #ffffff; --sidebar-primary: #ffffff; --sidebar-primary-foreground: #000000; --sidebar-accent: #2b2b2b; --sidebar-accent-foreground: #ffffff; --sidebar-border: #8c8c8c; --sidebar-ring: #66b3ff; --control: #1f1f1f; --control-hover: #2e2e2e; --surface-raised: #0d0d0d; --surface-sunken: #000000; --surface-glass: rgb(13 13 13 / 95%); --text-tertiary: #d4d4d4; --scrollbar-thumb: rgb(255 255 255 / 45%); --scrollbar-thumb-hover: rgb(255 255 255 / 65%); } /* Code blocks carry a fixed GitHub-like surface (globals.css). Under a preset they take the palette's sunken surface instead, so a Solarized page does not hold a gray box; the syntax colors stay the GitHub ones, which read on every light (resp. dark) surface above. */ /* Accent presets keep minddy's surfaces, so they keep its code blocks too. */ :is( html:not(.dark):is( [data-theme-light="solarized-light"], [data-theme-light="catppuccin-latte"], [data-theme-light="nord-light"], [data-theme-light="high-contrast-light"] ), html.dark:is( [data-theme-dark="solarized-dark"], [data-theme-dark="catppuccin-mocha"], [data-theme-dark="nord-dark"], [data-theme-dark="high-contrast-dark"] ) ) .code-block-node-view { --code-block-background: var(--surface-sunken); --code-block-foreground: var(--foreground); --code-block-muted: var(--muted-foreground); --code-block-hover: color-mix(in oklab, var(--foreground) 8%, transparent); } /* ── Transparency off ───────────────────────────────────────────────────── The account can turn the glass of floating surfaces off (Account → Preferences). Popovers, menus, selects and the command palette then paint their opaque `--popover`, and no backdrop blur runs anywhere in the app. The high-contrast presets imply it: a menu showing the page through itself loses exactly the contrast those presets exist to guarantee. */ :is( html[data-transparency="off"], html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :is( [data-slot="popover-content"], [data-slot="dropdown-menu-content"], [data-slot="dropdown-menu-sub-content"], [data-slot="select-content"], [data-floating-surface], [role="menu"], .desktop-palette-overlay > .desktop-palette-surface ) { background-color: var(--popover); -webkit-backdrop-filter: none; backdrop-filter: none; } /* Pseudo-elements cannot sit inside `:is()`, hence three selectors. */ :is( html[data-transparency="off"], html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) *, :is( html[data-transparency="off"], html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) *::before, :is( html[data-transparency="off"], html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) *::after { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; } /* ── High contrast: boundaries the borderless design leaves out ──────────── minddy draws controls and surfaces without borders on purpose; separation comes from soft fills that high contrast cannot rely on (a #ebebeb input on white is 1.2:1). These rules apply ONLY under the two high-contrast palettes and add what WCAG 1.4.11 asks of them: a visible boundary of at least 3:1 for controls and floating surfaces, a visible keyboard focus — The field and select primitives declare `outline-none ring-0` — and a current/selected state that does not depend on a faint background. Outlines rather than borders: they take no layout space, so nothing moves in this mode either, and they follow `border-radius`. Unlayered, they win over the Tailwind utilities (`outline-none`, `outline-hidden`) that live in the utilities layer. `--border` is #767676 / #8c8c8c here (≥ 4.5:1). */ /* Controls: fields, selects, outline buttons, switches, input groups. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :is( [data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"], [data-slot="input-group"], [data-slot="command-input-wrapper"], [data-slot="switch"], button[data-variant="outline"], /* App-built fields (rich-text composers, filters, chips) share the same borderless fill rather than a slot. */ [class~="bg-control"], textarea, input:is([type="text"], [type="search"], [type="email"], [type="password"], [type="url"], [type="number"], :not([type])):not([data-slot="input-group-control"]) ) { outline: 1px solid var(--border); outline-offset: -1px; } /* An unchecked switch's white thumb sat on a near-white track. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) [data-slot="switch-thumb"] { box-shadow: 0 0 0 1px var(--border); } /* Floating surfaces, dialogs and sheets: their edge, not their shadow. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :is( [data-slot="popover-content"], [data-slot="dropdown-menu-content"], [data-slot="dropdown-menu-sub-content"], [data-slot="select-content"], [data-slot="hover-card-content"], [data-slot="tooltip-content"], [data-slot="dialog-content"], [data-slot="sheet-content"], [data-slot="side-panel-content"], [data-floating-surface], [role="menu"], .desktop-palette-surface ) { outline: 1px solid var(--border); outline-offset: -1px; } /* Scrims dim the page enough to read the dialog as the only active layer. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :is([data-slot="dialog-overlay"], [data-slot="sheet-overlay"], [data-slot="side-panel-overlay"]) { background-color: rgb(0 0 0 / 55%); } /* Translucent borders (`border-border/60`, an issue card's `border-foreground/10`…) lose the contrast the palette gives `--border`: draw them at full strength. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :is([class*="border-border/"], [data-card-surface]) { border-color: var(--border); } /* Tabs: every tab gets an edge; the selected one a strong one. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) .app-tab { outline: 1px solid var(--border); outline-offset: -1px; } /* Current, selected and highlighted states: a 2px ink outline, readable whatever the fill underneath — sidebar page, selected tab, pressed segment, highlighted menu / select / command-palette row. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :is( [aria-current]:not([aria-current="false"]), [role="tab"][aria-selected="true"], [aria-pressed="true"], [role="option"][aria-selected="true"], [data-slot="command-item"][data-selected="true"]:not(:where(html[data-app-layout="mobile"] *)), [data-highlighted], [data-slot="select-item"]:focus, [role="menuitem"]:focus, [role="menuitemradio"]:focus, [role="menuitemcheckbox"]:focus ) { outline: 2px solid var(--foreground); outline-offset: -2px; } /* Keyboard focus, everywhere: the focus ring color, outside the element. Menu rows and options keep the inset outline above — outside, their scrolling list would clip it. Containers that take focus when they open (dialogs, panels, menus) keep their 1px edge instead of a frame. */ :is( html:not(.dark)[data-theme-light="high-contrast-light"], html.dark[data-theme-dark="high-contrast-dark"] ) :focus-visible:not( [role^="menuitem"], [role="option"], [data-slot="select-item"], [role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], [data-slot$="-content"], .desktop-palette-surface ) { outline: 2px solid var(--ring); outline-offset: 1px; }