The runtime injects brand colors, safe-area insets, layout helper classes, and a signature glow — so widgets look at home on the phone and adapt to light/dark without shipping their own theme.
Brand color variables
The --preen-* variables below are set on :root, resolved for the current
appearance. The runtime also sets color-scheme and data-preen-scheme
("light" / "dark") on the document element.
Core tokens — the neutrals and the brand accent:
| Variable | Light | Dark |
|---|---|---|
--preen-accent | #C2143A | #E0435C |
--preen-bg | #F0EEE6 | #241D15 |
--preen-ink | #1A1915 | #ECE4D6 |
--preen-muted | #6B6862 | #A79C8C |
--preen-grain | #CDC1AA | #3A2D1F |
The bird palette — the six tints in the flock, one per bird. Each is a
warm, slightly muted hue tuned for cream in light mode and lifted for dark wood
in dark mode (the same way the accent lifts). --preen-bird (below)
resolves to whichever tint this device wears; the whole family is always
available, so you can theme with the palette regardless of the device’s own tint.
| Variable | Light | Dark |
|---|---|---|
--preen-green | #2EA043 | #32783A |
--preen-yellow | #DAAA3A | #EDC964 |
--preen-sunset | #E26A37 | #F18A54 |
--preen-navy | #1E3A7A | #486CBA |
--preen-crimson | #C2143A | #E0435C |
--preen-violet | #6A2D94 | #9C5CD6 |
--preen-crimson is the same hue as --preen-accent — the brand crimson, and
the app-icon bird.
.value { color: var(--preen-ink); }
.label { color: var(--preen-muted); }
.accent { color: var(--preen-accent); }
.chip { background: var(--preen-sunset); }
Your device’s bird color
Each paired phone is given its own bird tint — the color of its bird in the
Mac’s device strip (and of its “active” check in the widget list). The runtime
exposes that same color to widgets as --preen-bird, so a widget can theme
itself to match the device it’s running on:
/* Match this device's bird; fall back to the brand green if unset. */
.hero { color: var(--preen-bird, var(--preen-green)); }
Notes:
- Like the brand colors, it’s resolved for the current appearance (light/dark).
- It’s optional — always provide a fallback (
var(--preen-bird, …)). It’s absent when a widget is opened standalone, and from older Mac hubs. - Each device starts with a tint from its position in the paired-device strip
(cycling a small palette so two phones read as distinct); you can override it
per device on the Mac (right-click the phone → Bird color). Either way the
widget’s
--preen-birdfollows whatever color that phone shows on the Mac.
The built-in Tap demo uses it: its bird, button, and pulse ring all take
--preen-bird, so the on-screen bird matches the one it chirps on the Mac.
Layout regions & safe-area insets
The notch, calibration offset, and bottom bleed are pushed as variables — use them for padding so content clears the hardware. They re-resolve on rotation: in portrait the island is a top inset; in landscape it’s a side inset.
| Variable | Meaning |
|---|---|
--preen-safe-top | Clears the notch / Dynamic Island + user calibration. ~0px in landscape (the island moves to a side). |
--preen-safe-right / --preen-safe-left | Mirrored side insets — the larger of the two OS side insets, applied to both, so a one-sided landscape island reads as balanced margin. ~0px in portrait. |
--preen-safe-bottom | Always 0px — content bleeds to the bottom edge in both orientations. |
--preen-display-bottom | Symmetric chin (= top) for vertically-centered layouts. |
--preen-screen-radius | The device’s physical display corner radius (px). 0px on a square display or an older hub. |
The region classes apply these for you. The first three are full-bleed
layers (position: fixed; inset: 0, stacking in DOM order), so a widget can
use one region — or stack several as siblings when it needs more than one
coordinate space:
.preen-screen— the entire physical screen. Center a hero element here and it’s centered on the device, not the (top-inset) safe area. Content only: never paint backgrounds on it — nothing painted may cross the notch line..preen-safe— the safe area: clears the notch, mirrors side insets, bleeds to the bottom. Edge-anchored content lives here, and a background painted on it clips to the safe box (the body background shows through the notch strip)..preen-display—.preen-safeplus a symmetric bottom chin, for notch-safe flow layouts (full-width dashboards) that should read optically centered..preen-content— a full-bleed child wrapper (100%width/height,border-box).
More regions cover the strips .preen-display leaves over — the bands
between the display box and the screen edges. Each is a fixed band along one
edge (not a full-bleed layer), stacking with the others in DOM order:
.preen-unsafe-top— the strip above the display area (screen top → the top inset): the notch band. The notch / Dynamic Island may cover its middle, so use it for ambient decoration — a tint, a hairline, a corner ornament — never for content that must stay visible..preen-unsafe-bottom— the strip below the display area (the symmetric bottom chin). No hardware covers it — it’s “unsafe” only in that.preen-displayreserves it — but treat it the same way, so display-region layouts keep their even framing..preen-unsafe-left/.preen-unsafe-right— the side bands (screen edge → the mirrored side inset). Effectively zero-width in portrait; in landscape the island lives in one of them, so the same decoration-only rule applies.
In portrait the top strip holds the island and the side strips collapse; in landscape the top strip collapses and a side strip holds it. Decorate all four and the widget is correct in both orientations.
Each strip’s outer corners meet the glass, so anything painted flush inside
one should take the device’s own curve there:
--preen-screen-radius on the two outer corners.
One catch — the strips are usually shorter than the device radius, and CSS
auto-scales radii down to fit the box, which pulls your curve squarer than the
glass (the physically rounded corner then hides it). So don’t round the strip
itself: make the strip an overflow: hidden clip and round a taller inner
element, anchored to the strip’s outer edge, so its radius is never scaled:
/* A decorated top strip whose corners genuinely follow the glass. */
.notch-band { position: fixed; top: 0; left: 0; right: 0;
height: var(--preen-safe-top); overflow: hidden; }
.notch-band::before { content: ""; position: absolute; inset: 0 0 auto 0;
height: 100vh; background: #2a1020;
border-radius: var(--preen-screen-radius); }
Rule of thumb: centered → preen-screen · edge-anchored → preen-safe ·
painted → <body> (or clipped on preen-safe).
<body class="preen-glow"> <!-- hue: base fills the screen, wash confined to the safe view -->
<div class="preen-safe"> <!-- edge-anchored: a label hanging from the notch-safe top -->
<div class="preen-label">Studio</div>
</div>
<div class="preen-screen"> <!-- hero: centered on the device -->
<div class="preen-orb"><svg viewBox="0 0 24 24"><!-- glyph --></svg></div>
</div>
</body>
Regions are click-transparent while their direct children stay tappable, so an upper layer never swallows taps aimed at the one below. One caveat: a full-size child of an upper layer re-blocks it, so keep overlay layers sparse.
Orientation
The runtime keeps two classes in sync on <html> and <body> — style
per-orientation in pure CSS and never add your own resize /
orientationchange listeners (the CSS orientation media feature is also
unreliable inside the widget’s web view):
.stats { display: flex; flex-direction: column; }
.preen-landscape .stats { flex-direction: row; }
Everything above is orientation-aware for free: the inset variables re-push on
rotation, and the region classes re-resolve from them. Size elements in vmin
so they keep one physical size in both orientations, and use the classes only
to reflow — see Patterns.
Follow the device’s curve
A frame that hugs the screen edge looks wrong with square corners on a
round-cornered phone. Round it to the glass with --preen-screen-radius —
the device’s real display corner radius, in px:
/* A card pinned to the screen edges, rounded to match the device. */
.frame {
position: fixed; inset: 12px;
/* When the frame is inset from the edge, subtract that inset so its curve
stays concentric with the screen's — and clamp at 0 for square displays. */
border-radius: max(0px, calc(var(--preen-screen-radius) - 12px));
}
It’s 0px on a square display or an older hub that doesn’t report a radius, so a
literal fallback isn’t needed — square corners are the graceful default. Give a
non-zero fallback (var(--preen-screen-radius, 44px)) only if you’re previewing
the widget outside the phone and want a rounded look there too.
The glow
Add preen-glow to <body> for the signature Preen hue — a linear wash
rising from the bottom into the background. Tune it with three variables:
body {
--preen-glow-color: #C2143A; /* the hue */
--preen-glow-base: #000; /* what it fades into */
--preen-glow-intensity: 0; /* 0 = off … 1 = full */
}
Drive --preen-glow-intensity from your data for an at-a-glance signal (e.g. the
system widget ramps it with load):
window.PREEN.onData((d) => {
const hot = Math.max(0, Math.min(1, (d.cpu - 80) * 0.05));
document.body.style.setProperty("--preen-glow-intensity", hot.toFixed(3));
});
The style kit
A handful of themeable building blocks ship with the runtime so widgets share one
visual language instead of re-styling the same idioms. Each reads the brand
tokens (--preen-accent / --preen-muted) with literal fallbacks — so override
--preen-accent on <body> to re-tint a whole widget at once.
| Class | What it is |
|---|---|
.preen-press | The house “jelly” tap feedback — springs down on press, bounces back. Drop it on any tappable element. |
.preen-label | Small uppercase caption (section headers, device names, on/off state). Muted; add is-on to accent it. |
.preen-readout | A large live number/timer — tabular digits (no jitter as it ticks) at a light weight. Set your own font-size. |
.preen-faint | Ambient low-contrast text (a best-time line, idle stats). |
.preen-orb | The circular hero button (toggles, push-to-talk, power). Neutral dark by default; add is-on to fill it with the accent + a matching glow. Size with --preen-orb-size; nest an <svg> glyph. |
<body class="preen-glow" style="--preen-accent:#7b6cff; --preen-orb-size:148px">
<!-- a toggle: tap-springs, fills + glows when active -->
<div class="preen-orb preen-press is-on">
<svg viewBox="0 0 24 24"><!-- glyph --></svg>
</div>
<div class="preen-label is-on">Focused</div>
<!-- a faint, ticking count-up timer -->
<div class="preen-readout preen-faint" style="font-size:58px">12:34</div>
</body>
Toggle is-on from JS to switch state; the orb, label, and accent all follow it:
const on = !el.classList.contains("is-on");
orb.classList.toggle("is-on", on);
label.classList.toggle("is-on", on);
Display longevity (OLED)
Your widget may sit on an OLED iPhone for hours a day, mostly static — the classic burn-in scenario. The display protects itself (the whole stage drifts a few device pixels on a slow orbit, and the phone auto-sleeps after a period without interaction), but pixel drift only protects edges; what you render still decides how the panel ages. Rules of thumb:
- Keep the background black (or near it) — dark pixels barely age, and it’s
the Preen look anyway. The
.preen-glowwash is fine: soft gradients have no hard edges to etch in. - Never paint pure
#FFFFFF. Usevar(--preen-ink)for your brightest text — on a black canvas it reads white, at meaningfully lower emission. OLED lifetime falls superlinearly with luminance, so every step down from white buys panel life. - Prefer outlines and strokes over large bright fills (a power ring, not a solid disc). The interior of a big bright shape is what pixel drift can’t protect.
- Avoid static, maximum-brightness fine detail — thin white hairlines, sharp high-contrast borders that never move. Burn-in reads first at static edges.
- Blue subpixels age fastest; for elements that are lit all day, warm tones (the brand palette) are kinder than saturated blues/violets.