Source of truth: Design tokens are defined in
tailwind.config.js(colors, fonts, animation) andstyles/input.css(component classes:.btn,.card,.tool-card,.input,.label, etc.). This document describes intent and the patterns that the code actually ships — if the two disagree, the code wins and this file is the bug. Last reconciled with shipped code: 2026-08-22.
- What this is: Browser-based developer and everyday utilities served from a
single Cloudflare Worker. 47 tools in production (50 registered; 3 are
dev-only games —
ladder-game,roulette-wheel,marble-roulette— hidden in production viahiddenInProductioninsrc/utils/tool-registry.js).changelogwas retired as a catalog tool 2026-08-22 (the/changelogpage stays live). - Who it's for: Developers and general users on the open web.
- Space/industry: Online tool sites (CyberChef, 10015.io, IT-Tools, DevUtils).
- Project type: Web app (tool collection + Pipe Mode workspace).
- Differentiator: Tool input and output are processed in the browser. The Worker renders and routes pages; page and asset requests still pass through Cloudflare, and — when configured in production — non-personalized AdSense and Cloudflare Web Analytics make third-party requests. Sensitive tools never load ad scripts (see Ads & Privacy below). This is deliberately not an absolute "nothing ever leaves the browser" claim.
- Direction: Modern Utility (adopted 2026-08-22, replacing the earlier "Industrial/Utilitarian" look). Polished and friendly, but deliberately escapes default-blue SaaS slop via a violet brand anchor and a cyan accent, with soft depth on interaction.
- Decoration level: Minimal-plus — typography and spacing still do most of the work, but a single signature (violet→cyan gradient + smooth hover depth on the tool grid) gives the site something to be remembered by.
- Mood: Trustworthy, precise, functional, quietly distinctive. Feels like a well-made tool, not a template and not a marketing page. The design stays out of the way so the tools can work.
- Signature: The tool grid — violet→cyan gradient accents plus cards that lift
(
shadow-sm→shadow-md, 2px rise, violet border tint) on hover. - Reference sites: 10015.io (card grid), IT-Tools (sidebar + cards), CyberChef (recipe chaining UX).
- Family: Geist (sans) + Geist Mono (mono), loaded from Google Fonts.
- Loading: A single
<link rel="stylesheet">withpreconnecthints ingetStylesheetLinks()(src/utils/common-ui.js). The font URL ishttps://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap. (Do not re-add an@importof this URL tostyles/input.css— that caused a duplicate request on every page; removed 2026-08-22.) - Icon font: Material Symbols Rounded, self-hosted at
/fonts/material-symbols.woff2(@font-faceincommon-ui.js). Used by.info-hint. Keep it self-hosted — a CDN load previously 404'd. - The icon font is a subset, and stays one. The full family is 5.3MB; it was
98.5% of the home page's transfer until it was cut to the ~38KB of glyphs the
site actually uses.
scripts/icon-inventory.mjsscanssrc/for every icon name — literal<span class="material-symbols-rounded">content_copy</span>markup, thedata-i18nkeys on those spans, and data-drivenicon: '…'properties — andscripts/download-fonts.jsasks Google Fonts for exactly those ligatures via itsicon_namesparameter. Adding an icon to a route is enough: the nextnpm run buildsees the manifest (scripts/icon-manifest.json) is stale and refetches. An icon the scanner cannot see must be declared inALWAYS_INCLUDE, or it renders as its raw text instead of a glyph.src/utils/icon-subset.test.jsfails when the shipped subset and the scanned list drift apart.- The subset keeps the FILL/GRAD/opsz/wght axes, because
.info-hint .material-symbols-roundedsetsfont-variation-settings. - To get the unsubsetted font back for comparison, fetch the CSS at
https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swapand download the woff2 it points at.
- The subset keeps the FILL/GRAD/opsz/wght axes, because
- Roles:
- Display/Hero: Geist 700
- Body / UI: Geist 400/500/600
- Data / Code: Geist Mono 400/500
- Type scale: Standard Tailwind scale (
text-xs12 →text-5xl48). There is no customfontSizeconfig, so use Tailwind's class names directly. The hero usestext-4xl sm:text-5xl(src/ui/home.js).
Defined in tailwind.config.js under theme.extend.colors. Use the token names
below; never raw indigo-* / emerald-* (they read as AI-generated slop).
The Modern Utility palette (2026-08-22) is 6 named hues: primary (violet),
info (cyan accent), surface (slate neutrals), and semantic
success/warning/error.
primary-*— violet, brand accent (CTAs, links, active states, focus rings,::selection). Action =primary-600#7c3aed, hover =primary-700#6d28d9(the identity anchor), tint =primary-100#ede9fe. Chosen over blue/indigo to be distinct without reading as slop.info-*— cyan accent,info-600#0891b2. Formerly a byte-for-byte duplicate of the old blue primary; now a genuinely distinct accent used for the brand gradient, informational states, and (progressively) the Pipe-Mode signal. Still ~28 live usages across 15+ route files (cidr-calculator,encoding-workbench,oauth-debugger,cron-builder, …).surface-*— slate neutral scale, 50 (#f8fafc) → 950 (#0f0f12). Mid-tones are stock slate;900(#12141c, card elevation on dark) and950(#0f0f12, page background on dark) are tuned to neutral charcoal so dark mode reads near-black rather than slate-blue.success-*/warning-*/error-*— semantic. Standard Tailwind green/amber/red scales.- Pipe Mode accent (cyan): Pipe Mode uses the identity's
infocyan as its territory signal (.btn-info,info-*tints on the final-output box, recipe "Try it →", shared-pipeline banner). The legacy stock-teal accent was swapped out whenpipe.jswas re-skinned on 2026-08-26;teal-*classes no longer appear outside content-categorical maps. - Brand gradient: violet→cyan, via the
.text-gradient-brand/.bg-brand-gradient/.bg-brand-gradient-softutilities instyles/input.css. This is the signature accent — use sparingly (hero, section headers, tool-grid affordance), not as page-wide chrome. - Dark mode:
darkMode: "class". Both light and dark are authored deliberately (neither is merely derived). Mobile browser chrome (<meta name="theme-color">) is scheme-aware:#f8fafclight /#0f0f12dark.
The design language is shadcn/ui's — its token model and its "new-york" component set — ported onto this app's vanilla Tailwind + Cloudflare Worker stack. No React, no Radix, no client runtime is shipped (shadcn's own premise is "you own the code"). The Modern Utility palette lives inside shadcn's vocabulary:
- Semantic tokens are HSL triplets on
:root/.darkinstyles/input.css(--background,--foreground,--card,--primary,--secondary,--muted,--accent,--info,--border,--input,--ring,--radius, + semantic--success/--warning/--destructive).tailwind.config.jsmaps each to a utility, sobg-primary,text-muted-foreground,border-border,ring-ring,bg-info/10all resolve per theme. - Naming caveat: shadcn's
--accentis the neutral hover/highlight surface (slate-100/800), not a brand color. Our cyan brand accent is--info(violet stays--primary). Don't conflate them. --radius(0.5rem) drivesrounded-lg/md/sm(8 / 6 / 4px). Tool cards keep the explicitrounded-xl(12px).- Coexistence: the numeric hue scales (
primary-*,surface-*,info-*) remain intailwind.config.jsso the not-yet-migrated routes keep working; components migrate onto the semantic tokens during the Track A waves.
- Base unit / density: 4px, comfortable. Use Tailwind's default spacing scale
(
p-4,gap-4,space-y-16, …). - Grid: Responsive card grid on the home page; content max width
max-w-7xl(1280px). - Border radius (
--radius-*reference; realized via Tailwindrounded-*):sm4px ·md8px ·lg12px ·xl16px ·full9999px.- Buttons:
rounded-md(8px) — every.btnvariant. - Generic content cards (
.card, used on legal / FAQ / blog pages):rounded-lg(12px). - Home tool cards (
.tool-card):rounded-xl(16px)shadow-sm. - Contributor rule (mirrored in
AGENTS.md): card rounding isrounded-xl shadow-sm— neverrounded-2xl shadow-lg.
- Buttons:
- Breakpoints: Tailwind defaults (
sm640 ·md768 ·lg1024 ·xl1280).
- Approach: Minimal-functional. Use Tailwind utilities directly
(
transition-colors duration-200,animate-fade-in,animate-fade-in-up,animate-spin). - Durations in use: ~200ms for color/theme transitions, ~300ms for fades/accordions/spinners. Pipe Mode step changes get the longer end for visual continuity.
Component classes live in styles/input.css (@layer components + plain rules).
- Base:
.btn(rounded-md,text-sm,font-medium, focus ring). - Variants:
.btn-primary,.btn-secondary,.btn-ghost,.btn-danger,.btn-info(Pipe Mode cyan;info-700fill in light because white oninfo-600is 3.7:1, light-cyan fill with dark text in dark). Sizes:.btn-sm,.btn-xs.
.tool-page-shell— the shared route canvas (max-w-7xl) with responsive horizontal gutters and a consistent 32→48px vertical rhythm..tool-page-panel— the primarybg-card/border-bordertool surface,rounded-xl shadow-sm, with a restrained 2px violet→cyan top rule. A migrated route should use exactly one primary panel; nested functional groups remain lighter-weight instead of becoming cards inside cards.
The identity is only universal if it is unavoidable. The rule contributors follow:
Anything a user can interact with, and any surface that carries the page's identity, must come from a component class. Raw utilities are for LAYOUT.
| Element | Required class | Raw utilities still allowed for |
|---|---|---|
<button>, [role="button"] |
.btn-primary / .btn-secondary / .btn-ghost / .btn-danger / .btn-info |
w-, flex-, justify-, margins, responsive visibility |
| List-item button (tool palette, menu) | .menu-item |
width, margins |
| Icon-only button | that variant + .btn-icon (36px) or .btn-icon-sm (32px) |
position, md:hidden |
| Floating action button | .btn-fab |
fixed, bottom-, left-, z- |
<input>, <textarea>, <select> |
.input or .input-mono |
width, pl-/pr- for adornments, resize- |
| Nav search trigger | .input-search |
width |
| Severity filter toggle | .filter-chip + its semantic tint |
— |
| Page surface | .tool-page-panel |
— |
| Nested group | .tool-group (--flush / --inset) |
padding, flex chains |
Colour is never a raw utility on an interactive element — it comes from the
variant. The one exception is a tint that is the information rather than
decoration: .filter-chip carries bg-error-100/bg-warning-100 because the
severity is content. If the colour would be the same for every instance, it
belongs in the component.
Do not hand-roll to "avoid a load-order dependency." getThemeToggleButton
carried that comment for a hand-built copy of .btn-ghost; there is no such
dependency, because component classes ship in the same compiled stylesheet as
the utilities that would replace them. The hand-rolled copy had drifted: it used
focus:ring (which fires on mouse click) where .btn uses focus-visible:ring
(keyboard only).
Enforced by npm run test:ui-audit, which reports handrolled-button /
handrolled-input for any interactive element built from a utility stack.
-
.tool-groupis that lighter-weight group:rounded-lg border border-border p-4, with no fill and no shadow. It deliberately adds no second surface.--cardis#fff(light) and#12141c(dark), so the pre-rebuildbg-white dark:bg-surface-900group painted the same colour as the panel it sat in — a card inside an identical card, separated only by a hairline and ashadow-smthat cannot read on a same-colour ground. Filling it withbg-mutedwould break the palette the other way:.inputisbg-background(98% light / 6% dark), so an input would sit above a muted group in light but below it in dark — elevation inverting between themes. Leaving the fill inherited preserves the one rule the palette encodes: card = raised, background = recessed well. A group is a delineation, not an elevation..tool-group--flush(p-0) for containers whose children own the padding (editor panes, log tables) — pair withoverflow-hidden..tool-group--inset(bg-muted/60, transparent border) for read-only output regions. Safe to fill because they hold rendered output, not inputs, so the inversion above cannot arise.- Radius steps down when nesting: panel
rounded-xl→ grouprounded-lg. - Floating surfaces (dropdowns, modals, sheets) use
bg-popover— they do own a fill, because they leave the panel's plane entirely.
-
createToolHeader()emits stable.tool-header*hooks. Inside.tool-page-shell, those hooks opt into a larger 30→36px H1, semantic text tokens, a gradient-wash icon tile, a cyan trust badge, and responsive stacking. Outside the shell, the legacy utility presentation remains intact during the incremental migration. -
.content-page-shell/.content-page-panel— the prose counterpart, used by legal, FAQ, blog (index + article), and changelog. Same vertical rhythm as the tool shell but amax-w-4xlreading measure, since prose wants a shorter line than a tool workspace. Adopted 2026-08-22;changelogmoved off its one-offmax-w-3xlat the same time. The home page keeps its own hero/grid composition.Because
.tool-page-shell .tool-header-titleis a two-class selector, it outranks the single-class size utilitiescreateToolHeaderstill emits inline — so every tool page renders the canonical H1 regardless of those utilities. A route that hand-writes its own<h1>therefore only needs the.tool-header-titleclass; adding size/weight/color utilities alongside it is what caused the pre-rebuild H1 drift. -
Migration status: merged routes (
network-reference,repo-ops) established the shell. The first independent-tool wave (unit-converter,bandwidth-calculator,uuid-generator,caffeinate) adopted it on 2026-08-22. Route-specific controls, scripts, IDs, and educational content remain unchanged; later waves should repeat that wrapper-only migration before deeper component refactors.
.card— generic surface card,rounded-lg shadow-sm, now used for content inside prose pages (article cards, related-tool tiles); the page-level surface on those routes is.content-page-panel(rounded-xl), matching.tool-page-panel..tool-card—rounded-xl shadow-smsurface panel, dual-use: (1) static content panels inside tool routes (cidr, dns, port, protocol-headers, wireguard, wireshark) use thep-6default and stay static; (2) home-grid cards addp-4for density. The base class carries no hover/transform so static panels don't animate..tool-card-link— modifier added to the home grid's clickable cards for the signature hover:shadow-sm→shadow-md, a 2px rise (-translate-y-0.5), and a violet border tint, on a 200mstransition-all. Home cards use a 36px Material Symbols tile (bg-muted,text-primary), title, and description — not registry emoji. The card itself lifts; the glyph does not scale.- Hero wash —
.hexagon-pattern(home hero) is a low-opacity violet→cyan radial glow, the identity's hero signature. Kept subtle so it never competes with the H1/search contrast. The home hero is a left-aligned product header in the samemax-w-7xlcolumn as the catalog (search, middot trust line, flagship chips), not a centered marketing poster.
.input/.input-mono— full-width,rounded-md,border-input, focus-visible ring--ring(violet)..label— uppercase, tracked,text-xs font-semibold,text-muted-foreground.
.tabs-list+.tab-trigger— segmented tab bar on abg-mutedtrack; the active trigger ([aria-selected="true"]or.active) lifts tobg-backgroundshadow-sm. Keyboard-accessible (role="tablist"/aria-selected), focus ring via--ring. Used by the merged tabbed tools (repo-ops,network-reference) and adoptable site-wide.
.badge+.badge-default/.badge-secondary/.badge-info/.badge-outline— pill labels.badge-infois the cyan (--info) tinted variant for "Client-Side Only" / informational tags.- These live in
@layer components; Tailwind's JIT only emits them once a route references them, so they appear indist/styles.cssafter first use.
- Toasts — bottom-right notifications are built inline in JS
(
src/utils/common-ui.js,#toast-container+ ashowToast-style helper that composesbg-*/animate-*classes on the fly). There are no.toast*component classes in CSS (the old unused ones were removed 2026-08-22). .spinner(+-sm/-lg),.empty-state(+-icon/-title/-desc).[data-tooltip]— pure-CSS tooltip with-pos="bottom"/"right"variants..info-hint— small Material Symbols help icon that triggers a tooltip..cheatsheet— collapsible reference panel used inside tools..glass— translucent, blurred nav/header bar..mobile-tab-bar/.mobile-tab-btn/.mobile-tab-active— two-pane mobile switcher..rich-editor*— textarea + highlighted<pre>overlay with.re-*token colors (JSON, JWT, keywords).
The design must never imply absolute privacy. Current honest stance (enforced by
src/ui/honest-copy.test.js):
- Non-personalized AdSense only; Auto ads off;
ads.txtand the account meta ship with a publisher ID, ad units only after real slot IDs. - Allow-list (ads may appear):
home,json,legal. - Deny-list (never load ad scripts): password, SSH keys, Token Studio, WireGuard, certificates, secret scanner, encoding tools, and Pipe Mode.
- Copy may not claim "no tracking" / "never track" / "0 bytes stored."
- See
docs/adsense-integration.mdfor the authoritative ad rules.
New tool pages are frozen until the eight flagships (JSON, Token studio / JWT,
Regex, Cron, Password, SSH/certs, Curl, CIDR) beat the bookmarks people already
use. Existing routes stay live; the freeze is about new catalog entries, not
deletions. (Also stated in AGENTS.md.)
| Date | Decision | Rationale |
|---|---|---|
| 2026-03-27 | Initial design system created | Via /design-consultation; office-hours product context + competitive research (10015.io, IT-Tools, CyberChef). |
| 2026-03-27 | Geist over system fonts | An intentional typeface creates identity; ~50KB is worth every page feeling designed. |
| 2026-03-27 | Blue primary over indigo | Indigo (#6366f1) is the default AI-generated accent; blue (#2563eb) reads trustworthy and avoids the slop signal. |
| 2026-03-27 | Teal accent for Pipe Mode | A second color gives Pipe Mode its own territory: teal = pipes, blue = tools. |
| 2026-03-27 | Minimal decoration | Tool sites are utilities; every decoration pixel competes with the tool. Typography and spacing do the work. |
| 2026-08-22 | Doc reconciled with shipped code | Tool count 47 production (50 registered, 3 hidden games); languages 10; tool cards documented as rounded-xl; buttons rounded-md; teal via stock Tailwind; tailwind.config.js named as token source of truth. |
| 2026-08-22 | Privacy positioning made honest | Removed absolute "data never leaves the browser" framing to match the honesty-pass copy and the ad allow/deny model. |
| 2026-08-22 | Removed dead CSS custom properties from input.css | The :root/.dark --font-*/--primary-*/--accent-*/--space-*/--shadow-*/--ease-*/--dur-*/tint vars had zero consumers (theming is Tailwind dark:); only --scroll-* remain. |
| 2026-08-22 | Added missing .btn-teal |
pipe.js used btn btn-teal but the class was undefined, so the Pipe Mode "Copy" button rendered unstyled. |
| 2026-08-22 | New identity: Modern Utility (violet + cyan) | Full frontend rebuild (Track A). Primary blue→violet (#7c3aed/#6d28d9) to escape default-blue slop while staying trusted; info blue-dup→distinct cyan accent (#0891b2); surface zinc→slate with neutral-charcoal darks (#0f0f12). Chosen by the user over Precision-Instrument / Warm-Editorial / Brutalist-Mono directions. |
| 2026-08-22 | Signature = tool-grid gradient + hover depth | The one memorable element: violet→cyan brand gradient (.text-gradient-brand/.bg-brand-gradient) plus tool cards that lift on hover. Applied via shared tokens/classes so all ~50 pages re-skin without per-route color edits. |
| 2026-08-22 | Both light + dark authored deliberately | User chose "both" as the default; neither mode is a mechanical derivation. theme-color meta is scheme-aware. |
| 2026-08-22 | Adopt shadcn design language (no React) | User asked for shadcn; it is React/Radix-only, and this app is vanilla JS on CF Workers. Ported shadcn's token model (semantic HSL CSS vars → Tailwind utilities) + new-york component set (btn/card/input/tabs/badge) onto the existing stack instead of re-platforming. Violet→--primary, cyan→--info, slate→neutrals; --accent reserved as shadcn's neutral hover surface. Numeric scales kept alongside for small tinted elements (chips, badges, stat tiles) whose correct target is bg-muted, not bg-card. |
| 2026-08-26 | Pipe Mode re-skinned onto the identity | The last teal holdout: header rebuilt on createToolHeader (gradient icon tile + cyan Beta pill), .btn-teal→.btn-info (cyan, AA-safe fills both themes), final-output/banner/links teal→info-*, palette items on new .menu-item, step-card chrome on semantic border-border/bg-muted, recipe cards on .tool-card-link signature hover, icon buttons on .btn-icon-sm. Behavior, IDs, and the client contract registry untouched. Follow-up same day: .tool-page-shell--wide (full-bleed, pipe-only) and cron-builder's min-h-screen removed — every tool route now shares the identical max-w-7xl shell + panel. |
| 2026-08-26 | AA contrast pass on shared tokens | Browser audit (test:ui-audit) measured: trust pill / .badge-info cyan-on-tint 3.39:1 → text info-700/info-300; --muted-foreground 47%→44% (was 4.49:1 on --background, 4.30:1 on --muted); dark --primary 66%→64% (white button text was 4.29:1); ~90 text-primary-600 route usages gained missing dark:text-primary-400. Also: tool-group never carries shadow-*; button decoration beyond the variant removed (mock-data CTA); accent-primary-600 is the checkbox accent everywhere; bandwidth unit toggle moved onto .tabs-list/.tab-trigger. |
| 2026-08-24 | Nested groups are delineation, not elevation | --card is #fff/#12141c — the same colour as .tool-page-panel — so the pre-rebuild nested card was a card inside an identical card. bg-muted would invert elevation between themes against .input (bg-background). .tool-group therefore adds border+radius+padding and no fill; --flush for child-padded containers, --inset for output-only regions, bg-popover for surfaces that leave the panel's plane. 104 group edits + 27 form controls to .input; guarded by src/ui/card-contract.test.js. |
| 2026-09-09 | Home is a product catalog, not a poster | Hero left-aligns to the same max-w-7xl column as the grid. Cards use Material Symbols tiles (src/ui/home-icons.js) instead of registry emoji. Flagships are chips; trust is a middot line. Editorial stays below the catalog. |