diff --git a/app/routes/_renderer.tsx b/app/routes/_renderer.tsx
index 55b68c7..140967e 100644
--- a/app/routes/_renderer.tsx
+++ b/app/routes/_renderer.tsx
@@ -9,13 +9,13 @@ import "../styles/sprinkles.css.js";
// 1. Blocking script: set data-theme before paint
// 2. Critical inline CSS: base colors, font, line-height — no external request needed
-// Uses the same OKLCH values as the theme tokens. When full CSS loads, it takes over seamlessly.
+// Mirrors the Midnight bg/text token values. When full CSS loads, it takes over seamlessly.
const themeBootstrapBody = `(function(){var t=localStorage.getItem('theme');if(t){document.documentElement.dataset.theme=t}else if(matchMedia('(prefers-color-scheme:dark)').matches){document.documentElement.dataset.theme='dark'}})()`;
const headInlineStyles = ``;
// Inline ThemeToggle island CSS (~300B) in prod. The island loads dynamically
@@ -114,6 +114,23 @@ export default jsxRenderer(({ children, title, description, ogImage }, c) => {
{headInline}
+
+ {/* The only Junicode glyphs we render are the h1 display headline, which
+ is italic. Preload the italic face (not Roman, which is never used) so
+ the hero does not flash the fallback serif before the face swaps in. */}
+
{!import.meta.env.PROD && }
No users match this search.
Airglow subscribes to Jetstream, matches your filter, and runs the action you picked.
@@ -157,7 +156,6 @@ export default createRoute(async (c) => {
Duplicate any of these to your account in one click.
Airglow works with any lexicon: Bluesky, community, or your own. Here are a few
diff --git a/app/routes/lexicons/[nsid].tsx b/app/routes/lexicons/[nsid].tsx
index 29e86b0..f790a21 100644
--- a/app/routes/lexicons/[nsid].tsx
+++ b/app/routes/lexicons/[nsid].tsx
@@ -80,7 +80,7 @@ export default createRoute(async (c) => {
description={description}
actions={
No automations are using this lexicon yet. Webhooks & Automations for the AT Protocol No automations yet. Secrets are not configured on this instance.
@@ -47,7 +47,7 @@ export default createRoute(async (c) => {
No activity yet. Hnb9uf`%7Y{JE
z;G#h-dUQEZ>l`A6wFeE60+F%Ubb}=DVRCL!L^TiLK2&jhJk+`wk4n7lFWSW;Zuf!4
zLa@6RlD4qRks^xQ22xDHREyhvh;;QIwCWA66?6wVoP)st^)BFoSVs%sve?Id5YjkU
zC^q0wgG(4xCcygsAfJQyFtz`a82JD1MsC}JRKR3GG&p6gW57}}f@C2W*+hs4j6^Ma
zu(b#NYarVW)dy+s?S}GjP{=JCSlLWk+X#ojR2PQ14!^;WS_VlC{MN4jJ+KxPk}Mfa
z3>!f88@L^U8c(DejkAov=#?OwjMUy!W&rn(kY$Nu<8n7*e2)lo2;^8$?*Z8qP|XV&
zzeD%;|Mj4J_J0PV6v2ISC&UC~a}jKi4M;rD3N%pf2NYY7{w7os)K-Mu9|xjAA^Jby
z|8o!Automations people run
Three guarantees, no surprises.
Popular lexicons
Airglow
- Identity
bg, surface, text, accent, success, error..."]
+ B["Per-app brand layer (rationalized)
bsky, cosmik, sifa, tangled... + ColorKey union"]
+ SP["sprinkles.css.ts (atomic props)"]
+ C["Component + page CSS
app/components, app/styles/pages, island *.css.ts"]
+ OG["OG rasterizer (satori)
hard-coded hex mirror of light semantics"]
+
+ P --> S
+ S --> SP
+ S --> C
+ B --> C
+ SP --> C
+ S -. "manually mirrored as hex" .-> OG
+```
+
+The dashed edge to OG is the one place values are copied by hand (satori can't parse
+OKLCH) — flagged so the OG redesign updates it deliberately rather than letting it
+drift.
+
+---
+
+## Implementation Units
+
+Phased: Foundation (U1-U4) → Primitives (U5-U8) → Assets (U9-U10) → Page sweep
+(U11-U16). Pages depend on foundation and primitives landing first.
+
+### Phase 1 — Foundation
+
+### U1. Self-host the brand typefaces
+
+- Goal: Replace system-fonts-only with self-hosted Overused Grotesk, LT Superior,
+ and Junicode, preserving FOUC behavior. (R1, R2)
+- Dependencies: none
+- Files:
+ - `public/fonts/` — add woff2 (site) and TTF/OTF (OG) for the three faces;
+ remove Inter once OG migrates (U10).
+ - `app/styles/tokens/typography.ts` — update both `fontFamily.sans` and
+ `fontFamily.ui` (brand faces first, then fallbacks); this is the source
+ `global.css.ts` re-exports.
+ - `app/styles/global.css.ts` — `@font-face` (`globalFontFace`), `font-display`
+ choice. The `html` font-family already reads `fontFamily.sans`.
+ - `app/routes/_renderer.tsx` — update inline critical font stack (line ~16); add
+ `` near the icon
+ link (~120).
+ - `vite.config.ts` / build — verify `public/fonts` is copied to `dist/fonts` for
+ the prod static handler (the OG loader expects `./dist/fonts`).
+ - Convention note: update the "// System font stacks — no external fonts loaded"
+ comment in `app/styles/tokens/typography.ts:1`. The font convention lives there
+ (and in user memory), not in `CLAUDE.md` — do not edit a non-existent CLAUDE.md
+ rule.
+- Approach: First confirm a satori-loadable static format (TTF/OTF) exists for each
+ of the three faces; if one is missing, decide its OG substitution deliberately (see
+ Open Questions) rather than letting OG fall back silently. Use `font-display: swap`
+ (or `optional`). Subset to Latin to bound payload, but verify the subset covers real
+ content (handles, NSIDs, automation titles can carry non-ASCII glyphs). Keep three
+ sync points aligned: `typography.ts` (sans + ui), the inline critical stack in
+ `_renderer.tsx:16`, and the `@font-face` set. Do not touch the OG TTF loader yet
+ (U10 owns OG).
+- Patterns to follow: existing inline-critical-CSS approach in `_renderer.tsx`; the
+ dev-CSS plugin in `vite.config.ts:29-66` already serves vanilla CSS in dev.
+- Test scenarios: Test expectation: none -- font wiring is verified by build +
+ visual/network check (preload fires, woff2 served, no FOUC), not unit tests.
+- Verification: `vp build` succeeds; dev and prod render the brand faces; preload
+ links present; no flash of fallback on reload; OG route still renders (still on
+ Inter TTF until U10).
+
+### U2. Primitive color scale layer + derived theming
+
+- Goal: Introduce the five Figma scales as primitives and re-point semantic tokens
+ to them; remove the hand-maintained dark mirror. (R4, R5)
+- Dependencies: none
+- Files:
+ - `app/styles/tokens/palette.ts` (new) — Midnight (100-1000), Infrared, Sky, Glow,
+ Aurora (100-700) as OKLCH ramps.
+ - `app/styles/tokens/colors.ts` — rewrite `lightColors`/`darkColors` to map
+ semantic names onto palette steps.
+ - `app/styles/theme.css.ts` — keep `vars` shape; derive the
+ `:root:not([data-theme])` block (`:84-141`) for colors, shadows, and focus.ring
+ (see Key Decisions).
+ - `app/styles/tokens/index.ts` — export palette.
+ - `app/routes/_renderer.tsx` — update the inline critical-CSS bg/text OKLCH
+ (`:16-19`) and the `theme-color` meta (`:125-126`) to the new bg values for both
+ themes; these are hard-coded copies of the bg/text tokens this unit changes.
+ - `app/styles/global.css.ts` — wrap the universal color/bg/border/shadow transition
+ (`:91-95`) in `@media (prefers-reduced-motion: no-preference)` while touching the
+ token layer.
+ - `lib/og/automation.ts` — update the `COLORS` hex mirror (`:90-99`) to the new
+ light-token equivalents (satori needs hex), so OG does not drift during the gap
+ before U10.
+- Approach: Pull exact step values from the Figma Colors node (`152:457`) via
+ `get_variable_defs`. Semantic mapping decisions (which step is `bg`, `accent`,
+ etc.) follow the brand guideline usage. Check WCAG AA contrast (4.5:1 body, 3:1
+ large text) for text-on-surface and accent-on-bg before committing the mapping.
+- Patterns to follow: existing `createGlobalThemeContract` / `createGlobalTheme`
+ structure in `theme.css.ts`.
+- Test scenarios: Test expectation: none -- token values verified by visual review
+ in both themes; build proves the contract still resolves.
+- Verification: `vp check` passes; every semantic var (color, shadow, focus)
+ resolves in light, dark, and first-paint prefers-dark; theme toggle works; the
+ inline critical CSS, `theme-color`, and OG `COLORS` all reflect the new bg/text
+ values; the dark block no longer lists vars by hand.
+
+### U3. Typography scale tokens
+
+- Goal: Express the Figma type scale (subheading, heading xs-2xl, body
+ regular/semibold/bold xs-xl, mono medium/bold xs-xl). (R3)
+- Dependencies: U1 (fonts available)
+- Files:
+ - `app/styles/tokens/typography.ts` — `fontSize`, `fontWeight`, `lineHeight`,
+ `letterSpacing` to match Figma steps.
+ - `app/styles/global.css.ts` — heading element sizes (`h1`-`h4`) mapped to the new
+ scale.
+ - `app/styles/sprinkles.css.ts` — confirm `fontSize`/`fontWeight`/`lineHeight`
+ atoms still cover usage.
+- Approach: Read exact numbers from the Figma Headings/Body/Mono nodes
+ (`152:311`, `152:346`, `152:415`). Confirm Junicode's role (display/all-headings)
+ before mapping `h*`.
+- Patterns to follow: current `global.css.ts` heading rules.
+- Test scenarios: Test expectation: none -- scale verified visually.
+- Verification: headings/body/mono render at the spec sizes/weights in both themes.
+
+### U4. Rationalize the per-app brand palette
+
+- Goal: Audit and align the per-app colors to the new scales; prune unused. (R6)
+- Dependencies: U2 (scales exist)
+- Files:
+ - `app/styles/tokens/colors.ts` — per-app values (base + `*Subtle`).
+ - `app/styles/theme.css.ts` — contract names + derived dark mirror entries.
+ - `lib/automations/follow-targets.ts` — `ColorKey` union (prune dropped keys).
+ - `lib/automations/action-catalogue.ts` — the `catId` union (`:33`) and `colorKey`
+ propagation; `data-cat` is rendered from `info.colorKey ?? info.catId`
+ (`app/components/ActionHeader/index.tsx:35`), so catIds are the other half of the
+ value space.
+ - `app/styles/action-header.css.ts` — `data-cat` selectors (primary consumer).
+ - `app/islands/AutomationForm.css.ts` — `catDot` category-level keys.
+ - Editors referencing colorKeys: `app/islands/action-editors/*` (semble-save,
+ bsky-post, kipclip-annotation, calendar-rsvp) only if keys are renamed.
+- Approach: Decide which per-app colors survive (keep ones tied to live action
+ types; drop orphans). Align retained hues' lightness/chroma to the new scales so
+ tiles read on-brand. Keep keys string-matched via `data-cat`; reconcile both the
+ `ColorKey` union and the `catId` union so every value that can reach `data-cat` has
+ a matching selector. Note the `social` catId currently has no selector (it always
+ overrides to a per-target colorKey) — preserve that invariant rather than "fixing"
+ it.
+- Patterns to follow: the `ColorKey`-to-`data-cat` contract noted in
+ `lib/automations/follow-targets.ts`.
+- Test scenarios:
+ - Happy path: every action type still resolves to a defined tile color (no
+ unstyled `data-cat`).
+ - Edge case: a pruned key has no remaining references in editors/catalogue.
+- Verification: `vp check` passes; action-header tiles and cat dots render correct
+ colors for all current action types in both themes.
+
+### Phase 2 — Primitives
+
+### U5. Restyle Button to the Figma matrix
+
+- Goal: Button implements primary/secondary/tertiary/link × default/hover/disabled
+ × lg/sm. (R7, R10)
+- Dependencies: U2, U3
+- Files:
+ - `app/components/Button/index.tsx`, `app/components/Button/styles.css.ts`,
+ `app/components/Button/index.test.tsx` (new).
+ - Call sites using old variant names (`ghost`, `danger`) and unsized call sites
+ across `app/routes`, `app/islands`, `app/components`.
+- Approach: Rename `styleVariants` to the Figma set; keep the ``-when-`href`
+ behavior. Two migration hazards beyond the rename: (1) the existing `md` size has
+ no Figma equivalent and is the current default prop — collapse it into `lg` and set
+ the default to `lg`, or the ~11 unsized call sites break. (2) `danger` has no Figma
+ type — decide before migrating whether to extend the matrix with a `destructive`
+ type or keep it as a local composition over `tertiary` (`AutomationToolbar`
+ composes `ghost`+`danger` today; see Open Questions). Map removed variants at call
+ sites via a typed old->new map so a missed migration is a compile error, not a
+ silent remap to a wrong-but-valid variant. Confirm `:focus-visible` is visible on
+ all four variant backgrounds in both themes (the component has no focus rule today;
+ it inherits the global ring) — add per-variant focus styles if the global ring
+ fails on any. Disabled state per Figma; respect the pre-hydration disabled
+ convention for island-hosted buttons.
+- Patterns to follow: existing `styleVariants` structure in `Button/styles.css.ts`.
+- Test scenarios:
+ - Happy path: renders each type/size; renders `` with `href`, `)ok^k#pECl_3KY|TmLF`~)_&*s|&i?QD_u_xg
zzwZ!v5Q!Y?klYCK$NwM(hX1AiUxHL&Vo=(Hn$HW81+kIM0*U>*2O?4Ne^AQtK#2Xn
zj$lJr=%zqKsYQZ(_g{^H0Yrn$gv1Jn1;fbpBeP-h#M3Z4kaa-v4?+Yp)(~>&tpEG|
z??V?Om5)eq|1bZ)4)Gr-|02ZEO@zrnQo{f9Ao>5zAQBb-PeA2k=l!?)fA=4bGzHNC
zNrezL6-gX*9CEHA$u?B;q)E~XR}2eVxWE82FmofwL9h}N#Kw>RKgBOkt}O8`AY{Y;
zOjH#E5d;6PGB7}DWRP3_e*%%1m^Lnh
;GG9Ngyo--%m}f#LrJm@GIKLplHFKp9BX|E-Af7fG1TOyWX-
z&>S5lj>hkT|K~uZ9wp|3_2O;;fu$)SAf*r`CQzyuQVLOO3T68LW0~P6(?mwAC!qB)
zT(dyyYpfoC*r4@G42)c}!6H{cYhAb^xS~L-eXPzfFj!q;U}Rutd&%~m?F0KyPJXTh
zTss&T*