One vocabulary, two contexts · numbers in mono · show the math · cost models named & colored · don't invent mechanisms · informational blue accent (never status)
One vocabulary, two contextsAtoms, type, spacing, principles shared. Palettes split by context. Track colors carry semantic across both.
Numbers in monoJetBrains Mono = readouts & labels. Inter = titles & prose.
Show the mathCalcs render as visible arithmetic, not a single number.
Cost models named, typed, coloredEvery tour has exactly one; the colored pill anchors it.
Don't invent business mechanismsUI visualizes the data model. New field needed? Extend canonical model.
Informational blue for accent · never statusSelected/active states use --track-content (#4f7cac). Status colors stay reserved for state (healthy / warn / critical / refund).
Export prompt · canonical source · paste into another LLM
What's in itAll canonical CSS variables (Business + Backend palettes, semantic, tracks, cost-model, accents). Type ramp. Scoping vocabulary (3 axes). 6-app roster with primary tracks. Strict page structure (topbar / tool switcher / sidebar chrome / page head) with full HTML examples + rules. Within-screen atoms list. Entity names + don't-invent rules. 6 principles. 5 anti-patterns.
How to useCopy → paste as system prompt → ask the LLM to build screens. It will use canonical class names, follow page structure rules, respect principles + anti-patterns.
Tokens · 41 · 8 groups
surfaces · borders · text · semantic · accents · cost-model · warm · tracks · flip to Detail to browse + copy individually
surfaces 5 · navy
--bg
#0c1521
--elev
#142033
--elev-2
#1a2942
--card
#16223a
--card-hi
#1d2c47
borders 3 · navy
--border
#243246
--border-soft
#1d2a3c
--border-hi
#324562
text 4 · navy
--text
#e7eef9
--text-mid
#95a4bb
--text-dim
#5e7090
--text-faint
#3e526e
semantic 5 · state colors
--healthy
#46d28a
--warn
#f5b95a
--critical
#f47272
--info
#5fb4ff
--refund
#c98ee5
accents 3 · CTAs & chart
--accent-yellow
#f5c247
--accent-cyan
#5fd6cc
--accent-coral
#ff9a6a
cost-model pills 5 · brown family · cousins of --track-cost
Three axes that classify what a surface is for. Components carry these as tags; the Business tab uses them as filters. The Data Adaptor's scoping picker (built in-app from the atoms on the Backend tab →) is its user-facing realization.
Tracks · 5 concerns · same color, two forms
What aspect of the data. .tag.tag--track-* as passive label · .concern-pill.concern-pill--* as in-picker toggle (warm cream context). Both use the same 5 track colors.
As tag · passive label
revenuecostcapacitycontentdispatch
used on component cards · filter chips · tag rows
As concern pill · selectable
idle · selected (filled) · hover (thicker border) add .concern-pill--selected for active fill
How wide the lens is. Drives what the surface aggregates over.
season→voyage→tour day→departure→bus
Fleet view rolls up season · voyage. Operations live at tour day · departure. Dispatch lives at bus (the brief, data-intensive moment of execution).
Viewing context · 2 audiences
Who the surface is built for. Drives information density, vocabulary, and what's exposed.
guest experienceoperator detail
guest experience · what the guest paid for · tour name, time, price, simple state (Front Desk Portfolio, Folio line items) · operator detail · what the ops team needs to run the day · financials, vendor cost, manifests, dispatch (tty, Planner, Folio Resolver)
How this gets used
Every Business component carries data-tracks · data-levels · data-contexts · data-surfaces. The Business tab's filter row uses these to narrow what's shown. The Data Adaptor's in-app scoping picker is the user-facing realization of the same vocabulary — picking lifecycle level + concern tells the adaptor what to surface. The Backend tab catalogs its building blocks (lifecycle cards, concern pills, numbered step header, concern group).
.page-headTop of every Business page. h1 + sub + right-aligned actions. Actions: 0–4 buttons; last 1–2 may be primary.
.frame-cardTitled section wrapper. Used for Day at a glance, Day P&L, Tours today, anything that needs a labeled boundary.
.timeline-rowTime-axis + per-item rows with positioned bars. Active state via the .at-gate nested chip on the bar.
.tour-card2-col grid item. Time/duration/state on left, margin badge top-right, capacity bar at bottom. Drop in any Business surface that lists tours.
.page-metric-strip4-card grid with 1px-gap (border shows through). Smaller than .metric-card — page-level summary, not detail. Variant value colors: --healthy, --accent.
.walkup-barSegmented projection bar that nests inside a .page-metric. Two segments: .seg-pos green + .seg-warn red.
.date-nav / .date-linkPer-app sidebar date selector with left-border active accent (yellow). Used in Manager Dashboard for Today / Tomorrow / Yesterday / Overall.
.dev-source-blockSidebar-bottom dev affordance — Mockup/Test toggle, "view/export test data" link, warn-chip, user-chip. Hidden in production builds. The .ide-link (inline < > SVG next to "Data source") opens the IDE in a new tab — subtle (50% opacity), brightens on hover. Plain <a> with target="_blank" rel="noopener noreferrer".
Filter chip row · chips × dimensions · used at top of tab panes
Multi-dimensional filter chip row. Each chip carries data-filter-dim + data-filter-value. Sections being filtered carry data-{dim} with space-separated values. AND across dimensions · OR within. Untagged sections in a given dimension are permissive (always pass).
AnatomyGroup label + chips, repeated per dimension. Optional result count + clear button. One filter-row per tab pane.
Active statearia-pressed="true". Border + text adopt the dimension's accent color (track family carries its semantic).
Match logicAND across dimensions (cost AND tour-day). OR within (cost OR revenue). Untagged dimension on a section = permissive.
CountLive "showing X of Y" appears when any filter is active. Hidden when cleared.
PersistencePer-scope localStorage key (shorex-ds-filter-{scope}) so the filter sticks across sessions and tab switches.
A11yEach chip is a button with aria-pressed. Clear is a separate button. Keyboard nav follows the source order.
When to use
When a tab pane has more than ~5 sections AND those sections cleanly partition along the scoping vocabulary. Below that threshold, a flat ladder is fine — filters add cognitive overhead, only earn it when there's enough material. Realized today on the Business tab; ready to drop onto Backend when it grows past 3 sections.
Streamline | Detail toggle · per-card density axis · variant of .toggle-group
.toggle-group.level-group
streamline default · expert-first
[Card name]
essentials · always shown
rare info · hidden until Detail picked
[Card name]
essentials · always shown
rare info · visible in Detail
PurposeSecond density axis, separate from the collapse accordion. Streamline = at-a-glance essentials for an expert daily. Detail = useful-but-rare info.
Active accent--track-content (#4f7cac), informational blue. NEVER a status color.
Mount → injectOne template (levelToggleHTML()) so the toggle never drifts. Card carries data-level="streamline|detail"; CSS hides .detail-only when streamline.
AccessibilityContainer role="radiogroup" aria-label. Each button role="radio" aria-checked. Tooltip on :hover + :focus-visible. No native title=.
PersistencelocalStorage['shorex-level-<cardId>']. Distinct from collapse accordion's shorex-density-<cardId>.
IconsRect-bar motif. Streamline = 2 bars · Detail = 3 bars. More rows → more detail.
Variation worth knowing — minimal-by-health
In the Connections card, Streamline goes further than hiding .detail-only: a healthy feed collapses to a one-glance pill (dot · name · freshness · status), while a feed needing attention (attn) stays fully expanded. "Streamline" there means show me only what's wrong, not uniform trim. Per-card override of the default behavior — document explicitly when used.
✓ per-card · drop in via .level-mount · let JS inject the template✓ use across both Business and Backend cards✗ hand-write the two buttons (template drift)✗ color active with a status color · use only --track-content✗ conflate with collapse accordion (different storage key, different axis)
Guidelines
Use whenA card mixes expert-essentials with useful-but-rare info. Streamline hides the rare; Detail reveals it on demand.
Don't pair withSurfaces where every field is workflow-critical. If hiding any breaks the task, the toggle is wrong — restructure the card instead.
Distinct from collapseCollapse = "is the card open?" Density = "how much shows when it is?" Both can coexist on one card with separate persistence keys.
One template, many mountsDrop a single <span class="level-mount"> at the desired position. initCardLevel() injects buttons from levelToggleHTML() — never hand-write them.
Cross-contextSame markup in Business and Backend. The cascade adapts surfaces / borders / text; the active accent (--track-content) is globally locked.
NamingBase atom = .toggle-group. Density variant = .level-group. Future variants (period, sort, scale) get their own .foo-group modifier — don't pile semantics onto the base.
When the variation mattersIf "what's worth showing" depends on a card's data (e.g. health), document that override explicitly — see Connections callout. Otherwise the structural .detail-only hide is the default.
Cross-app exports · 6-app suite · the contract
Six apps share one shell. Every primitive listed here renders identically across the suite — that's what makes the apps feel like one product instead of six. The catalog is the contract.
The 6 apps · primary track per app
Track assignment is editorial — drives the dot color in the tool switcher and informs analytic groupings. Adjust as apps evolve.
Canonical data model17 entities · GO-DaniShorex adaptor → ↓
Drift is the enemy
If two apps render the topbar at different heights, or the tool switcher uses different dot colors, the suite stops feeling like one product. Audit every release. Any inline style that duplicates a token gets migrated, not tolerated — see Commitments · Drift.
Tool switcher · sidebar-bottom · 6-app list
Same component, both contexts. Active row replaces the arrow with "YOU'RE HERE"; others are external links to sibling apps.
Anatomy.tool-switcher wraps a uppercase mono .tool-switcher-label + a vertical .tool-switcher-list of .tool-link rows.
Each rowtrack-colored .tool-link-dot + app name + right-aligned arrow (or YOU'RE HERE).
Active state.tool-link.is-here — no hover bg, arrow replaced by mono YOU'RE HERE, dot gets a ring (own color over card bg).
Order is fixedSame 6, same order, every app. Order = roughly the cruise lifecycle (sales → resolution → dispatch → analysis → admin).
Dot = primary trackFrom the roster above. If an app's primary track changes, update once here; every app's switcher inherits.
A11yThe active row is not a link (no href); it's a <a> with aria-current="page". Other rows are external links → render with ↗ to signal off-app navigation.
Anti-pattern
Don't reorder per-app, don't hide the current app from its own list, don't swap the dot color based on state (e.g. red for "needs attention"). Status belongs on the destination app, not on the switcher row — keep the switcher boring and predictable.
Topbar · 72px multi-slot · canonical
Brand chip · labeled info slots · spacer · weather pill (cruise context) · live time. 72px tall. Built from .topbar-slot + .weather-pill + .live-time atoms.
Business · navy
M
SHOREX
[Cruise itinerary]
Ship
[Ship name]
Voyage
Day 4 of 8 · 2,650 pax
Port today
[Port name] 08:00–17:00
☀84°F · Sunny·NE 6 kt
9:04AM
WED · MAR 18
Height72px standard. Tall enough for label + value per slot.
Brand chip28×28 mark + 2-line label (app name caps + cruise-itinerary sub). Mark uses the app's accent color (Manager = yellow). Bordered right edge separates from the next slot.
Info slots0–4 labeled .topbar-slot blocks (ship / voyage / port / etc.). Each is label (mono caps, dim) + value. Right-bordered for visual rhythm. Actions don't live here — they live in the page header.
Weather + timeRight-aligned chrome. Weather pill is a cruise-app pattern (shipboard ops care about conditions). Live time shows current local time + date.
BordersBottom border + per-slot vertical borders, all --border. Background is --elev.
Per-app brand colorMark color is the app's accent — Manager yellow, future apps get their own. Don't reuse the same accent across two apps.
Cross-app contract
The topbar is the strongest signal of "this is the same suite" — every app uses the same height, slot pattern, brand chip placement, and right-chrome layout. Apps with no port/voyage context can drop those slots; the slot pattern remains. Actions live in the page header (h1 + right-aligned action group), never in the topbar.
Sidebar-bottom chrome · user chip · data source · sign-in (alt)
Lives at the bottom of every app's sidebar. Logged-in is the canonical state — user chip + (dev) data-source affordance. Logged-out shows the sign-in button instead.
User chip28×28 initials avatar + name + role (mono caps). Clicking opens the account popover (tool switcher + sign out + settings).
Data source toggleMockup / Test segmented control. Dev/staging affordance — hidden in production builds. Uses the base .toggle-group with tight (5×12 padding) sizing.
IDE shortcutInline .ide-link next to the "Data source" label — small < > SVG chevron at 50% opacity, brightens on hover. Real <a>, target="_blank" rel="noopener noreferrer". Subtle by design — devs find it when they need it, end users don't see it.
Warn chipInline state surfacing — dot + caps label on a tinted bg. Use for system-level conditions (missing data, stale sync) not user actions. See .warn-chip atom.
Sign-in (logged-out)Inverted fill — background: var(--text); color: var(--bg);. Only renders on the login screen. Once authenticated → user chip replaces it.
Help buttonOptional. Lives next to sign-in in the logged-out state; in logged-in, help lives in the account popover.
Tool switcher lives in the account popover
Per the v0.16 design, the sidebar-bottom is data-source + user chip — there's no real estate for the 6-app tool switcher. Clicking the user chip opens an account popover that contains the tool switcher + sign-out + settings. See Tool switcher for the list spec.
Entity names · 17 canonical · use these in copy & field names
When a label refers to one of these things, use the canonical name. Tour, not "excursion." Voyage, not "cruise." Vendor, not "operator" or "supplier." Designers don't need the full schema — owners, source-limited fields, mappings live in canonical-model.json for when engineers wire it up.
Any time a screen displays or labels a real-world thing — a tour, a vendor, a guest, a payment. Use the canonical name in copy AND in the data-* hooks on components. Inventing freeform field names (excursion_id, supplier, passenger) breaks the contract with the data layer. If a label feels like it needs a new entity, that's a flag — extend the canonical model, don't fork it.
Commitments · 10 · how this catalog stays sane
Tools, not pagesThe catalog houses primitives (atoms, patterns, page chrome). Pages live in apps, built from these. Don't paste rendered page demos here — extract the atoms, then delete the page.
Lift before forkSee a pattern in 2+ surfaces? Lift to a canonical class name immediately. No namespace prefixes (no .md-*, no .biz-*). Atoms scope by purpose, not by app.
Bg color tells contextApps don't know they're "Business" or "Backend" — those are designer mental models. Navy :root vs warm .theme-warm IS the signal. Don't add explicit context labels in app UI.
Streamline by defaultThe catalog defaults to streamline mode for experts. Mark explainer content .detail-only — anatomy callouts, when-to-use, anti-pattern callouts, spec dense-lists. Renders + tag rows + class names always stay visible.
Catalog + prompt update togetherThe spec lives once as <pre id="ds-prompt-source"> in the catalog; prompt.html mirrors it for standalone export. When the design system changes, update both.
DriftAudit surfaces each release. Inline styles that duplicate tokens get migrated, not tolerated. Use var() tokens, not literal hexes.
PromotionA pattern earns "component" status after 3+ uses in 2+ surfaces. Until then it stays a pattern — no abstract API, no public class name commitment.
A11yFocus rings + contrast verification when a component reaches v1.0. Don't ship visual without keyboard. role + aria-* on interactive groups (toggle-group, filter chips, concern pills).
IntentEach component states why it exists, not just what it is. Why is in the description + when-to-use callout. No intent → rejected.
Anti-patterns · 4
Plain text cost-model captionUse .pill.pill--cm-flat-pax, not COST MODEL · FLAT PER-PAX caption.
Bucket as per-seat dead weightvehicle_flat = flat per vehicle. Empty seats = unsold capacity, not cost. Phantom pax lives on PER-PAX-W-MIN.
Invented business mechanicsNo fictional wallets / flows / tour names. Use [Tour name] placeholders + structural language.
Context confusionNo dark-navy Data Adaptor. No warm-beige Planner. Visual context is part of the affordance.
Business
TrackLevel
ViewSurface
Ways cost can show · 7
costoperator detail
01 Calc line show the math, scales across all cost models
✓ after cost-model pill in head row✗ categorical (use cost-model pill) · new state colors
AT GATE is an operational state (time-bound, day-of-execution) — distinct from the lifecycle states above. Future operational states (PRE-BOARDING, BOARDED, DEPARTED, RETURNED) will join the same family using --accent-cyan variants.
Per-tour row · 4 states
revenuecostcapacitytour dayoperator detailPlanner
① HEALTHY · ABOVE MIN, CONFIRMED
TIERED / PAX[Tour name][Vendor]CONFIRMED
1-15 @ $38 · 16-30 @ $32 · 31-∞ @ $28
36 pax → tiered rate = $1,218
SELL$75/paxREV$2,700GROSS$1,482MARGIN55%FILL36/40
② AT-RISK · BELOW MIN GUARANTEE
PER-PAX-W-MIN[Tour name][Vendor]BELOW MIN
$40/pax · min 20 pax
max(14, 20) × $40 = $800
SELL$65/paxREV$910GROSS$110MARGIN12%FILL14/40 · below min 20
③ PENDING VENDOR · NOT YET CONFIRMED
PER-VEHICLE[Tour name][Vendor]PENDING VENDOR
$1,100 per vehicle · 20 cap
2 vehicles × $1,100 = $2,200
SELL$99/paxREV$4,356GROSS$2,156MARGIN50%FILL28/40
④ CANCELLED
FLAT / PAX[Tour name][Vendor]CANCELLED
$34/pax
0 pax × $34 = $0
SELL$75/paxREV$0GROSS$0FILL—
Planner port-day view
Tour Departure Financials — operational · tty shipboard modal
Tour cards, manifest entries, vendor cards, adaptor feeds — anywhere a card mixes always-relevant essentials with sometimes-needed depth. Same template, same color rules (active = --track-content), same persistence key namespace (shorex-level-<cardId>).
Backend atoms
Atoms used to build the Data Adaptor's scoping picker. The assembled picker lives in-app; these are the deconstructed pieces it composes from.