From b3a6843d5b413383b70252aa46dab97c8a80e0a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 13:53:09 +0000 Subject: [PATCH 01/18] Add design system codifying the house style Distils a 32-template reference set of corporate presentation and web designs into a reusable system, so websites, PowerPoint templates and commissioned work all come back in the same format. Contents: - DESIGN-BRIEF.md, the spec to hand a designer or an AI tool: colour and type rules, shape language, imagery and chart treatment, the standard 14-step deck sequence, an acceptance checklist, and an explicit list of what the style is not. - tokens/ as JSON and CSS custom properties. Six accent themes (amber, electric, navy, teal, green, coral) swap from a single class; each carries an --accent-on value that clears 4.5:1 on its accent. - css/ split into base (reset and type primitives), components (cards, stats, steps, timelines, SWOT, buttons, media treatments, ornament) and slides (16:9 surfaces and slide archetypes). - powerpoint/SPEC.md with the theme colour slot mapping, the type scale in points with pre-computed tracking, corner radii as PowerPoint adj values, and the 14 slide-master layouts. - examples/ with a web landing page and a 14-slide deck, both composed from system classes only. No build step, no dependencies, no network requests; open either file in a browser. Slide internals restate their spacing in container query units rather than the rem-based space tokens, so a slide composes identically as a 460px thumbnail and as a full-screen projection. Ornaments sit on a negative z-index layer inside an isolated stacking context, which keeps them behind content without forcing a position on the slide furniture. The offset accent slab is held inside its own box so it survives clipping ancestors. Verified by rendering both examples in Chromium: no page errors, no console errors, no content overflowing any of the 14 slides, and all six themes applying cleanly. --- README.md | 18 + design-system/DESIGN-BRIEF.md | 262 ++++++++++++++ design-system/README.md | 188 ++++++++++ design-system/css/base.css | 182 ++++++++++ design-system/css/components.css | 578 ++++++++++++++++++++++++++++++ design-system/css/slides.css | 323 +++++++++++++++++ design-system/examples/deck.html | 420 ++++++++++++++++++++++ design-system/examples/index.html | 291 +++++++++++++++ design-system/powerpoint/SPEC.md | 195 ++++++++++ design-system/tokens/tokens.css | 204 +++++++++++ design-system/tokens/tokens.json | 207 +++++++++++ 11 files changed, 2868 insertions(+) create mode 100644 design-system/DESIGN-BRIEF.md create mode 100644 design-system/README.md create mode 100644 design-system/css/base.css create mode 100644 design-system/css/components.css create mode 100644 design-system/css/slides.css create mode 100644 design-system/examples/deck.html create mode 100644 design-system/examples/index.html create mode 100644 design-system/powerpoint/SPEC.md create mode 100644 design-system/tokens/tokens.css create mode 100644 design-system/tokens/tokens.json diff --git a/README.md b/README.md index 05b7fc9..0b05976 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,24 @@ committing, and opening a pull request. This change was made from a remote Claude Code session to confirm that kicking off work from outside the terminal (web/mobile) works end to end. +## Design system + +`design-system/` holds my house style, codified so a website, a PowerPoint +template, or anything I commission comes back in the same format. + +- [`design-system/DESIGN-BRIEF.md`](design-system/DESIGN-BRIEF.md) — the spec to + hand a designer or an AI tool +- [`design-system/tokens/`](design-system/tokens) — colour, type, space and + radius tokens as JSON and CSS custom properties +- [`design-system/css/`](design-system/css) — the component and slide layer +- [`design-system/powerpoint/SPEC.md`](design-system/powerpoint/SPEC.md) — point + sizes and inch positions for building a `.potx` +- [`design-system/examples/`](design-system/examples) — a web landing page and a + 14-slide deck, both built from the system; open either in a browser + +Six accent themes ship with it (amber, electric, navy, teal, green, coral) and +swap with a single class on ``. + ## Agent A minimal Claude Agent SDK (TypeScript) starter lives in `agent.ts`. diff --git a/design-system/DESIGN-BRIEF.md b/design-system/DESIGN-BRIEF.md new file mode 100644 index 0000000..7cc1274 --- /dev/null +++ b/design-system/DESIGN-BRIEF.md @@ -0,0 +1,262 @@ +# Design Brief — House Style + +**Hand this document to any designer, agency, or AI tool.** It defines the format +to keep. Anything not specified here is open to interpretation; anything +specified here is not. + +This brief was distilled from a 32-template reference set of corporate +presentation and web designs. It describes what those designs have in common — +the rules that make them read as one house style rather than 32 unrelated files. + +--- + +## 1. The one-sentence version + +> Clean corporate layouts on white, driven by **a single saturated accent colour** +> and **one dark anchor**, with heavy geometric headline type, generous rounded +> corners, oversized section numerals, and soft geometric ornament in the corners. + +If a deliverable does not read that way at a glance, it is off-brief. + +--- + +## 2. Colour + +### The rule: one accent, one anchor, lots of white + +Every document commits to **exactly one accent hue**. That hue does all the +emphasis work — buttons, icon chips, section panels, the first chart series, the +highlighted word in a headline. A second accent hue is never introduced. + +| Role | What it does | Share of the page | +|---|---|---| +| **Accent** | Every point of emphasis. The brand signal. | 10–20% | +| **Ink** (near-black / deep navy) | Headlines, dark-fill slides, gravity. | 15–25% | +| **White / off-white** | The default ground. Space to breathe. | 55–70% | +| **Grey** | Body copy, hairlines, receded chart series. | remainder | + +### Approved accent families + +Pick one per document. All six are equally house-correct. + +| Theme | Signature | Notes | +|---|---|---| +| **Amber** | `#F5B21A` | The signature default. Warmest, most-used. Dark text on top. | +| **Electric** | `#2B50EE` | Tech, SaaS, product. White text on top. | +| **Navy** | `#2B4A8B` | Finance, consulting, formal. White text on top. | +| **Teal** | `#22BCCE` | Product, startup, healthcare. Dark text on top. | +| **Green** | `#17A673` | Sustainability, wellness. White text on top. | +| **Coral** | `#E85D33` | Marketing, creative, retail. White text on top. | + +Full ramps (50 → 700) live in `tokens/tokens.json`. + +### Ink and surfaces + +- Ink: `#14181F` (near-black) or `#1B2333` / `#233049` for a navy-leaning anchor. +- Ground: pure `#FFFFFF`, or `#F7F8FA` / `#FAF6EC` (cream) for a softer document. +- Never grey-on-grey. Body copy sits at `#3A4761`, not lighter. + +### Hard rules + +- **Never** put two accent families in one document. +- **Never** use a gradient as a brand colour. Gradients are permitted only as a + photo scrim or a subtle decorative blob. +- Accent-on-accent text must clear 4.5:1 — use `--accent-on` from the tokens, + which is already set correctly per theme. + +--- + +## 3. Typography + +### The rule: one family, heavy display over quiet body + +**One geometric-grotesque family** across the whole document. Poppins or +Montserrat for display; Inter for long body copy is an acceptable pairing. Never +more than two families total, never a serif, never a script. + +| Level | Weight | Tracking | Use | +|---|---|---|---| +| Display | 800 | −0.03em | Cover titles only | +| H1 | 800 | −0.025em | Section and slide titles | +| H2 | 700 | −0.02em | Sub-heads | +| H3 | 700 | −0.01em | Card titles | +| H4 | 600 | 0 | Feature labels | +| Body | 400 | 0 | Paragraphs, 1.65 line-height | +| Small | 400 | 0 | Card copy, captions | +| **Eyebrow** | 600 | **+0.18em, UPPERCASE** | The label above a headline | +| Numeral | 800 | −0.04em | Oversized `01` `02` `03` | + +### Signature moves + +1. **The eyebrow.** A short uppercase, wide-tracked, accent-coloured label sits + above nearly every headline. This is the single most recognisable tic of the + style. Use it constantly. + +2. **The two-tone headline.** One word in the headline flips to the accent + colour, or from regular to black weight: + `Doing Everything **Right**` · `Business **Report**` · `Vision & **Mission**` + +3. **The light/heavy pair.** Cover titles stack a thin line over a black line: + `CORPORATE` (regular) over `PRESENTATION` (black). + +4. **Tabular numerals** everywhere figures appear — stats, tables, timelines. + +### Hard rules + +- Headlines are **tight** (line-height ≤ 1.12) and **negatively tracked**. +- Body is **roomy** (line-height 1.6–1.65) and never tracked. +- Body copy never exceeds ~62 characters per line. +- Never centre a paragraph longer than two lines. + +--- + +## 4. Layout & space + +- **4px base grid.** Every measurement lands on it. +- **12-column** structure for web; slides use a **6.5% safe margin** on all sides. +- The workhorse layout is the **asymmetric split** — roughly 5:7 or 7:5, headline + block on one side, content or media on the other. Rarely 50:50. +- Content sits in **cards** with 20px radius and a 32px inner pad. +- White space is a feature, not waste. When a slide feels crowded, cut content — + do not shrink the type. + +--- + +## 5. Shape language + +**Generous rounding is a signature.** Sharp corners are the exception, reserved +for full-bleed colour blocks. + +| Radius | Use | +|---|---| +| 6px | Inputs, small chips | +| 12px | Icon chips, buttons (square variant) | +| 20px | **Cards — the default** | +| 32px | Media panels, large feature blocks | +| 48px | Hero media | +| pill | Buttons, tags, avatars, meter bars | + +Recurring shape motifs, all of them ornamental and `aria-hidden`: + +- **Circles and quarter-circles** bleeding off a corner +- **Organic blobs** in accent-50 or accent-100 behind content +- **Dotted matrices** tucked into slide corners +- **Offset blocks** — a solid accent rectangle peeking out from behind a card or + photo (the "stacked paper" look) +- **The notched panel** — rounded on three corners, square on one + +--- + +## 6. Imagery + +- **Real photography** of people at work, cities, and product. No cartoon + clip-art in business decks. Flat vector illustration is acceptable only for + education and children's material. +- Photos are **masked**: rounded rectangles (32px), circles for people, or the + notched panel. Never a bare square photo dropped on a slide. +- Two permitted colour treatments: + - **Accent wash** — accent multiplied over the photo at ~55% for section covers + - **Bottom scrim** — dark gradient bottom-up when type sits over the image +- People photos are cropped to torso-up, eyes on the upper third. +- Icons are **line or solid, single weight, one style throughout**, placed in an + accent-filled chip. Never mix icon styles. + +--- + +## 7. Data & charts + +- **Series 1 is always the accent.** Everything after it recedes into greys, so + one bar or line reads as "the point". +- Gridlines are `#ECEFF3` hairlines. No chart borders, no 3D, no drop shadows. +- Label data directly on the marks where possible; a legend is a fallback. +- Big figures get the **stat treatment**: accent-coloured value at 3× the label + size, uppercase tracked label beneath. +- Percentage rings, meter bars and progress tracks all use the accent at full + saturation on an `--ink-100` track. + +--- + +## 8. Standard deck sequence + +A complete presentation follows this order. Drop sections that do not apply; +never reorder them. + +1. **Cover** — title, subtitle, presenter, date, logo +2. **Contents / agenda** — numbered, two columns +3. **Section divider** — oversized numeral + section title on an accent panel +4. **About / who we are** — split layout, photo + copy +5. **Vision & mission** — two-up cards +6. **Services / what we do** — 3 or 4 icon cards +7. **Process / how it works** — numbered steps `01 02 03` +8. **Team** — circular avatars, name + role +9. **Numbers** — stat row on a dark or accent slide +10. **Timeline / roadmap** — horizontal milestone track +11. **SWOT** — 2×2, strengths on accent, weaknesses on ink +12. **Charts / financials** — accent + grey series +13. **Testimonials** — quote mark, avatar, name + company +14. **Pricing / comparison table** — if applicable +15. **Contact / thank you** — closing, details, socials + +Section dividers reset the numeral count. Every non-cover slide carries logo +top-left, section kicker top-right, page number bottom-right. + +--- + +## 9. Deliverable checklist + +Before accepting work, verify: + +- [ ] Exactly one accent hue used throughout +- [ ] White or off-white is the dominant ground (>55%) +- [ ] Eyebrow labels present above headlines +- [ ] At least one two-tone headline on the cover +- [ ] Headline tracking is negative, body tracking is zero +- [ ] Card radius is consistent at 20px (or 32px for media) +- [ ] All photos are masked, none are bare rectangles +- [ ] Chart series 1 is the accent; the rest are grey +- [ ] Slide furniture (logo / kicker / page number) on every content slide +- [ ] Section numerals are oversized and present on dividers +- [ ] Nothing crosses the 6.5% safe margin +- [ ] Body copy ≤ 62 characters per line +- [ ] Accent-on-accent text passes 4.5:1 contrast + +--- + +## 10. What this style is not + +Stating the negative space saves rounds of revision: + +- Not brutalist, not neumorphic, not glassmorphic +- Not dark-mode-first — the ground is light +- Not maximalist; no texture, no noise, no pattern fills +- No drop shadows harder than `0 12px 32px rgba(20,24,31,0.09)` +- No outlined display type, no letterpress, no bevels +- No stock illustration of the "corporate Memphis" blob-people variety in + business material +- No more than one accent hue, ever + +--- + +## 11. Machine-readable assets + +| File | What it is | +|---|---| +| `tokens/tokens.json` | Platform-agnostic tokens — import into Figma, Style Dictionary, or a build | +| `tokens/tokens.css` | CSS custom properties + the six accent themes | +| `css/base.css` | Reset and typographic primitives | +| `css/components.css` | Cards, stats, steps, timelines, SWOT, buttons, media | +| `css/slides.css` | 16:9 slide surfaces and slide archetypes | +| `powerpoint/SPEC.md` | Point sizes and inch positions for a 13.333″ × 7.5″ slide | +| `examples/index.html` | Web landing page built from the system | +| `examples/deck.html` | Full slide deck built from the system | + +### Prompt to hand an AI tool + +> Build this using the Corporate Deck Design System. Single accent hue +> (`{amber|electric|navy|teal|green|coral}`), near-black ink anchor, white ground. +> Poppins display / Inter body. Uppercase tracked eyebrow labels above headlines, +> one two-tone headline on the cover, 20px card radius, oversized section +> numerals, accent-filled icon chips, masked photography, charts with the accent +> as series 1 and greys after. Follow the standard deck sequence. Import +> `tokens/tokens.css` and use the existing component classes rather than writing +> new CSS. diff --git a/design-system/README.md b/design-system/README.md new file mode 100644 index 0000000..d97d1d1 --- /dev/null +++ b/design-system/README.md @@ -0,0 +1,188 @@ +# Corporate Deck Design System + +A codified version of my house style, so that a website, a PowerPoint template, +or anything I commission from a designer comes back looking like it belongs to +the same family. + +Distilled from a 32-template reference set of corporate presentation and web +designs. The system captures what those files have in common — one saturated +accent hue, a near-black anchor, heavy geometric headline type, generous rounded +corners, oversized section numerals, and soft geometric ornament. + +--- + +## What's here + +``` +design-system/ +├── DESIGN-BRIEF.md ← hand this to any designer or AI tool +├── tokens/ +│ ├── tokens.json ← platform-agnostic tokens (Figma, Style Dictionary, builds) +│ └── tokens.css ← CSS custom properties + the six accent themes +├── css/ +│ ├── base.css ← reset + typographic primitives +│ ├── components.css ← cards, stats, steps, timelines, SWOT, buttons, media +│ └── slides.css ← 16:9 slide surfaces and slide archetypes +├── powerpoint/ +│ └── SPEC.md ← pt sizes and inch positions for a 13.333" × 7.5" slide +└── examples/ + ├── index.html ← web landing page built from the system + └── deck.html ← 14-slide deck built from the system +``` + +Open either example file directly in a browser — no build step, no dependencies, +no network requests. + +--- + +## Using it on the web + +```html + + + + + + + + +``` + +Then compose from the existing classes: + +```html +
+
+
+ What we do +

Four practices, one rhythm

+
+
+
+
+ +

Planning

+

Plans that survive the calendar.

+
+ +
+
+
+``` + +### Re-theming + +Six accent families ship with the system. Swap the class on `` — or on any +container for a scoped change — and everything downstream follows, including +charts, buttons, icon chips and section panels. + +```html + +``` + +Both example files include a live theme switcher so you can see this happen. + +### Dark blocks + +Add `on-dark` alongside a dark surface and the foreground roles flip. Components +need no dark-specific variants of their own. + +```html +
+``` + +--- + +## Building slides + +A `.slide` is a self-contained 16:9 canvas. Everything inside sizes in container +query units, so a slide is identical full-screen or as a thumbnail in a grid. + +```html +
+ +
Who we are
+
04
+
+
+
+ About us +

Let's talk about who we are

+
+

+
+
…
+
+
+
+``` + +Printing `examples/deck.html` gives one slide per landscape page. + +Slide archetypes provided: cover (split / bleed), agenda, section divider, split +with media, card grids, numbered steps, stat row, timeline, SWOT, chart, team, +testimonial, closing. + +--- + +## Building a PowerPoint template + +`powerpoint/SPEC.md` has everything needed to build a `.potx`: + +- theme colour slot mapping for all six accent families +- the type scale in points, with pre-computed tracking values +- corner radii as PowerPoint `adj` values +- the single approved shadow preset +- the 14 slide-master layouts and where the furniture sits +- chart formatting rules + +--- + +## The rules, in brief + +The full brief is in [`DESIGN-BRIEF.md`](DESIGN-BRIEF.md). The short version: + +- **One accent hue per document.** Never two. It carries every point of emphasis. +- **White or off-white dominates** — 55–70% of the page. +- **Uppercase tracked eyebrow labels** above headlines. The most recognisable tic. +- **Two-tone headlines** — one word flips to the accent, or from regular to black. +- **Headlines tight and negatively tracked; body roomy and untracked.** +- **20px card radius**, 32px on media. Sharp corners only on full-bleed colour. +- **Photos are always masked** — rounded, circular, or notched. Never bare. +- **Chart series 1 is the accent**, everything after it is grey. +- **Oversized numerals** on section dividers and process steps. + +And the negatives, which save revision rounds: not brutalist, not neumorphic, not +glassmorphic, not dark-mode-first, no texture or noise, no gradients as brand +colour, no hard shadows, no mixed icon styles. + +--- + +## Handing this to someone + +For a human designer: send `DESIGN-BRIEF.md`, plus `powerpoint/SPEC.md` if the +deliverable is a deck template. The checklist in §9 of the brief is the +acceptance criteria. + +For an AI tool, the prompt at the end of the brief works as-is: + +> Build this using the Corporate Deck Design System. Single accent hue +> (`{amber|electric|navy|teal|green|coral}`), near-black ink anchor, white ground. +> Poppins display / Inter body. Uppercase tracked eyebrow labels above headlines, +> one two-tone headline on the cover, 20px card radius, oversized section +> numerals, accent-filled icon chips, masked photography, charts with the accent +> as series 1 and greys after. Follow the standard deck sequence. Import +> `tokens/tokens.css` and use the existing component classes rather than writing +> new CSS. + +--- + +## Notes + +- **Fonts.** Poppins and Inter are the intended families; the stacks in + `tokens.css` fall back through Montserrat and Segoe UI to system sans, so + nothing breaks if they are unavailable. Load the real fonts in production. +- **Accessibility.** `--accent-on` is set per theme to the text colour that + clears 4.5:1 on that accent. Use it rather than hard-coding white or black. + Decorative geometry is `aria-hidden` throughout. +- **No dependencies.** Plain CSS, no build step, no framework, no network calls. diff --git a/design-system/css/base.css b/design-system/css/base.css new file mode 100644 index 0000000..5e37ae7 --- /dev/null +++ b/design-system/css/base.css @@ -0,0 +1,182 @@ +/* ========================================================================== + Corporate Deck Design System — Base + Reset, document defaults, and the typographic primitives. + Requires: tokens.css + ========================================================================== */ + +*, *::before, *::after { box-sizing: border-box; } + +html { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + margin: 0; + background: var(--surface-page); + color: var(--fg-body); + font-family: var(--font-body); + font-size: var(--fs-body); + line-height: var(--lh-loose); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +img, svg, video, canvas { display: block; max-width: 100%; height: auto; } + +h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; } +ul, ol { margin: 0; padding: 0; } + +a { color: var(--accent-600); text-decoration-thickness: 1px; text-underline-offset: 2px; } +a:hover { color: var(--accent-700); } + +:focus-visible { + outline: 3px solid var(--accent-500); + outline-offset: 2px; + border-radius: var(--radius-sm); +} + +/* -------------------------------------------------------------------------- + Type primitives + The house voice: heavy, tight display type over roomy, quiet body copy. + -------------------------------------------------------------------------- */ + +.display, +.h1, .h2, .h3, .h4 { + font-family: var(--font-display); + color: var(--fg-strong); + text-wrap: balance; +} + +.display { + font-size: clamp(2.75rem, 6vw, var(--fs-display)); + font-weight: var(--fw-black); + line-height: 1.02; + letter-spacing: var(--tracking-display); +} + +.h1 { + font-size: clamp(2.25rem, 4.5vw, var(--fs-h1)); + font-weight: var(--fw-black); + line-height: var(--lh-tight); + letter-spacing: -0.025em; +} + +.h2 { + font-size: clamp(1.75rem, 3.2vw, var(--fs-h2)); + font-weight: var(--fw-bold); + line-height: 1.12; + letter-spacing: var(--tracking-tight); +} + +.h3 { + font-size: var(--fs-h3); + font-weight: var(--fw-bold); + line-height: var(--lh-snug); + letter-spacing: -0.01em; +} + +.h4 { + font-size: var(--fs-h4); + font-weight: var(--fw-semibold); + line-height: 1.3; +} + +.body-lg { font-size: 1.125rem; line-height: var(--lh-loose); } +.body { font-size: var(--fs-body); line-height: var(--lh-loose); } +.small { font-size: var(--fs-small); line-height: 1.6; } +.caption { font-size: var(--fs-caption); line-height: 1.5; color: var(--fg-muted); font-weight: var(--fw-medium); } + +/* The small tracked-out label that sits above nearly every headline. + This is the single most recognisable tic of the system — use it often. */ +.eyebrow { + display: inline-block; + font-family: var(--font-display); + font-size: var(--fs-eyebrow); + font-weight: var(--fw-semibold); + letter-spacing: var(--tracking-eyebrow); + text-transform: uppercase; + color: var(--accent-600); + line-height: var(--lh-snug); +} + +.eyebrow--muted { color: var(--fg-muted); } + +/* Two-tone headline. Wrap the emphasised word: "Doing Everything Right" */ +.h1 em, .h2 em, .display em { + font-style: normal; + color: var(--accent-600); +} + +/* The light/heavy headline pair seen on most cover slides: + a thin first line, a black second line. */ +.headline-pair { display: block; } +.headline-pair__light { display: block; font-weight: var(--fw-regular); } +.headline-pair__heavy { display: block; font-weight: var(--fw-black); } + +/* Oversized ghost numeral used behind section dividers and stat blocks. */ +.numeral { + font-family: var(--font-display); + font-size: var(--fs-numeral); + font-weight: var(--fw-black); + line-height: 0.9; + letter-spacing: -0.04em; + color: var(--fg-strong); + font-variant-numeric: tabular-nums; +} + +.numeral--ghost { color: var(--ink-100); } +.numeral--accent { color: var(--accent-500); } + +/* The short accent rule that separates an eyebrow or headline from its body. */ +.rule { + width: 56px; + height: 4px; + border: 0; + background: var(--accent-500); + border-radius: var(--radius-pill); + margin: 0; +} + +.rule--center { margin-inline: auto; } +.rule--long { width: 100%; height: 1px; background: var(--ink-200); border-radius: 0; } + +/* -------------------------------------------------------------------------- + Utilities + -------------------------------------------------------------------------- */ + +.stack > * + * { margin-top: var(--flow, var(--space-4)); } +.stack-2 { --flow: var(--space-2); } +.stack-3 { --flow: var(--space-3); } +.stack-5 { --flow: var(--space-5); } +.stack-6 { --flow: var(--space-6); } + +.container { + width: 100%; + max-width: var(--container-max); + margin-inline: auto; + padding-inline: var(--space-5); +} + +.section { padding-block: var(--section-py); } +.section--muted { background: var(--surface-muted); } +.section--cream { background: var(--surface-cream); } +.section--accent { background: var(--accent-500); color: var(--accent-on); } +.section--dark { background: var(--ink-900); } + +.text-center { text-align: center; } +.text-accent { color: var(--accent-600); } +.text-strong { color: var(--fg-strong); } +.text-muted { color: var(--fg-muted); } + +.measure { max-width: 62ch; } +.measure-sm { max-width: 46ch; } + +.visually-hidden { + position: absolute; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/design-system/css/components.css b/design-system/css/components.css new file mode 100644 index 0000000..a4109fc --- /dev/null +++ b/design-system/css/components.css @@ -0,0 +1,578 @@ +/* ========================================================================== + Corporate Deck Design System — Components + The recurring furniture: cards, stats, steps, timelines, buttons, tables, + photo treatments and the decorative geometry. + Requires: tokens.css, base.css + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Grid + -------------------------------------------------------------------------- */ + +.grid { + display: grid; + gap: var(--space-5); + grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); +} +.grid--2 { --cols: 2; } +.grid--3 { --cols: 3; } +.grid--4 { --cols: 4; } +.grid--gap-lg { gap: var(--space-7); } + +/* Asymmetric split: the workhorse layout for "headline left, content right". */ +.split { + display: grid; + gap: var(--space-8); + grid-template-columns: minmax(0, var(--split, 5fr)) minmax(0, 7fr); + align-items: center; +} +.split--even { --split: 6fr; } +.split--wide-l { --split: 7fr; } + +@media (max-width: 900px) { + .grid { grid-template-columns: 1fr; } + .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; } + .split { grid-template-columns: 1fr; gap: var(--space-6); } +} + +/* -------------------------------------------------------------------------- + Card — the base container for almost every content block + -------------------------------------------------------------------------- */ + +.card { + background: var(--surface-page); + border: 1px solid var(--ink-100); + border-radius: var(--radius-lg); + padding: var(--space-6); + box-shadow: var(--shadow-sm); + transition: box-shadow var(--duration-base) var(--ease-standard), + transform var(--duration-base) var(--ease-standard); +} + +.card--raised { box-shadow: var(--shadow-md); border-color: transparent; } +.card--flat { box-shadow: none; background: var(--surface-muted); border-color: transparent; } +.card--accent { background: var(--accent-500); border-color: transparent; color: var(--accent-on); } +.card--dark { background: var(--ink-900); border-color: transparent; } +.card--interactive:hover { box-shadow: var(--shadow-lg); transform: translateY(-3px); } + +.card--accent .card__title, +.card--accent .h3 { color: var(--accent-on); } +.card--accent .card__body { color: color-mix(in srgb, var(--accent-on) 78%, transparent); } + +/* Left accent bar — used to mark the "featured" item in a row of three. */ +.card--barred { + border-left: 4px solid var(--accent-500); + border-radius: var(--radius-sm) var(--radius-lg) var(--radius-lg) var(--radius-sm); +} + +/* Top accent bar — the alternative emphasis treatment. */ +.card--capped { border-top: 4px solid var(--accent-500); } + +.card__title { font-family: var(--font-display); font-size: var(--fs-h4); font-weight: var(--fw-bold); color: var(--fg-strong); } +.card__body { font-size: var(--fs-small); color: var(--fg-body); margin-top: var(--space-2); } + +/* -------------------------------------------------------------------------- + Icon chip — icon in a rounded square or circle, always accent-filled + -------------------------------------------------------------------------- */ + +.icon-chip { + display: inline-grid; + place-items: center; + width: 52px; + height: 52px; + border-radius: var(--radius-md); + background: var(--accent-500); + color: var(--accent-on); + flex: none; +} + +.icon-chip--round { border-radius: var(--radius-pill); } +.icon-chip--soft { background: var(--accent-50); color: var(--accent-700); } +.icon-chip--dark { background: var(--ink-900); color: #fff; } +.icon-chip--outline { + background: transparent; + border: 2px solid var(--accent-500); + color: var(--accent-600); +} +.icon-chip--sm { width: 38px; height: 38px; border-radius: var(--radius-sm); } +.icon-chip--lg { width: 72px; height: 72px; border-radius: var(--radius-lg); } + +.icon-chip svg { width: 45%; height: 45%; } + +/* Feature row: chip left, copy right. The most-used content atom in the set. */ +.feature { + display: flex; + gap: var(--space-4); + align-items: flex-start; +} +.feature__title { font-family: var(--font-display); font-weight: var(--fw-semibold); color: var(--fg-strong); } +.feature__body { font-size: var(--fs-small); color: var(--fg-body); margin-top: var(--space-1); } + +/* -------------------------------------------------------------------------- + Stat — big number over a quiet label + -------------------------------------------------------------------------- */ + +.stat { display: flex; flex-direction: column; gap: var(--space-1); } + +.stat__value { + font-family: var(--font-display); + font-size: clamp(2.25rem, 4vw, 3.25rem); + font-weight: var(--fw-black); + line-height: 1; + letter-spacing: -0.035em; + color: var(--accent-600); + font-variant-numeric: tabular-nums; +} + +.stat__label { + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fg-muted); +} + +.stat__note { font-size: var(--fs-small); color: var(--fg-body); } +.stat--ink .stat__value { color: var(--fg-strong); } + +.stat-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: var(--space-6); +} + +/* Divided stat strip — hairline rules between each figure. */ +.stat-row--divided > .stat + .stat { + border-left: 1px solid var(--ink-200); + padding-left: var(--space-6); +} +.on-dark .stat-row--divided > .stat + .stat { border-left-color: rgba(255,255,255,0.18); } +.on-dark .stat__value { color: var(--accent-300); } + +@media (max-width: 700px) { + .stat-row--divided > .stat + .stat { border-left: 0; padding-left: 0; } +} + +/* -------------------------------------------------------------------------- + Numbered steps — "01 / 02 / 03" process rows + -------------------------------------------------------------------------- */ + +.steps { display: grid; gap: var(--space-6); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); counter-reset: step; } + +.step { position: relative; padding-top: var(--space-6); } + +.step::before { + counter-increment: step; + content: "0" counter(step); + position: absolute; + top: 0; + left: 0; + font-family: var(--font-display); + font-size: 2.5rem; + font-weight: var(--fw-black); + line-height: 1; + letter-spacing: -0.04em; + color: var(--accent-100); +} + +.step__title { font-family: var(--font-display); font-weight: var(--fw-bold); color: var(--fg-strong); position: relative; } +.step__body { font-size: var(--fs-small); margin-top: var(--space-2); } + +/* Badge variant — the number sits in a filled chip instead of ghosting behind. */ +.step-badge { + display: inline-grid; + place-items: center; + min-width: 44px; + height: 44px; + padding-inline: var(--space-3); + border-radius: var(--radius-pill); + background: var(--accent-500); + color: var(--accent-on); + font-family: var(--font-display); + font-weight: var(--fw-bold); + font-variant-numeric: tabular-nums; +} + +/* -------------------------------------------------------------------------- + Timeline — horizontal milestone track + -------------------------------------------------------------------------- */ + +.timeline { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + gap: var(--space-5); + position: relative; + padding-top: var(--space-7); +} + +.timeline::before { + content: ""; + position: absolute; + top: 10px; + left: 0; + right: 0; + height: 2px; + background: var(--ink-200); +} + +.timeline__item { position: relative; } + +.timeline__item::before { + content: ""; + position: absolute; + top: calc(var(--space-7) * -1 + 4px); + left: 0; + width: 14px; + height: 14px; + border-radius: var(--radius-pill); + background: var(--accent-500); + box-shadow: 0 0 0 4px var(--surface-page); +} + +.timeline__date { + font-family: var(--font-display); + font-weight: var(--fw-bold); + color: var(--accent-600); + font-variant-numeric: tabular-nums; +} +.timeline__body { font-size: var(--fs-small); margin-top: var(--space-1); } + +@media (max-width: 800px) { + .timeline { grid-auto-flow: row; padding-top: 0; padding-left: var(--space-6); } + .timeline::before { top: 0; bottom: 0; left: 6px; right: auto; width: 2px; height: auto; } + .timeline__item::before { top: 4px; left: calc(var(--space-6) * -1); } +} + +/* -------------------------------------------------------------------------- + Buttons + -------------------------------------------------------------------------- */ + +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: 0.85rem 1.75rem; + border: 2px solid transparent; + border-radius: var(--radius-pill); + font-family: var(--font-display); + font-size: var(--fs-small); + font-weight: var(--fw-semibold); + letter-spacing: 0.01em; + text-decoration: none; + cursor: pointer; + transition: background var(--duration-base) var(--ease-standard), + color var(--duration-base) var(--ease-standard), + transform var(--duration-fast) var(--ease-standard); +} + +.btn:hover { transform: translateY(-1px); } +.btn:active { transform: translateY(0); } + +.btn--primary { background: var(--accent-500); color: var(--accent-on); } +.btn--primary:hover { background: var(--accent-600); color: var(--accent-on); } + +.btn--ink { background: var(--ink-900); color: #fff; } +.btn--ink:hover { background: var(--ink-800); color: #fff; } + +.btn--outline { border-color: var(--accent-500); color: var(--accent-600); background: transparent; } +.btn--outline:hover { background: var(--accent-500); color: var(--accent-on); } + +.btn--ghost { color: var(--fg-strong); background: transparent; padding-inline: var(--space-2); } +.btn--ghost:hover { color: var(--accent-600); } + +.btn--square { border-radius: var(--radius-md); } +.btn--lg { padding: 1.05rem 2.25rem; font-size: var(--fs-body); } + +/* Pill tag / chip label */ +.tag { + display: inline-block; + padding: 0.35rem 0.85rem; + border-radius: var(--radius-pill); + background: var(--accent-50); + color: var(--accent-700); + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); + letter-spacing: 0.04em; +} +.tag--solid { background: var(--accent-500); color: var(--accent-on); } +.tag--ink { background: var(--ink-900); color: #fff; } + +/* -------------------------------------------------------------------------- + Media — photo treatments + -------------------------------------------------------------------------- */ + +.media { + border-radius: var(--radius-lg); + overflow: hidden; + background: var(--surface-sunken); +} +.media img { width: 100%; height: 100%; object-fit: cover; } + +.media--xl { border-radius: var(--radius-xl); } +.media--pill { border-radius: var(--radius-pill); } +.media--blob { border-radius: var(--radius-blob); } +.media--square { border-radius: 0; } + +/* The rounded-rect-with-one-square-corner mask used on section covers. */ +.media--notched { border-radius: var(--radius-xl) var(--radius-xl) var(--radius-xl) 0; } + +.media--circle { border-radius: var(--radius-pill); aspect-ratio: 1; } +.media--4x3 { aspect-ratio: 4 / 3; } +.media--16x9 { aspect-ratio: 16 / 9; } +.media--3x4 { aspect-ratio: 3 / 4; } + +/* Accent duotone wash — turns any stock photo into brand furniture. */ +.media--wash { position: relative; } +.media--wash::after { + content: ""; + position: absolute; + inset: 0; + background: var(--accent-500); + mix-blend-mode: multiply; + opacity: 0.55; +} + +.media--dim { position: relative; } +.media--dim::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(20,24,31,0.15) 0%, rgba(20,24,31,0.72) 100%); +} + +/* Avatar */ +.avatar { + width: 96px; + height: 96px; + border-radius: var(--radius-pill); + object-fit: cover; + border: 4px solid var(--surface-page); + box-shadow: var(--shadow-md); +} +.avatar--sm { width: 48px; height: 48px; border-width: 2px; } +.avatar--ring { border-color: var(--accent-500); } + +/* -------------------------------------------------------------------------- + Decorative geometry + The blobs, quarter-circles and offset blocks that sit behind content. + All are aria-hidden ornaments — never put meaning in them. + -------------------------------------------------------------------------- */ + +/* Ornaments sit on a negative layer so they fall behind in-flow content + without needing a z-index on the content itself — which would otherwise + clobber the `position: absolute` on slide furniture. `.has-deco` isolates, + so a negative layer never escapes behind the container's own background. */ +.deco { position: absolute; pointer-events: none; z-index: -1; } + +.deco-circle { + border-radius: var(--radius-pill); + background: var(--accent-500); +} +.deco-circle--soft { background: var(--accent-50); } +.deco-circle--ink { background: var(--ink-100); } +.deco-circle--ring { background: transparent; border: 2px solid var(--accent-300); } + +/* Quarter-circle corner ornament. */ +.deco-quarter { + border-radius: 0 0 100% 0; + background: var(--accent-100); +} + +.deco-blob { + border-radius: var(--radius-blob); + background: var(--accent-100); +} + +/* Dotted matrix — the small grid of dots tucked into slide corners. */ +.deco-dots { + background-image: radial-gradient(var(--accent-300) 1.5px, transparent 1.5px); + background-size: 12px 12px; + opacity: 0.6; +} + +/* Offset block behind a card or image — the "stacked paper" look. + The accent slab is kept *inside* the element's own box (via padding) rather + than bled outside it, so the effect survives any ancestor that clips + overflow — which `.has-deco` and `.slide` both do. */ +.offset-block { + position: relative; + isolation: isolate; + padding: 0 var(--offset-shift, var(--space-5)) var(--offset-shift, var(--space-5)) 0; +} +.offset-block::before { + content: ""; + position: absolute; + inset: var(--offset-shift, var(--space-5)) 0 0 var(--offset-shift, var(--space-5)); + background: var(--accent-500); + border-radius: var(--radius-lg); + z-index: -1; +} + +/* Shift the slab to the other side. */ +.offset-block--left { + padding: 0 0 var(--offset-shift, var(--space-5)) var(--offset-shift, var(--space-5)); +} +.offset-block--left::before { + inset: var(--offset-shift, var(--space-5)) var(--offset-shift, var(--space-5)) 0 0; +} + +.slide .offset-block { --offset-shift: 2.4cqw; } + +/* Anything that hosts .deco children needs this. */ +.has-deco { position: relative; overflow: hidden; isolation: isolate; } + +/* -------------------------------------------------------------------------- + Progress / meters + -------------------------------------------------------------------------- */ + +.meter { + height: 8px; + border-radius: var(--radius-pill); + background: var(--ink-100); + overflow: hidden; +} +.meter__fill { + height: 100%; + border-radius: inherit; + background: var(--accent-500); +} +.meter--thick { height: 14px; } + +.meter-row { display: grid; gap: var(--space-2); } +.meter-row__head { + display: flex; + justify-content: space-between; + font-size: var(--fs-small); + font-weight: var(--fw-semibold); + color: var(--fg-strong); +} + +/* -------------------------------------------------------------------------- + Table + -------------------------------------------------------------------------- */ + +.table-wrap { overflow-x: auto; } + +.table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-small); + min-width: 520px; +} + +.table th { + text-align: left; + font-family: var(--font-display); + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-muted); + padding: var(--space-3) var(--space-4); + border-bottom: 2px solid var(--ink-200); + white-space: nowrap; +} + +.table td { + padding: var(--space-4); + border-bottom: 1px solid var(--ink-100); + color: var(--fg-body); +} + +.table tbody tr:hover { background: var(--surface-muted); } +.table td:first-child { font-weight: var(--fw-semibold); color: var(--fg-strong); } + +/* -------------------------------------------------------------------------- + Quote / testimonial + -------------------------------------------------------------------------- */ + +.quote { + position: relative; + padding-left: var(--space-7); + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: var(--fw-medium); + line-height: 1.5; + color: var(--fg-strong); +} + +.quote::before { + content: "\201C"; + position: absolute; + left: 0; + top: -0.35em; + font-size: 4rem; + line-height: 1; + color: var(--accent-300); +} + +.quote__cite { + display: flex; + align-items: center; + gap: var(--space-3); + margin-top: var(--space-5); + font-family: var(--font-body); + font-size: var(--fs-small); + font-weight: var(--fw-regular); + color: var(--fg-muted); +} +.quote__name { font-weight: var(--fw-semibold); color: var(--fg-strong); display: block; } + +/* -------------------------------------------------------------------------- + SWOT — the 2x2 that appears in nearly every deck in the reference set + -------------------------------------------------------------------------- */ + +.swot { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } +.swot > * { padding: var(--space-5); border-radius: var(--radius-lg); } +.swot__s { background: var(--accent-500); color: var(--accent-on); } +.swot__w { background: var(--ink-900); color: #fff; } +.swot__o { background: var(--accent-50); color: var(--ink-900); } +.swot__t { background: var(--surface-muted); color: var(--ink-900); } +.swot__label { + font-family: var(--font-display); + font-size: var(--fs-caption); + font-weight: var(--fw-bold); + letter-spacing: 0.14em; + text-transform: uppercase; + opacity: 0.75; +} +.swot__body { font-size: var(--fs-small); margin-top: var(--space-2); } + +@media (max-width: 700px) { .swot { grid-template-columns: 1fr; } } + +/* -------------------------------------------------------------------------- + Navbar (web templates) + -------------------------------------------------------------------------- */ + +.navbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-6); + padding: var(--space-4) 0; +} + +.navbar__brand { + display: flex; + align-items: center; + gap: var(--space-3); + font-family: var(--font-display); + font-size: 1.125rem; + font-weight: var(--fw-bold); + letter-spacing: -0.01em; + color: var(--fg-strong); + text-decoration: none; +} + +.navbar__links { + display: flex; + gap: var(--space-6); + list-style: none; + font-size: var(--fs-small); + font-weight: var(--fw-medium); +} +.navbar__links a { color: var(--fg-body); text-decoration: none; } +.navbar__links a:hover { color: var(--accent-600); } + +@media (max-width: 800px) { .navbar__links { display: none; } } diff --git a/design-system/css/slides.css b/design-system/css/slides.css new file mode 100644 index 0000000..ba5ab3b --- /dev/null +++ b/design-system/css/slides.css @@ -0,0 +1,323 @@ +/* ========================================================================== + Corporate Deck Design System — Slides + 16:9 slide surfaces and the standard slide archetypes. + Requires: tokens.css, base.css, components.css + + A .slide is a self-contained 16:9 canvas that scales with its container. + Everything inside sizes in `cqw` (container query units) so a slide looks + identical whether it renders full-screen or as a thumbnail in a grid. + ========================================================================== */ + +.slide { + container-type: inline-size; + position: relative; + aspect-ratio: 16 / 9; + width: 100%; + overflow: hidden; + background: var(--surface-page); + color: var(--fg-body); + border-radius: var(--radius-md); + box-shadow: var(--shadow-slide); + isolation: isolate; +} + +/* Safe area — nothing meaningful crosses this margin. */ +.slide__inner { + position: relative; + z-index: 1; + height: 100%; + padding: 7cqw 6.5cqw; + display: flex; + flex-direction: column; + justify-content: center; +} + +.slide__inner--top { justify-content: flex-start; } +.slide__inner--bottom { justify-content: flex-end; } +.slide__inner--flush { padding: 0; } + +/* Slide surface variants */ +.slide--muted { background: var(--surface-muted); } +.slide--cream { background: var(--surface-cream); } +.slide--accent { background: var(--accent-500); color: var(--accent-on); } +.slide--dark { background: var(--ink-900); } +.slide--dark .h1, .slide--dark .h2, .slide--dark .h3, +.slide--dark .display { color: #fff; } + +/* -------------------------------------------------------------------------- + Slide type scale + Sized in cqw so type stays proportional at any render size. + -------------------------------------------------------------------------- */ + +.slide .display { font-size: 7.2cqw; line-height: 1.0; letter-spacing: -0.03em; } +.slide .h1 { font-size: 5.4cqw; line-height: 1.05; letter-spacing: -0.025em; } +.slide .h2 { font-size: 4.0cqw; line-height: 1.1; letter-spacing: -0.02em; } +.slide .h3 { font-size: 2.6cqw; line-height: 1.2; } +.slide .h4 { font-size: 2.0cqw; line-height: 1.3; } +.slide .body { font-size: 1.65cqw; line-height: 1.6; } +.slide .small { font-size: 1.45cqw; line-height: 1.55; } +.slide .caption { font-size: 1.25cqw; } +.slide .eyebrow { font-size: 1.3cqw; letter-spacing: 0.16em; } +.slide .numeral { font-size: 11cqw; } +.slide .stat__value { font-size: 5cqw; } +.slide .stat__label { font-size: 1.2cqw; } +.slide .btn { font-size: 1.5cqw; padding: 1.1cqw 2.4cqw; } +.slide .icon-chip { width: 5.6cqw; height: 5.6cqw; border-radius: 1.2cqw; } +.slide .rule { width: 6cqw; height: 0.45cqw; } +.slide .tag { font-size: 1.2cqw; padding: 0.5cqw 1.2cqw; } +.slide .card { padding: 2.6cqw; border-radius: 1.6cqw; } +.slide .card__title { font-size: 2cqw; } +.slide .card__body { font-size: 1.45cqw; } +.slide .grid { gap: 2.2cqw; } +.slide .split { gap: 4cqw; } +.slide .quote { font-size: 2.4cqw; padding-left: 5cqw; } +.slide .quote::before { font-size: 7cqw; } +.slide .avatar { width: 9cqw; height: 9cqw; border-width: 0.4cqw; } +.slide .swot > * { padding: 2.2cqw; border-radius: 1.4cqw; } +.slide .swot__label { font-size: 1.15cqw; } +.slide .swot__body { font-size: 1.4cqw; margin-top: 0.6cqw; } +.slide .meter { height: 0.85cqw; } +.slide .step__title { font-size: 2cqw; } +.slide .step__body { font-size: 1.4cqw; margin-top: 0.6cqw; } +.slide .timeline__date { font-size: 1.9cqw; } +.slide .timeline__body { font-size: 1.35cqw; margin-top: 0.4cqw; } +.slide .feature__body { font-size: 1.4cqw; } + +/* --- Spacing inside a slide is proportional, not absolute ----------------- + The `--space-*` tokens are rem-based, which is correct for a document but + wrong on a fixed canvas: at thumbnail size a 24px gap eats a tenth of the + slide and pushes content out of the frame. Every rhythm inside .slide is + therefore restated in cqw so a slide composes identically at any size. */ + +.slide .stack { --flow: 1.8cqw; } +.slide .stack-2 { --flow: 0.8cqw; } +.slide .stack-3 { --flow: 1.2cqw; } +.slide .stack-5 { --flow: 2.6cqw; } +.slide .stack-6 { --flow: 3.4cqw; } + +.slide .steps { gap: 2.4cqw; padding-top: 0; } +.slide .step { padding-top: 3.6cqw; } +.slide .step::before { font-size: 3.4cqw; } +.slide .timeline { gap: 2.2cqw; padding-top: 3.4cqw; } +.slide .timeline__item::before { top: -3cqw; width: 1.5cqw; height: 1.5cqw; box-shadow: 0 0 0 0.45cqw var(--surface-page); } +.slide .stat-row { gap: 2.6cqw; } +.slide .stat-row--divided > .stat + .stat { padding-left: 2.6cqw; } +.slide .swot { gap: 1.8cqw; } +.slide .quote__cite { gap: 1.4cqw; margin-top: 2.2cqw; font-size: 1.4cqw; } +.slide .meter-row { gap: 0.8cqw; } +.slide .meter-row__head { font-size: 1.4cqw; } +.slide .agenda__item { padding-bottom: 1.2cqw; } + +/* --- A slide is a fixed canvas: it never reflows -------------------------- + The responsive collapses in components.css are right for a scrolling page + and wrong here. Column counts inside a slide are pinned at every width so + a 460px thumbnail and a 1920px projection show the same composition. */ + +.slide .grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.slide .grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.slide .grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.slide .split { grid-template-columns: minmax(0, var(--split, 5fr)) minmax(0, 7fr); } +.slide .swot { grid-template-columns: 1fr 1fr; } + +/* auto-fit minimums are px-based and wrap far too early on a slide canvas */ +.slide .steps, +.slide .stat-row { + grid-template-columns: none; + grid-auto-flow: column; + grid-auto-columns: 1fr; +} + +.slide .timeline { + grid-auto-flow: column; + grid-auto-columns: 1fr; + padding-left: 0; +} +.slide .timeline::before { top: 0.4cqw; bottom: auto; left: 0; right: 0; width: auto; height: 0.2cqw; } +.slide .stat-row--divided > .stat + .stat { border-left: 1px solid var(--ink-200); } +.slide.on-dark .stat-row--divided > .stat + .stat { border-left-color: rgba(255,255,255,0.18); } + +/* -------------------------------------------------------------------------- + Slide furniture — the persistent chrome on every non-cover slide + -------------------------------------------------------------------------- */ + +.slide__logo { + position: absolute; + top: 4cqw; + left: 6.5cqw; + z-index: 2; + display: flex; + align-items: center; + gap: 1cqw; + font-family: var(--font-display); + font-size: 1.6cqw; + font-weight: var(--fw-bold); + letter-spacing: -0.01em; + color: var(--fg-strong); +} + +.slide__kicker { + position: absolute; + top: 4.4cqw; + right: 6.5cqw; + z-index: 2; + font-family: var(--font-display); + font-size: 1.15cqw; + font-weight: var(--fw-semibold); + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--fg-muted); +} + +.slide__pagenum { + position: absolute; + bottom: 4cqw; + right: 6.5cqw; + z-index: 2; + font-family: var(--font-display); + font-size: 1.3cqw; + font-weight: var(--fw-bold); + color: var(--ink-300); + font-variant-numeric: tabular-nums; +} + +.slide--dark .slide__logo, +.slide--accent .slide__logo { color: currentColor; } +.slide--dark .slide__kicker { color: rgba(255,255,255,0.55); } +.slide--dark .slide__pagenum { color: rgba(255,255,255,0.45); } + +/* A slide with furniture needs headroom so content clears the logo row. */ +.slide--chromed .slide__inner { padding-top: 11cqw; padding-bottom: 9cqw; } + +/* -------------------------------------------------------------------------- + Archetype: cover + Split canvas — type block on one side, full-bleed media on the other. + -------------------------------------------------------------------------- */ + +.slide-cover { display: grid; grid-template-columns: 1.05fr 1fr; height: 100%; } + +.slide-cover__type { + padding: 7cqw 5cqw 7cqw 6.5cqw; + display: flex; + flex-direction: column; + justify-content: center; + gap: 2cqw; +} + +.slide-cover__media { position: relative; overflow: hidden; } +.slide-cover__media img { width: 100%; height: 100%; object-fit: cover; } + +/* Media panel with the signature curved inner edge. */ +.slide-cover__media--curved { border-radius: 18cqw 0 0 18cqw; overflow: hidden; } + +.slide-cover--reverse { grid-template-columns: 1fr 1.05fr; } +.slide-cover--reverse .slide-cover__type { order: 2; padding: 7cqw 6.5cqw 7cqw 5cqw; } +.slide-cover--reverse .slide-cover__media { order: 1; } + +/* Full-bleed cover — image behind, type on top. */ +.slide-cover--bleed { grid-template-columns: 1fr; } +.slide-cover--bleed .slide-cover__media { position: absolute; inset: 0; } +.slide-cover--bleed .slide-cover__type { position: relative; z-index: 2; max-width: 62%; } + +/* -------------------------------------------------------------------------- + Archetype: section divider + Oversized numeral, short title, optional accent panel. + -------------------------------------------------------------------------- */ + +.slide-section { display: grid; grid-template-columns: 1fr 1fr; height: 100%; align-items: stretch; } + +.slide-section__panel { + background: var(--accent-500); + color: var(--accent-on); + padding: 7cqw 5cqw 7cqw 6.5cqw; + display: flex; + flex-direction: column; + justify-content: center; + gap: 1.5cqw; +} + +.slide-section__panel .numeral { color: color-mix(in srgb, var(--accent-on) 32%, transparent); } +.slide-section__panel .h1 { color: var(--accent-on); } + +.slide-section__aside { + padding: 7cqw 6.5cqw 7cqw 5cqw; + display: flex; + flex-direction: column; + justify-content: center; + gap: 1.5cqw; +} + +/* -------------------------------------------------------------------------- + Archetype: contents / agenda + -------------------------------------------------------------------------- */ + +.agenda { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4cqw 4cqw; } + +.agenda__item { + display: flex; + align-items: baseline; + gap: 1.6cqw; + padding-bottom: 1.2cqw; + border-bottom: 1px solid var(--ink-100); +} + +.agenda__num { + font-family: var(--font-display); + font-size: 2.4cqw; + font-weight: var(--fw-black); + color: var(--accent-500); + letter-spacing: -0.03em; + font-variant-numeric: tabular-nums; + flex: none; +} + +.agenda__label { + font-family: var(--font-display); + font-size: 1.9cqw; + font-weight: var(--fw-semibold); + color: var(--fg-strong); +} + +/* -------------------------------------------------------------------------- + Archetype: closing + -------------------------------------------------------------------------- */ + +.slide-close { text-align: center; display: grid; place-content: center; gap: 2cqw; height: 100%; padding: 6.5cqw; } +.slide-close .display { font-size: 8cqw; } + +/* -------------------------------------------------------------------------- + Deck — a scrollable grid of slide thumbnails + -------------------------------------------------------------------------- */ + +.deck { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: var(--space-6); +} + +.deck--single { grid-template-columns: 1fr; } +.deck--wide { grid-template-columns: repeat(auto-fill, minmax(460px, 1fr)); } + +.deck__slide { display: flex; flex-direction: column; gap: var(--space-2); } +.deck__caption { + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-muted); +} + +/* -------------------------------------------------------------------------- + Print — one slide per page, landscape + -------------------------------------------------------------------------- */ + +@media print { + @page { size: landscape; margin: 0; } + .deck { display: block; } + .deck__caption { display: none; } + .slide { + break-after: page; + border-radius: 0; + box-shadow: none; + width: 100%; + } +} diff --git a/design-system/examples/deck.html b/design-system/examples/deck.html new file mode 100644 index 0000000..1c0edb6 --- /dev/null +++ b/design-system/examples/deck.html @@ -0,0 +1,420 @@ + + + + + +Corporate Deck Design System — Slide Deck + + + + + + + + +
+ +
+
+ Reference deck +

Fourteen slide archetypes

+

Every slide below is composed from system classes only — no bespoke CSS. Print this page to get one slide per landscape page.

+
+
+ + + + + + +
+
+ +
+ + +
+
+
+
+ + Inspirational business presentation +

+ Doing everything + Right +

+
+

+ Presented by Elizabeth Jones
+ 25 September 2025 · London, UK +

+
+
Cover photography
+
+
+

01 — Cover, split

+
+ + +
+
+
+
Full-bleed photo
+
+ Annual review +

Business Report 2025

+

Nullam eu tempor purus. Nunc a leo magna, sit amet consequat risus.

+
+
+
+

02 — Cover, full bleed

+
+ + +
+
+ +
Contents
+
02
+
+
+
+ Table of contents +

What we will cover

+
+
+
01Who we are
+
04The numbers
+
02Our services
+
05Roadmap
+
03How we work
+
06Next steps
+
+
+
+
+

03 — Agenda

+
+ + +
+
+
+
+ 01 +

Who we are

+

A short read on the firm, the bench, and how we staff.

+
+
Section photo
+
+
+

04 — Section divider

+
+ + +
+
+ +
Who we are
+
04
+
+
+
+ About us +

Let's talk about who we are

+
+

We are a twenty-person operating firm. Every engagement is led by a partner who stays on the work from diagnosis through delivery — there is no handoff to a junior bench, because there isn't one.

+

We take on six clients a year. That is the constraint, and we are not trying to solve it.

+
+
Team photo
+
+
+
+

05 — Split, media right

+
+ + +
+
+ +
Our services
+
05
+
+
+
+ What we do +

Three practices

+
+
+
+ +

Planning

+

Annual and quarterly plans that survive contact with the calendar.

+
+
+ +

Analytics

+

The three numbers that matter, reported the same way every week.

+
+
+ +

Delivery

+

We stay through execution. Plans without follow-through are decks.

+
+
+
+
+
+

06 — Three cards

+
+ + +
+
+ +
How we work
+
06
+
+
+
+ Our process +

Three phases, twelve weeks

+
+
+
+

Diagnose

+

Two weeks inside the business. Interviews, data pull, and an honest read on what is blocking the quarter.

+
+
+

Design

+

A plan with named owners and dated milestones, reviewed until there are no open questions.

+
+
+

Deliver

+

Eight weeks embedded, running the weekly cadence until it runs without us.

+
+
+
+
+
+

07 — Numbered steps

+
+ + +
+
+ + +
The numbers
+
07
+
+
+
+ By the numbers +

Four years, measured

+
+
+
$1.4BRevenue influenced
+
168Engagements
+
11.5Weeks to impact
+
94%Clients retained
+
+
+
+
+

08 — Stat row, dark

+
+ + +
+
+ +
Roadmap
+
08
+
+
+
+ Our story so far +

A little bit of history

+
+
+
2019

Founded with three partners and one client.

+
2021

Analytics practice opens; first retained engagement.

+
2023

London office. Bench grows to fourteen.

+
2025

Twenty people, six clients a year, capped deliberately.

+
+
+
+
+

09 — Timeline

+
+ + +
+
+ +
Position
+
09
+
+
+
+ Where we stand +

SWOT analysis

+
+
+
Strengths

Senior bench on every engagement; repeatable operating cadence.

+
Weaknesses

Deliberately small — capacity is the binding constraint.

+
Opportunities

Mid-market operators underserved by the major firms.

+
Threats

In-house strategy teams maturing quickly.

+
+
+
+
+

10 — SWOT

+
+ + +
+
+ +
Financials
+
10
+
+
+
+ Financial overview +

Revenue by year

+
+

Series one carries the accent; every other series recedes to grey so the eye lands where the argument is.

+
+ + + + + + + + + + + + + + + + + + + + + + 2022 + 2023 + 2024 + 2025 + + +
+
+
+

11 — Chart

+
+ + +
+
+ +
The team
+
11
+
+
+
+ Our people +

The team

+
+
+
+
IL
+

Isabella Linton

+

Managing Partner

+
+
+
AD
+

Arthur Dodger

+

Head of Analytics

+
+
+
EJ
+

Elizabeth Jones

+

Delivery Lead

+
+
+
+
+
+

12 — Team

+
+ + +
+
+ +
Testimonials
+
12
+
+
+
Client photo
+
+ They did not hand us a deck and leave. Eleven weeks later the cadence was still running, and it was our team running it. +
+ Marta ReyesCOO, Halden Group +
+
+
+
+
+

13 — Testimonial

+
+ + +
+
+ +
+ For watching +

Thank you

+

+ hello@northbrook.example · +44 20 7946 0100 · northbrook.example +

+
+
+

14 — Closing

+
+ +
+
+ + + + + diff --git a/design-system/examples/index.html b/design-system/examples/index.html new file mode 100644 index 0000000..dab4d2b --- /dev/null +++ b/design-system/examples/index.html @@ -0,0 +1,291 @@ + + + + + +Corporate Deck Design System — Web Template + + + + + + + +
+ +
+ + +
+
+ +
+ Strategy & Operations +

Doing everything right, from day one.

+

+ We help operators turn a messy quarter into a plan the whole company can + follow — then we stay on to make sure it actually ships. +

+
+ Start a project + See our work +
+
+
168Engagements
+
29Countries
+
94%Retained
+
+
+
+
Photography
+
+
+
+ + +
+
+
+ What we do +

Four practices, one operating rhythm

+
+
+
+
+ +

Planning

+

Annual and quarterly plans that survive contact with the calendar.

+
+
+ +

Analytics

+

The three numbers that matter, reported the same way every week.

+
+
+ +

Org design

+

Structures that match the work, not last year's headcount.

+
+
+ +

Delivery

+

We stay through execution. Plans without follow-through are decks.

+
+
+
+
+ + +
+
+
+ How it works +

Three phases, twelve weeks

+
+
+
+
+

Diagnose

+

Two weeks inside the business. Interviews, data pull, and an honest read on what is actually blocking the quarter.

+
+
+

Design

+

A plan with named owners and dated milestones. Reviewed with the leadership team until there are no open questions.

+
+
+

Deliver

+

Eight weeks embedded with your team, running the weekly cadence until it runs without us.

+
+
+
+
+ + +
+
+ +
+ By the numbers +

What the last four years looked like

+
+
+
+
+ $1.4B + Revenue influenced +
+
+ 168 + Engagements closed +
+
+ 11.5 + Avg. weeks to impact +
+
+ 94% + Clients who return +
+
+
+
+ + +
+
+
+ Capability mix +

Where the team is strongest

+

+ We publish our own capability scores twice a year and staff engagements + against them, so you know exactly what you are getting. +

+
+
+
+
Operating strategy92%
+
+
+
+
Financial modelling85%
+
+
+
+
Org & talent design78%
+
+
+
+
Go-to-market71%
+
+
+
+
+
+ + +
+
+
+ The team +

Senior people, on the work

+
+
+
+
+
IL
+

Isabella Linton

+

Managing Partner

+
+
+
AD
+

Arthur Dodger

+

Head of Analytics

+
+
+
EJ
+

Elizabeth Jones

+

Delivery Lead

+
+
+
+
+ + +
+
+
+ They did not hand us a deck and leave. Eleven weeks later the weekly + cadence was still running, and it was our team running it. +
+
MR
+ Marta ReyesCOO, Halden Group +
+
+
+
Strengths

Senior bench, repeatable operating cadence.

+
Weaknesses

Deliberately small — capacity is the constraint.

+
Opportunities

Mid-market operators underserved by the majors.

+
Threats

In-house strategy teams maturing quickly.

+
+
+
+ + +
+
+

Ready to fix the quarter?

+

+ Tell us what is stuck. We will come back within two days with a view on + whether we are the right people for it. +

+
Book a call
+
+
+ + + + + + + diff --git a/design-system/powerpoint/SPEC.md b/design-system/powerpoint/SPEC.md new file mode 100644 index 0000000..fa9d7d3 --- /dev/null +++ b/design-system/powerpoint/SPEC.md @@ -0,0 +1,195 @@ +# PowerPoint Implementation Spec + +Concrete numbers for building the system as a `.potx` template. All values assume +the **16:9 widescreen** slide size: **13.333″ × 7.5″** (33.87 cm × 19.05 cm). + +To convert to the older 10″ × 5.625″ canvas, multiply every length by `0.75`. + +--- + +## Slide setup + +| Setting | Value | +|---|---| +| Slide size | Widescreen 16:9 — 13.333″ × 7.5″ | +| Safe margin | 0.87″ all sides (6.5%) | +| Grid | 12 columns, 0.87″ outer margin, 0.17″ gutter → column width 0.93″ | +| Baseline | 0.042″ (4px at 96 DPI) | + +Content never crosses the safe margin. Full-bleed photos and colour blocks are +the only exception. + +--- + +## Theme colours + +Map these into **Design → Variants → Colors → Customize Colors**. Slots are named +by PowerPoint's own labels. + +| Slot | Amber (default) | Electric | Navy | Teal | Green | Coral | +|---|---|---|---|---|---|---| +| Text/Background – Dark 1 | `14181F` | `14181F` | `14181F` | `14181F` | `14181F` | `14181F` | +| Text/Background – Light 1 | `FFFFFF` | `FFFFFF` | `FFFFFF` | `FFFFFF` | `FFFFFF` | `FFFFFF` | +| Text/Background – Dark 2 | `233049` | `233049` | `152547` | `233049` | `233049` | `233049` | +| Text/Background – Light 2 | `F7F8FA` | `F7F8FA` | `F7F8FA` | `F7F8FA` | `F7F8FA` | `FAF6EC` | +| **Accent 1** (the accent) | `F5B21A` | `2B50EE` | `2B4A8B` | `22BCCE` | `17A673` | `E85D33` | +| Accent 2 (accent light) | `FFD24D` | `8DA5FF` | `8095C4` | `63D6E2` | `63CFA1` | `F29B80` | +| Accent 3 (accent pale) | `FFF8E6` | `EDF1FF` | `EEF2F9` | `E6F8FA` | `E7F7EF` | `FDEEEA` | +| Accent 4 (ink mid) | `3A4761` | `3A4761` | `3A4761` | `3A4761` | `3A4761` | `3A4761` | +| Accent 5 (grey) | `8993A6` | `8993A6` | `8993A6` | `8993A6` | `8993A6` | `8993A6` | +| Accent 6 (hairline) | `ECEFF3` | `ECEFF3` | `ECEFF3` | `ECEFF3` | `ECEFF3` | `ECEFF3` | +| Hyperlink | `E09A00` | `1D3ACC` | `1F3768` | `1897A6` | `11855C` | `C64720` | +| Followed hyperlink | `B87A00` | `152B99` | `152547` | `11707B` | `0B6244` | `993518` | + +**Chart series order** must be set on the theme so charts inherit it: +Accent 1 → Dark 2 → Accent 2 → Accent 5 → Accent 3 → Accent 6. + +--- + +## Theme fonts + +| Slot | Font | Fallback if unavailable | +|---|---|---| +| Headings | Poppins SemiBold / Bold | Montserrat → Segoe UI Semibold | +| Body | Inter Regular | Poppins Regular → Segoe UI | + +Embed fonts in the `.potx` (**File → Options → Save → Embed fonts in the file → +Embed only the characters used**) so the deck survives being opened elsewhere. + +--- + +## Type scale in points + +Slide-native sizes. These are the cqw values from `css/slides.css` resolved +against a 13.333″ canvas. + +| Style | Size | Weight | Tracking | Line spacing | +|---|---|---|---|---| +| Cover title | 66 pt | Black (800) | −0.03em ≈ −2.0 pt | 0.95 | +| Slide title (H1) | 50 pt | Black (800) | −0.025em ≈ −1.25 pt | 1.0 | +| Sub-head (H2) | 37 pt | Bold (700) | −0.02em ≈ −0.75 pt | 1.05 | +| Card title (H3) | 24 pt | Bold (700) | −0.01em | 1.15 | +| Feature label (H4) | 18 pt | SemiBold (600) | 0 | 1.25 | +| Body | 15 pt | Regular (400) | 0 | 1.5 | +| Small / card copy | 13 pt | Regular (400) | 0 | 1.45 | +| Caption | 11 pt | Medium (500) | 0 | 1.4 | +| **Eyebrow** | 12 pt | SemiBold (600) | **+0.18em ≈ +2.2 pt**, ALL CAPS | 1.15 | +| Section numeral | 100 pt | Black (800) | −0.04em ≈ −4 pt | 0.9 | +| Stat value | 46 pt | Black (800) | −0.035em | 1.0 | +| Stat label | 11 pt | SemiBold (600) | +0.06em, ALL CAPS | 1.3 | + +PowerPoint expresses tracking in points via **Format Text → Character Spacing → +Spacing: Expanded/Condensed By**. The pt equivalents above are pre-computed. + +--- + +## Shape geometry + +| Element | Size | Corner radius | +|---|---|---| +| Card | column-spanning, min 2.8″ × 1.9″ | 0.21″ (20px) | +| Media panel | varies | 0.33″ (32px) | +| Hero media | varies | 0.50″ (48px) | +| Icon chip | 0.55″ × 0.55″ | 0.125″ (12px) | +| Icon chip (large) | 0.75″ × 0.75″ | 0.21″ | +| Button | height 0.42″, pad 0.36″ horizontal | fully rounded | +| Accent rule | 0.58″ × 0.042″ | fully rounded | +| Meter bar | height 0.085″ | fully rounded | + +PowerPoint's rounded rectangle uses a *proportional* corner handle, so set radius +by dragging until the numeric adjustment reads the fraction below, or edit the +shape's `adj` value in the XML: + +| Target radius | On a 1.9″-tall shape | `adj` value | +|---|---|---| +| 0.21″ | 11% of the short side | `11000` | +| 0.33″ | 17% | `17000` | +| 0.50″ | 26% | `26000` | + +--- + +## Shadows + +One shadow preset only, applied to cards and floating panels: + +- **Outer shadow**, colour `14181F` at **9% opacity** +- Blur **24 pt**, Distance **9 pt**, Angle **90°**, Size 100% + +Never use PowerPoint's default shadow presets — they are far too heavy. + +--- + +## Slide layouts to build in the Slide Master + +Build these 14 layouts. Every one except Cover and Closing carries the furniture +described below. + +| # | Layout name | Structure | +|---|---|---| +| 1 | **Cover — split** | Type block left 52%, full-bleed photo right 48% | +| 2 | **Cover — bleed** | Full-bleed photo, dark scrim, type block over the left 62% | +| 3 | **Agenda** | Two columns × up to 4 numbered rows, hairline under each | +| 4 | **Section divider** | Accent panel left 50% with numeral + title; photo right | +| 5 | **Title + body** | H1 top-left, body block 7 columns | +| 6 | **Split — media right** | 5:7 type/media | +| 7 | **Split — media left** | 7:5 media/type | +| 8 | **Three cards** | 3 equal cards, icon chip + title + copy | +| 9 | **Four cards** | 4 equal cards | +| 10 | **Numbered steps** | 3–4 columns, ghost numeral above each | +| 11 | **Stat row** | Dark or accent fill, 3–4 stats, hairline dividers | +| 12 | **Timeline** | Horizontal rule with 4–6 accent dots | +| 13 | **SWOT** | 2×2 — S on accent, W on ink, O on accent-pale, T on light-2 | +| 14 | **Closing** | Centred display type, contact block, socials | + +### Furniture (place on the Slide Master, not per-slide) + +| Item | Position | Style | +|---|---|---| +| Logo | 0.87″ from left, 0.53″ from top | 12–16 pt, display SemiBold | +| Section kicker | right-aligned to 0.87″ from right, 0.58″ from top | 10 pt, +0.16em, CAPS, grey `8993A6` | +| Page number | right-aligned to 0.87″ from right, 0.53″ from bottom | 12 pt Bold, `B4BCC9`, tabular | + +Content placeholders on furnished layouts start at **1.47″ from the top** and end +**1.2″ from the bottom** so nothing collides with the chrome. + +--- + +## Picture placeholders + +Set every picture placeholder to **fill** (not fit) so photos crop rather than +letterbox. Apply the corner radius by using a rounded-rectangle *picture +placeholder*, not by cropping to shape after the fact — the latter breaks when +the user swaps the image. + +Two permitted photo treatments: + +1. **Accent wash** — place a rectangle of Accent 1 over the photo, set to + **Multiply** blend at **55%** transparency. Group it with the picture. +2. **Bottom scrim** — a rectangle filled with a linear gradient, `14181F` at 15% + opacity (top) → `14181F` at 72% opacity (bottom), 90°. + +--- + +## Charts + +- Delete: chart border, gridline major/minor on the category axis, tick marks, + chart area fill. +- Value-axis gridlines: `ECEFF3`, 0.75 pt. +- Axis labels: 11 pt, `8993A6`. +- Data labels: 11 pt SemiBold, `14181F`, positioned outside end. +- Bar gap width: **60%**. Bar corner radius: 0.06″ on the outer end only. +- Line charts: 2.5 pt line, no markers unless there are fewer than 8 points; then + circular markers 6 pt in Accent 1 with a white 1.5 pt outline. +- Pie/doughnut: doughnut hole **68%**, first slice Accent 1, remainder greys. + +--- + +## Export and handoff + +- Ship as **`.potx`** (template), not `.pptx`. Users then get a clean + New-from-template flow with all layouts available. +- Include a **`.thmx`** theme file so the colour and font set can be applied to + existing decks. +- Verify in **Slide Master view** that no layout has stray placeholders, and that + every layout's name matches the table above — designers pick layouts by name. +- Test at 100% zoom and as a printed handout (3-per-page) before sign-off. diff --git a/design-system/tokens/tokens.css b/design-system/tokens/tokens.css new file mode 100644 index 0000000..b379839 --- /dev/null +++ b/design-system/tokens/tokens.css @@ -0,0 +1,204 @@ +/* ========================================================================== + Corporate Deck Design System — Design Tokens + -------------------------------------------------------------------------- + Single source of truth for colour, type, space, radius, shadow and motion. + Import this first; every other stylesheet in the system depends on it. + + Re-theming: add a theme class to or any container. + -> whole document re-themes +
-> scoped re-theme + ========================================================================== */ + +:root { + /* --- Accent ramp (default: amber) ------------------------------------ */ + --accent-50: #FFF8E6; + --accent-100: #FFEDBF; + --accent-300: #FFD24D; + --accent-500: #F5B21A; + --accent-600: #E09A00; + --accent-700: #B87A00; + + /* Text colour that stays legible on top of --accent-500 */ + --accent-on: #14181F; + + /* --- Ink (dark anchor) ---------------------------------------------- */ + --ink-900: #14181F; + --ink-800: #1B2333; + --ink-700: #233049; + --ink-600: #3A4761; + --ink-500: #5C6880; + --ink-400: #8993A6; + --ink-300: #B4BCC9; + --ink-200: #D8DDE5; + --ink-100: #ECEFF3; + + /* --- Surfaces -------------------------------------------------------- */ + --surface-page: #FFFFFF; + --surface-muted: #F7F8FA; + --surface-cream: #FAF6EC; + --surface-sunken: #EFF1F5; + --surface-inverse: #14181F; + + /* --- Foreground roles ------------------------------------------------ */ + --fg-strong: var(--ink-900); + --fg-body: var(--ink-600); + --fg-muted: var(--ink-400); + --fg-on-dark: #FFFFFF; + --fg-on-dark-muted: rgba(255, 255, 255, 0.72); + + /* --- Semantic -------------------------------------------------------- */ + --positive: #17A673; + --warning: #F5B21A; + --negative: #E04A3F; + --info: #2B50EE; + + /* --- Chart series ---------------------------------------------------- */ + --chart-1: var(--accent-500); + --chart-2: var(--ink-800); + --chart-3: var(--accent-300); + --chart-4: var(--ink-400); + --chart-5: var(--accent-100); + --chart-6: var(--ink-200); + --chart-grid: var(--ink-100); + --chart-axis: var(--ink-400); + + /* --- Typography ------------------------------------------------------ */ + --font-display: 'Poppins', 'Montserrat', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; + --font-body: 'Inter', 'Poppins', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; + + --fw-regular: 400; + --fw-medium: 500; + --fw-semibold: 600; + --fw-bold: 700; + --fw-black: 800; + + --fs-display: 4.5rem; + --fs-h1: 3rem; + --fs-h2: 2.25rem; + --fs-h3: 1.5rem; + --fs-h4: 1.125rem; + --fs-body: 1rem; + --fs-small: 0.875rem; + --fs-caption: 0.75rem; + --fs-eyebrow: 0.75rem; + --fs-numeral: 5rem; + + --lh-tight: 1.06; + --lh-snug: 1.2; + --lh-normal: 1.45; + --lh-loose: 1.65; + + --tracking-display: -0.03em; + --tracking-tight: -0.02em; + --tracking-normal: 0; + --tracking-eyebrow: 0.18em; + + /* --- Space (4px grid) ------------------------------------------------ */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-7: 2.5rem; + --space-8: 3rem; + --space-9: 4rem; + --space-10: 5rem; + --space-11: 6rem; + --space-12: 8rem; + + /* --- Radius ---------------------------------------------------------- */ + --radius-sm: 6px; + --radius-md: 12px; + --radius-lg: 20px; + --radius-xl: 32px; + --radius-2xl: 48px; + --radius-pill: 999px; + --radius-blob: 42% 58% 63% 37% / 41% 44% 56% 59%; + + /* --- Shadow ---------------------------------------------------------- */ + --shadow-sm: 0 1px 2px rgba(20, 24, 31, 0.05); + --shadow-md: 0 4px 16px rgba(20, 24, 31, 0.07); + --shadow-lg: 0 12px 32px rgba(20, 24, 31, 0.09); + --shadow-xl: 0 24px 64px rgba(20, 24, 31, 0.12); + --shadow-slide: 0 18px 48px rgba(20, 24, 31, 0.14); + + /* --- Motion ---------------------------------------------------------- */ + --duration-fast: 140ms; + --duration-base: 220ms; + --duration-slow: 380ms; + --ease-standard: cubic-bezier(0.2, 0, 0.2, 1); + --ease-enter: cubic-bezier(0, 0, 0.2, 1); + --ease-exit: cubic-bezier(0.4, 0, 1, 1); + + /* --- Layout ---------------------------------------------------------- */ + --container-max: 1200px; + --section-py: 6rem; + --slide-safe: 6.5%; +} + +/* ========================================================================== + Accent themes — swap the ramp, everything downstream follows + ========================================================================== */ + +.theme-amber { + --accent-50: #FFF8E6; --accent-100: #FFEDBF; --accent-300: #FFD24D; + --accent-500: #F5B21A; --accent-600: #E09A00; --accent-700: #B87A00; + --accent-on: #14181F; +} + +.theme-electric { + --accent-50: #EDF1FF; --accent-100: #D6E0FF; --accent-300: #8DA5FF; + --accent-500: #2B50EE; --accent-600: #1D3ACC; --accent-700: #152B99; + --accent-on: #FFFFFF; +} + +.theme-navy { + --accent-50: #EEF2F9; --accent-100: #D5DEEE; --accent-300: #8095C4; + --accent-500: #2B4A8B; --accent-600: #1F3768; --accent-700: #152547; + --accent-on: #FFFFFF; +} + +.theme-teal { + --accent-50: #E6F8FA; --accent-100: #C0EEF3; --accent-300: #63D6E2; + --accent-500: #22BCCE; --accent-600: #1897A6; --accent-700: #11707B; + --accent-on: #14181F; +} + +.theme-green { + --accent-50: #E7F7EF; --accent-100: #C2EBD8; --accent-300: #63CFA1; + --accent-500: #17A673; --accent-600: #11855C; --accent-700: #0B6244; + --accent-on: #FFFFFF; +} + +.theme-coral { + --accent-50: #FDEEEA; --accent-100: #FAD5CB; --accent-300: #F29B80; + --accent-500: #E85D33; --accent-600: #C64720; --accent-700: #993518; + --accent-on: #FFFFFF; +} + +/* ========================================================================== + Dark surface context + Any block that sits on ink flips its foreground roles here, so components + need no dark-specific variants of their own. + ========================================================================== */ + +.on-dark { + --fg-strong: #FFFFFF; + --fg-body: rgba(255, 255, 255, 0.78); + --fg-muted: rgba(255, 255, 255, 0.55); + --surface-page: var(--ink-900); + --surface-muted: var(--ink-800); + --chart-grid: rgba(255, 255, 255, 0.12); + --chart-axis: rgba(255, 255, 255, 0.45); + color: var(--fg-body); +} + +@media (prefers-reduced-motion: reduce) { + :root { + --duration-fast: 0ms; + --duration-base: 0ms; + --duration-slow: 0ms; + } +} diff --git a/design-system/tokens/tokens.json b/design-system/tokens/tokens.json new file mode 100644 index 0000000..b105b15 --- /dev/null +++ b/design-system/tokens/tokens.json @@ -0,0 +1,207 @@ +{ + "$name": "Corporate Deck Design System", + "$version": "1.0.0", + "$description": "Platform-agnostic design tokens distilled from a 32-deck reference set of corporate presentation and web templates. Consume from CSS, PowerPoint theme XML, Figma variables, or any design tool.", + + "color": { + "$comment": "The system is monochrome-accent: ONE accent hue carries the whole deck. Swap the `accent` ramp to re-theme; never mix two accent families in one document.", + + "accent": { + "$default": "amber", + "amber": { + "50": "#FFF8E6", + "100": "#FFEDBF", + "300": "#FFD24D", + "500": "#F5B21A", + "600": "#E09A00", + "700": "#B87A00", + "$signature": "500" + }, + "electric": { + "50": "#EDF1FF", + "100": "#D6E0FF", + "300": "#8DA5FF", + "500": "#2B50EE", + "600": "#1D3ACC", + "700": "#152B99", + "$signature": "500" + }, + "navy": { + "50": "#EEF2F9", + "100": "#D5DEEE", + "300": "#8095C4", + "500": "#2B4A8B", + "600": "#1F3768", + "700": "#152547", + "$signature": "500" + }, + "teal": { + "50": "#E6F8FA", + "100": "#C0EEF3", + "300": "#63D6E2", + "500": "#22BCCE", + "600": "#1897A6", + "700": "#11707B", + "$signature": "500" + }, + "green": { + "50": "#E7F7EF", + "100": "#C2EBD8", + "300": "#63CFA1", + "500": "#17A673", + "600": "#11855C", + "700": "#0B6244", + "$signature": "500" + }, + "coral": { + "50": "#FDEEEA", + "100": "#FAD5CB", + "300": "#F29B80", + "500": "#E85D33", + "600": "#C64720", + "700": "#993518", + "$signature": "500" + } + }, + + "ink": { + "$comment": "The dark anchor. Used for headlines, dark-fill slides, and the deck's 'gravity'.", + "900": "#14181F", + "800": "#1B2333", + "700": "#233049", + "600": "#3A4761", + "500": "#5C6880", + "400": "#8993A6", + "300": "#B4BCC9", + "200": "#D8DDE5", + "100": "#ECEFF3" + }, + + "surface": { + "page": "#FFFFFF", + "muted": "#F7F8FA", + "cream": "#FAF6EC", + "sunken": "#EFF1F5", + "inverse": "#14181F" + }, + + "semantic": { + "positive": "#17A673", + "warning": "#F5B21A", + "negative": "#E04A3F", + "info": "#2B50EE" + }, + + "chart": { + "$comment": "Ordered series palette. Series 1 is always the accent; everything after it recedes into neutrals so a single bar/line reads as 'the point'.", + "series": [ + "var(--accent-500)", + "var(--ink-800)", + "var(--accent-300)", + "var(--ink-400)", + "var(--accent-100)", + "var(--ink-200)" + ], + "grid": "#ECEFF3", + "axis": "#8993A6" + } + }, + + "typography": { + "$comment": "One geometric-grotesque family, three weights. Headlines are heavy and tight; body is regular and roomy.", + "family": { + "display": "'Poppins', 'Montserrat', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif", + "body": "'Inter', 'Poppins', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif", + "mono": "'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace" + }, + "weight": { + "regular": 400, + "medium": 500, + "semibold": 600, + "bold": 700, + "black": 800 + }, + "scale": { + "$comment": "Web values in rem. See powerpoint/SPEC.md for the pt equivalents on a 13.333in x 7.5in slide.", + "display": { "size": "4.5rem", "lineHeight": 1.02, "weight": 800, "tracking": "-0.03em" }, + "h1": { "size": "3rem", "lineHeight": 1.06, "weight": 800, "tracking": "-0.025em" }, + "h2": { "size": "2.25rem", "lineHeight": 1.12, "weight": 700, "tracking": "-0.02em" }, + "h3": { "size": "1.5rem", "lineHeight": 1.2, "weight": 700, "tracking": "-0.01em" }, + "h4": { "size": "1.125rem","lineHeight": 1.3, "weight": 600, "tracking": "0" }, + "body": { "size": "1rem", "lineHeight": 1.65, "weight": 400, "tracking": "0" }, + "small": { "size": "0.875rem","lineHeight": 1.6, "weight": 400, "tracking": "0" }, + "caption": { "size": "0.75rem", "lineHeight": 1.5, "weight": 500, "tracking": "0.01em" }, + "eyebrow": { "size": "0.75rem", "lineHeight": 1.2, "weight": 600, "tracking": "0.18em", "transform": "uppercase" }, + "numeral": { "size": "5rem", "lineHeight": 0.9, "weight": 800, "tracking": "-0.04em" } + } + }, + + "space": { + "$comment": "4px base grid. Slide gutters and card padding always land on this scale.", + "0": "0", + "1": "0.25rem", + "2": "0.5rem", + "3": "0.75rem", + "4": "1rem", + "5": "1.5rem", + "6": "2rem", + "7": "2.5rem", + "8": "3rem", + "9": "4rem", + "10": "5rem", + "11": "6rem", + "12": "8rem" + }, + + "radius": { + "$comment": "Generous rounding is a signature of this system. Sharp corners are the exception, used only for full-bleed color blocks.", + "none": "0", + "sm": "6px", + "md": "12px", + "lg": "20px", + "xl": "32px", + "2xl": "48px", + "pill": "999px", + "blob": "42% 58% 63% 37% / 41% 44% 56% 59%" + }, + + "shadow": { + "$comment": "Soft, low-contrast, never harsh. Cards float a few millimetres, not centimetres.", + "sm": "0 1px 2px rgba(20, 24, 31, 0.05)", + "md": "0 4px 16px rgba(20, 24, 31, 0.07)", + "lg": "0 12px 32px rgba(20, 24, 31, 0.09)", + "xl": "0 24px 64px rgba(20, 24, 31, 0.12)", + "slide": "0 18px 48px rgba(20, 24, 31, 0.14)" + }, + + "border": { + "hairline": "1px solid var(--ink-200)", + "card": "1px solid var(--ink-100)", + "accent": "2px solid var(--accent-500)", + "rule": "4px solid var(--accent-500)" + }, + + "motion": { + "$comment": "Presentation-grade: quick, confident, never bouncy.", + "duration": { "fast": "140ms", "base": "220ms", "slow": "380ms" }, + "easing": { + "standard": "cubic-bezier(0.2, 0, 0.2, 1)", + "enter": "cubic-bezier(0, 0, 0.2, 1)", + "exit": "cubic-bezier(0.4, 0, 1, 1)" + } + }, + + "layout": { + "slide": { + "aspect": "16 / 9", + "safeMarginPct": 6.5, + "columns": 12, + "gutterPct": 2 + }, + "web": { + "containerMax": "1200px", + "sectionPaddingY": "6rem", + "breakpoints": { "sm": "640px", "md": "768px", "lg": "1024px", "xl": "1280px" } + } + } +} From bc5ac1e828daf790c62475d53660da9b0417b634 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 14:03:04 +0000 Subject: [PATCH 02/18] Add favourites set: web-landing layer, three accents, step rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds a second reference set of 12 web landing pages into the system as its own layer rather than into the deck rules. Where the deck set is flat, printed and photography-led, the favourites set is screen-native: gradients, glow, and product UI as the hero visual. New accents: violet (signature of the set, six of twelve references, now the default for web work), indigo, and lime — the last pairing with a cream ground rather than white. Nine themes total, all with complete ramps and an --accent-on that clears 4.5:1. New css/web.css covers what the deck layer does not: gradient heroes with glow orbs and wave dividers, phone and browser mockup frames with floating stat cards, portrait label pins, trust bars, tinted feature cards, oversized ghost numerals on a dotted step rail, pricing with a featured tier, FAQ accordion, marquee strip, CTA bands and newsletter fields, plus the overrides that carry all of it onto a dark page. FAVORITES.md indexes the twelve references and states the three rules that differ from the core brief: gradients are allowed in three places and must stay within one accent ramp, product UI replaces stock photography in the hero, and dark pages are permitted here only. The brief and README now route deliverables to the right layer — decks to slides.css and amber, web to web.css and violet. Kept deliberately lean: the web layer is one file of new patterns with no duplication of the deck layer, and the tokens gained three ramps and four gradient values rather than a parallel set. Verified in Chromium across all three example pages by all nine themes: no page or console errors, no horizontal overflow, every accent ramp resolving. Fixed a flip-order bug found in rendering where the step rail left the numeral at its default order, pushing the visual into the narrow centre column. --- design-system/DESIGN-BRIEF.md | 45 ++- design-system/FAVORITES.md | 101 ++++++ design-system/README.md | 33 +- design-system/css/web.css | 482 ++++++++++++++++++++++++++++ design-system/examples/landing.html | 297 +++++++++++++++++ design-system/tokens/tokens.css | 29 ++ design-system/tokens/tokens.json | 29 ++ 7 files changed, 994 insertions(+), 22 deletions(-) create mode 100644 design-system/FAVORITES.md create mode 100644 design-system/css/web.css create mode 100644 design-system/examples/landing.html diff --git a/design-system/DESIGN-BRIEF.md b/design-system/DESIGN-BRIEF.md index 7cc1274..b242983 100644 --- a/design-system/DESIGN-BRIEF.md +++ b/design-system/DESIGN-BRIEF.md @@ -37,15 +37,18 @@ highlighted word in a headline. A second accent hue is never introduced. ### Approved accent families -Pick one per document. All six are equally house-correct. +Pick one per document. All nine are equally house-correct. | Theme | Signature | Notes | |---|---|---| -| **Amber** | `#F5B21A` | The signature default. Warmest, most-used. Dark text on top. | +| **Amber** | `#F5B21A` | **Default for decks.** Warmest, most-used. Dark text on top. | +| **Violet** | `#7C4DFF` | **Default for web.** Signature of the favourites set. White text on top. | | **Electric** | `#2B50EE` | Tech, SaaS, product. White text on top. | +| **Indigo** | `#4059F0` | Fintech, payments, product. White text on top. | | **Navy** | `#2B4A8B` | Finance, consulting, formal. White text on top. | | **Teal** | `#22BCCE` | Product, startup, healthcare. Dark text on top. | | **Green** | `#17A673` | Sustainability, wellness. White text on top. | +| **Lime** | `#C3DE4A` | Pairs with a cream ground, not white. Dark text on top. | | **Coral** | `#E85D33` | Marketing, creative, retail. White text on top. | Full ramps (50 → 700) live in `tokens/tokens.json`. @@ -59,8 +62,11 @@ Full ramps (50 → 700) live in `tokens/tokens.json`. ### Hard rules - **Never** put two accent families in one document. -- **Never** use a gradient as a brand colour. Gradients are permitted only as a - photo scrim or a subtle decorative blob. +- **Never** use a gradient as a brand colour. In decks, gradients are permitted + only as a photo scrim. On web pages they may additionally fill a hero surface, + a CTA band, or a glow orb — and must run *within one accent ramp* + (`accent-600 → accent-500 → accent-300`), so it is still one hue. See + [`FAVORITES.md`](FAVORITES.md) §"Rules specific to this set". - Accent-on-accent text must clear 4.5:1 — use `--accent-on` from the tokens, which is already set correctly per theme. @@ -241,22 +247,35 @@ Stating the negative space saves rounds of revision: | File | What it is | |---|---| +| `FAVORITES.md` | The second reference set — web landing pages — and the rules specific to it | | `tokens/tokens.json` | Platform-agnostic tokens — import into Figma, Style Dictionary, or a build | -| `tokens/tokens.css` | CSS custom properties + the six accent themes | +| `tokens/tokens.css` | CSS custom properties + the nine accent themes | | `css/base.css` | Reset and typographic primitives | | `css/components.css` | Cards, stats, steps, timelines, SWOT, buttons, media | | `css/slides.css` | 16:9 slide surfaces and slide archetypes | +| `css/web.css` | Web-only: gradient heroes, device mockups, rails, pricing, FAQ, marquee | | `powerpoint/SPEC.md` | Point sizes and inch positions for a 13.333″ × 7.5″ slide | -| `examples/index.html` | Web landing page built from the system | +| `examples/index.html` | Corporate web page built from the deck layer | | `examples/deck.html` | Full slide deck built from the system | +| `examples/landing.html` | SaaS landing page built from the web layer | + +**Which layer to start from:** presentations and reports use the deck layer and +default to amber; landing pages, SaaS sites and portfolios use the web layer and +default to violet. Both share the same tokens, so a deck and a site for the same +client stay in family. ### Prompt to hand an AI tool > Build this using the Corporate Deck Design System. Single accent hue -> (`{amber|electric|navy|teal|green|coral}`), near-black ink anchor, white ground. -> Poppins display / Inter body. Uppercase tracked eyebrow labels above headlines, -> one two-tone headline on the cover, 20px card radius, oversized section -> numerals, accent-filled icon chips, masked photography, charts with the accent -> as series 1 and greys after. Follow the standard deck sequence. Import -> `tokens/tokens.css` and use the existing component classes rather than writing -> new CSS. +> (`{amber|violet|electric|indigo|navy|teal|green|lime|coral}`), near-black ink +> anchor, white ground. Poppins display / Inter body. Uppercase tracked eyebrow +> labels above headlines, one two-tone headline at the top, 20px card radius, +> oversized section numerals, accent-filled icon chips, masked photography, +> charts with the accent as series 1 and greys after. Import `tokens/tokens.css` +> and use the existing component classes rather than writing new CSS. +> +> For a **deck**: also import `css/slides.css`, follow the standard deck +> sequence, default to amber. +> For a **web page**: also import `css/web.css`, default to violet, use a +> gradient hero with a device mockup, a trust bar, tinted feature cards, a step +> rail, pricing with a featured tier, an FAQ, and a CTA band. diff --git a/design-system/FAVORITES.md b/design-system/FAVORITES.md new file mode 100644 index 0000000..df7a3da --- /dev/null +++ b/design-system/FAVORITES.md @@ -0,0 +1,101 @@ +# Favourite Designs + +A second reference set, added after the original 32 deck templates. These are +**web landing pages**, and they behave differently enough to warrant their own +layer (`css/web.css`) rather than being folded into the deck rules. + +Where the deck set is flat, printed, and photography-led, the favourites set is +screen-native: gradients, glow, and **product UI as the hero visual**. + +--- + +## The 12 references + +| # | Subject | Accent | What it contributes | +|---|---|---|---| +| 1 | App landing page (FR) | Violet gradient | Gradient hero + **wave divider**, dotted matrices, icon-circle trio, stat cards | +| 2 | Fintech WordPress theme (FR) | **Lime on cream** | Dark UI cards on a pale ground, **underlined accent word**, FAQ accordion, CTA slab | +| 3 | Flowora — SaaS productivity | Indigo/violet | Two-tone headline, **tinted feature cards**, stat strip, pricing with "Most Popular" | +| 4 | Nexora — product/e-commerce | Indigo→violet | Laptop + phone mockups, floating price card, **trust icons row**, oversized display type | +| 5 | Swix — developer portfolio | Violet | **Marquee strip**, floating label pins over a portrait, service list rows | +| 6 | Alex — portfolio, **dark** | Violet on near-black | Full dark variant, glow orb, numbered work grid, dark stat cards | +| 7 | Aaron Reed — portfolio | Blue | Eyebrow labels throughout, stat row, 01–04 process with arrows | +| 8 | Education landing | Pink/violet | **Ghost numerals on a dotted rail**, alternating sides, quote card with tail | +| 9 | SwiftTechBuy — how it works | Red | `STEP 01/02/03` rail with dotted connectors, circular image masks | +| 10 | Ezypay — fintech transfer | Indigo | Numbered feature sections, full-page + thumbnail presentation | +| 11 | Paynt — e-wallet | Violet | Alternating light/violet section bands, logo wall, feature card grid | +| 12 | Payment gateway | Violet | Dark violet feature band, KPI row, testimonial grid | + +**Violet is the signature of this set** — six of twelve. It is now the default +recommendation for web work, as amber is for decks. + +--- + +## What this set adds to the system + +New accent themes: `theme-violet`, `theme-indigo`, `theme-lime`. + +New components in `css/web.css`: + +| Pattern | Class | +|---|---| +| Gradient hero + soft glow orb | `.hero-gradient`, `.orb` | +| Curved section transition | `.wave` | +| "NEW · Introducing X" pill | `.badge-pill` | +| Accent-underlined headline word | `.underlined` | +| Gradient-filled headline word | `.gradient-text` | +| Phone / browser mockup frames | `.device--phone`, `.device--browser` | +| Floating stat card over a mockup | `.float-card` | +| Label chip pinned to a portrait | `.pin` | +| Logo / trust row | `.trustbar` | +| Tinted feature cards | `.tint-grid`, `.tint-card`, `.tint-1…4` | +| Ghost numerals on a dotted rail | `.rail`, `.rail__num` | +| Pricing with a featured tier | `.price-card`, `.checklist` | +| FAQ accordion | `.faq` | +| Scrolling keyword band | `.marquee` | +| CTA slab + newsletter field | `.cta-band`, `.field-group` | +| Dark page variant | `.on-dark` overrides | + +--- + +## Rules specific to this set + +The core brief still governs — one accent, ink anchor, eyebrow labels, two-tone +headlines, 20px card radius. These are the additions and the exceptions: + +1. **Gradients are allowed here, in three places only:** the hero surface, a CTA + band, and a soft glow orb. Never on a card, a button, or a logo. The gradient + runs within one accent ramp — `accent-600 → accent-500 → accent-300` — so it + is still one hue, not two. + +2. **Product UI is the hero image.** A dashboard or app screen in a device frame + replaces stock photography. Screens are cropped to the top and never + letterboxed. + +3. **Tints are supports, not second accents.** The four card tints are + desaturated far enough that the accent still owns every point of emphasis. If + a tint starts competing, it is too saturated. + +4. **The step rail replaces the deck's `.steps`** for long-form web pages: + oversized ghost numerals down a dotted spine, copy and visual alternating + sides. The deck's compact three-column version stays for slides. + +5. **Dark pages are permitted on this set only** — portfolios and product pages. + Decks stay light-ground. Use `.on-dark` on a near-black surface; the web layer + carries cards, rails and fields across. + +6. **One motion element per page maximum.** The marquee is the usual choice. + Everything honours `prefers-reduced-motion`. + +--- + +## Choosing between the two sets + +| Deliverable | Start from | Default accent | +|---|---|---| +| Presentation, PPT template, report | deck layer (`slides.css`) | amber | +| Landing page, SaaS site, portfolio | web layer (`web.css`) | violet | +| Corporate website for a deck client | web layer, deck's accent | match the deck | + +Both layers share the same tokens, so a deck and a site for the same client stay +in family — swap the theme class and the whole system follows. diff --git a/design-system/README.md b/design-system/README.md index d97d1d1..db8a0c2 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -16,23 +16,37 @@ corners, oversized section numerals, and soft geometric ornament. ``` design-system/ ├── DESIGN-BRIEF.md ← hand this to any designer or AI tool +├── FAVORITES.md ← the web-landing reference set + its extra rules ├── tokens/ │ ├── tokens.json ← platform-agnostic tokens (Figma, Style Dictionary, builds) -│ └── tokens.css ← CSS custom properties + the six accent themes +│ └── tokens.css ← CSS custom properties + the nine accent themes ├── css/ │ ├── base.css ← reset + typographic primitives │ ├── components.css ← cards, stats, steps, timelines, SWOT, buttons, media -│ └── slides.css ← 16:9 slide surfaces and slide archetypes +│ ├── slides.css ← 16:9 slide surfaces and slide archetypes +│ └── web.css ← gradient heroes, device mockups, rails, pricing, FAQ ├── powerpoint/ │ └── SPEC.md ← pt sizes and inch positions for a 13.333" × 7.5" slide └── examples/ - ├── index.html ← web landing page built from the system - └── deck.html ← 14-slide deck built from the system + ├── index.html ← corporate web page (deck layer) + ├── deck.html ← 14-slide deck + └── landing.html ← SaaS landing page (web layer) ``` -Open either example file directly in a browser — no build step, no dependencies, +Open any example file directly in a browser — no build step, no dependencies, no network requests. +### Two layers, one set of tokens + +| Deliverable | Import | Default accent | +|---|---|---| +| Presentation, PPT template, report | `+ css/slides.css` | **amber** | +| Landing page, SaaS site, portfolio | `+ css/web.css` | **violet** | + +Both share `tokens.css`, so a deck and a site for the same client stay in family. +See [`FAVORITES.md`](FAVORITES.md) for what the web layer adds and the three +rules that differ (gradients, product-UI heroes, dark pages). + --- ## Using it on the web @@ -72,15 +86,16 @@ Then compose from the existing classes: ### Re-theming -Six accent families ship with the system. Swap the class on `` — or on any +Nine accent families ship with the system. Swap the class on `` — or on any container for a scoped change — and everything downstream follows, including -charts, buttons, icon chips and section panels. +gradients, charts, buttons, icon chips and section panels. ```html - + + ``` -Both example files include a live theme switcher so you can see this happen. +Every example file includes a live theme switcher so you can see this happen. ### Dark blocks diff --git a/design-system/css/web.css b/design-system/css/web.css new file mode 100644 index 0000000..328593a --- /dev/null +++ b/design-system/css/web.css @@ -0,0 +1,482 @@ +/* ========================================================================== + Corporate Deck Design System — Web layer + Patterns from the favourites set (see FAVORITES.md) that the deck layer + does not cover: gradient heroes, wave dividers, device mockups, trust bars, + tinted feature cards, ghost step rails, pricing, FAQ, marquee, CTA bands. + Requires: tokens.css, base.css, components.css + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Hero surfaces + -------------------------------------------------------------------------- */ + +.hero-gradient { + position: relative; + background: var(--gradient-hero); + color: #fff; + isolation: isolate; + overflow: hidden; +} +.hero-gradient .h1, +.hero-gradient .display, +.hero-gradient .h2 { color: #fff; } +.hero-gradient .eyebrow { color: rgba(255, 255, 255, 0.75); } + +.hero-sheen { background: var(--gradient-sheen); } + +/* Soft blurred orb — the glow behind hero content. Ornamental. */ +.orb { + position: absolute; + border-radius: var(--radius-pill); + background: var(--glow-accent); + filter: blur(28px); + pointer-events: none; + z-index: -1; +} + +/* Wave divider. Sits at the bottom of a coloured hero and curves into the + next section. Give it the *next* section's background via `color`. */ +.wave { + display: block; + width: 100%; + height: clamp(40px, 6vw, 90px); + color: var(--surface-page); +} +.wave svg { display: block; width: 100%; height: 100%; } +.wave path { fill: currentColor; } + +/* -------------------------------------------------------------------------- + Badge pill above a headline — "NEW · Introducing X" + -------------------------------------------------------------------------- */ + +.badge-pill { + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: 0.35rem 0.35rem 0.35rem 0.85rem; + border-radius: var(--radius-pill); + background: var(--accent-50); + color: var(--accent-700); + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); +} +.badge-pill__flag { + padding: 0.2rem 0.6rem; + border-radius: var(--radius-pill); + background: var(--accent-500); + color: var(--accent-on); + font-size: 0.6875rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.hero-gradient .badge-pill { + background: rgba(255, 255, 255, 0.16); + color: #fff; +} + +/* Headline word with an accent underline rather than an accent fill. */ +.underlined { + color: var(--accent-600); + text-decoration: underline; + text-decoration-thickness: 0.09em; + text-underline-offset: 0.12em; +} + +/* Gradient-filled headline word. Use once per page at most. */ +.gradient-text { + background: var(--gradient-text); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +/* -------------------------------------------------------------------------- + Device mockup frames — the product-UI hero visual + -------------------------------------------------------------------------- */ + +.device { + position: relative; + background: var(--ink-900); + box-shadow: var(--shadow-xl); + overflow: hidden; +} + +.device--phone { + border-radius: 2.25rem; + padding: 0.55rem; + aspect-ratio: 1179 / 2556; + max-width: 300px; +} +.device--phone::before { /* dynamic island */ + content: ""; + position: absolute; + top: 1.1rem; left: 50%; + transform: translateX(-50%); + width: 32%; height: 1.1rem; + border-radius: var(--radius-pill); + background: var(--ink-900); + z-index: 2; +} + +.device--browser { + border-radius: var(--radius-md); + padding: 1.9rem 0.4rem 0.4rem; +} +.device--browser::before { /* traffic lights */ + content: ""; + position: absolute; + top: 0.72rem; left: 0.85rem; + width: 2.6rem; height: 0.45rem; + border-radius: var(--radius-pill); + background: + radial-gradient(circle 3.5px at 3.5px 50%, var(--ink-600) 100%, transparent 0) 0 0/1rem 100% no-repeat, + radial-gradient(circle 3.5px at 3.5px 50%, var(--ink-600) 100%, transparent 0) 0.85rem 0/1rem 100% no-repeat, + radial-gradient(circle 3.5px at 3.5px 50%, var(--ink-600) 100%, transparent 0) 1.7rem 0/1rem 100% no-repeat; +} + +.device__screen { + border-radius: inherit; + overflow: hidden; + background: var(--surface-page); + height: 100%; +} +.device--browser .device__screen { border-radius: var(--radius-sm); } +.device__screen img { width: 100%; height: 100%; object-fit: cover; object-position: top; } + +/* Small stat/notification card that floats over a mockup. */ +.float-card { + position: absolute; + z-index: 3; + background: var(--surface-page); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: var(--space-3) var(--space-4); + font-size: var(--fs-caption); + color: var(--fg-body); + max-width: 200px; +} +.float-card__value { + font-family: var(--font-display); + font-size: 1.125rem; + font-weight: var(--fw-bold); + color: var(--fg-strong); + line-height: 1.2; +} + +/* Floating label chip — the tags pinned around a portrait. */ +.pin { + position: absolute; + z-index: 3; + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: 0.45rem 0.9rem; + border-radius: var(--radius-pill); + background: var(--surface-page); + box-shadow: var(--shadow-md); + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); + color: var(--fg-strong); + white-space: nowrap; +} +.pin--ink { background: var(--ink-900); color: #fff; } +.pin::before { + content: ""; + width: 7px; height: 7px; + border-radius: var(--radius-pill); + background: var(--accent-500); +} + +/* -------------------------------------------------------------------------- + Trust bar — "Loved by innovative teams" + logo row + -------------------------------------------------------------------------- */ + +.trustbar { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-8); + flex-wrap: wrap; +} +.trustbar__label { + font-size: var(--fs-small); + font-weight: var(--fw-medium); + color: var(--fg-muted); +} +.trustbar__logos { + display: flex; + align-items: center; + gap: var(--space-8); + flex-wrap: wrap; + list-style: none; +} +.trustbar__logos li { + font-family: var(--font-display); + font-size: 1.125rem; + font-weight: var(--fw-bold); + color: var(--ink-400); + letter-spacing: -0.01em; +} + +/* -------------------------------------------------------------------------- + Tinted feature cards — each card a different soft tint + -------------------------------------------------------------------------- */ + +.tint-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr)); } + +.tint-card { + padding: var(--space-5); + border-radius: var(--radius-lg); + background: var(--tint, var(--accent-50)); +} +.tint-card__title { font-family: var(--font-display); font-weight: var(--fw-bold); color: var(--fg-strong); margin-top: var(--space-3); } +.tint-card__body { font-size: var(--fs-small); color: var(--fg-body); margin-top: var(--space-2); } + +/* Four house tints. They are *neutral supports*, not second accents — each is + desaturated far enough that the accent still owns every emphasis on the page. */ +.tint-1 { --tint: var(--accent-50); } +.tint-2 { --tint: #FFF1E8; } +.tint-3 { --tint: #E9F7EF; } +.tint-4 { --tint: #EAF2FF; } + +@media (max-width: 900px) { .tint-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 560px) { .tint-grid { grid-template-columns: 1fr; } } + +/* -------------------------------------------------------------------------- + Step rail — oversized ghost numerals down a dotted centre line + Alternating sides, copy one side / visual the other. + -------------------------------------------------------------------------- */ + +.rail { position: relative; display: grid; gap: var(--space-10); } + +.rail::before { /* the dotted spine */ + content: ""; + position: absolute; + top: 0; bottom: 0; left: 50%; + border-left: 2px dotted var(--ink-200); +} + +.rail__row { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: var(--space-6); + align-items: center; +} + +.rail__num { + font-family: var(--font-display); + font-size: clamp(3.5rem, 7vw, 6rem); + font-weight: var(--fw-black); + line-height: 1; + letter-spacing: -0.04em; + color: var(--ink-100); + background: var(--surface-page); + padding-block: var(--space-3); + font-variant-numeric: tabular-nums; + text-align: center; +} +.rail__num--label::before { + content: "STEP"; + display: block; + font-size: 0.75rem; + font-weight: var(--fw-semibold); + letter-spacing: 0.18em; + color: var(--ink-300); +} + +/* Every child needs an explicit order, or the numeral keeps its default 0 and + lands in the first column — pushing the visual into the narrow centre one. */ +.rail__row--flip .rail__visual { order: 1; } +.rail__row--flip .rail__num { order: 2; } +.rail__row--flip .rail__copy { order: 3; } + +@media (max-width: 800px) { + .rail::before { left: 18px; } + .rail__row { grid-template-columns: auto 1fr; gap: var(--space-4); } + .rail__row .rail__num { order: 1; font-size: 2.5rem; } + .rail__row .rail__copy { order: 2; } + .rail__row .rail__visual { order: 3; grid-column: 2; } +} + +/* -------------------------------------------------------------------------- + Pricing + -------------------------------------------------------------------------- */ + +.price-grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); align-items: start; } + +.price-card { + position: relative; + padding: var(--space-6); + border: 1px solid var(--ink-100); + border-radius: var(--radius-lg); + background: var(--surface-page); +} +.price-card--featured { + border-color: var(--accent-500); + border-width: 2px; + box-shadow: var(--shadow-lg); +} +.price-card__flag { + position: absolute; + top: 0; left: 50%; + transform: translate(-50%, -50%); + padding: 0.3rem 0.9rem; + border-radius: var(--radius-pill); + background: var(--accent-500); + color: var(--accent-on); + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); + white-space: nowrap; +} +.price-card__amount { + font-family: var(--font-display); + font-size: 2.75rem; + font-weight: var(--fw-black); + letter-spacing: -0.035em; + color: var(--fg-strong); + line-height: 1; +} +.price-card__period { font-size: var(--fs-small); color: var(--fg-muted); } + +.checklist { list-style: none; display: grid; gap: var(--space-3); font-size: var(--fs-small); } +.checklist li { display: flex; gap: var(--space-3); align-items: flex-start; } +.checklist li::before { + content: ""; + flex: none; + width: 18px; height: 18px; + margin-top: 2px; + border-radius: var(--radius-pill); + background: var(--accent-500); + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m5 13 4 4L19 7' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/12px no-repeat, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m5 13 4 4L19 7' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/12px no-repeat, + linear-gradient(#000 0 0); + mask-composite: exclude; +} + +/* -------------------------------------------------------------------------- + FAQ accordion + -------------------------------------------------------------------------- */ + +.faq { display: grid; gap: var(--space-3); } + +.faq__item { + border: 1px solid var(--ink-100); + border-radius: var(--radius-md); + background: var(--surface-page); + padding: var(--space-4) var(--space-5); +} +.faq__item[open] { border-color: var(--accent-300); } + +.faq__q { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + cursor: pointer; + list-style: none; + font-family: var(--font-display); + font-size: var(--fs-small); + font-weight: var(--fw-semibold); + color: var(--fg-strong); +} +.faq__q::-webkit-details-marker { display: none; } +.faq__q::after { + content: "+"; + font-size: 1.25rem; + font-weight: var(--fw-regular); + color: var(--accent-600); + line-height: 1; +} +.faq__item[open] .faq__q::after { content: "\2212"; } +.faq__a { font-size: var(--fs-small); color: var(--fg-body); margin-top: var(--space-3); } + +/* -------------------------------------------------------------------------- + Marquee strip — the scrolling keyword band + -------------------------------------------------------------------------- */ + +.marquee { + overflow: hidden; + background: var(--accent-500); + color: var(--accent-on); + padding-block: var(--space-3); +} +.marquee__track { + display: flex; + gap: var(--space-6); + width: max-content; + animation: marquee-scroll 26s linear infinite; + font-family: var(--font-display); + font-size: 1.125rem; + font-weight: var(--fw-semibold); +} +.marquee__track > span { display: flex; align-items: center; gap: var(--space-6); } +.marquee__track > span::after { content: "\2726"; opacity: 0.6; } + +@keyframes marquee-scroll { to { transform: translateX(-50%); } } +@media (prefers-reduced-motion: reduce) { + .marquee__track { animation: none; } +} + +/* -------------------------------------------------------------------------- + CTA band — full-width accent slab with copy left, action right + -------------------------------------------------------------------------- */ + +.cta-band { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-6); + flex-wrap: wrap; + padding: var(--space-7) var(--space-8); + border-radius: var(--radius-xl); + background: var(--accent-500); + color: var(--accent-on); +} +.cta-band--gradient { background: var(--gradient-hero); color: #fff; } +.cta-band .h3 { color: inherit; } + +/* Newsletter input group */ +.field-group { + display: flex; + gap: var(--space-2); + background: var(--surface-page); + border: 1px solid var(--ink-200); + border-radius: var(--radius-pill); + padding: 0.3rem 0.3rem 0.3rem 1.1rem; + max-width: 420px; +} +.field-group input { + flex: 1; + min-width: 0; + border: 0; + background: transparent; + font: inherit; + font-size: var(--fs-small); + color: var(--fg-strong); +} +.field-group input:focus { outline: none; } +.field-group:focus-within { border-color: var(--accent-500); } + +/* -------------------------------------------------------------------------- + Dark page variant + The favourites include a fully dark portfolio. `on-dark` flips foreground + roles; these few overrides carry the web components across with it. + -------------------------------------------------------------------------- */ + +.on-dark .card, +.on-dark .price-card, +.on-dark .faq__item, +.on-dark .float-card { + background: var(--ink-800); + border-color: rgba(255, 255, 255, 0.09); +} +.on-dark .tint-card { background: rgba(255, 255, 255, 0.05); } +.on-dark .rail__num { background: var(--ink-900); color: rgba(255, 255, 255, 0.12); } +.on-dark .rail::before { border-color: rgba(255, 255, 255, 0.16); } +.on-dark .trustbar__logos li { color: rgba(255, 255, 255, 0.45); } +.on-dark .badge-pill { background: rgba(255, 255, 255, 0.1); color: var(--accent-300); } +.on-dark .field-group { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.14); } +.on-dark .field-group input { color: #fff; } +.on-dark .underlined { color: var(--accent-300); } diff --git a/design-system/examples/landing.html b/design-system/examples/landing.html new file mode 100644 index 0000000..3374f77 --- /dev/null +++ b/design-system/examples/landing.html @@ -0,0 +1,297 @@ + + + + + +Corporate Deck Design System — Landing Page (Favourites) + + + + + + + + + +
+
+ + +
+
+ New AI-powered productivity +

One platform.
Endless possibilities.

+

+ Flowora brings your tasks, projects and team together — so you can + focus on what actually matters. +

+ +
+ +
+ +
+
Dashboard UI
+
+
+ 99.9% + Uptime this quarter +
+
+
+
+ + +
+ + + + + +
+
+ Loved by innovative teams +
    +
  • Slack
  • Notion
  • Dropbox
  • Zoom
  • Airbnb
  • +
+
+
+ + +
+
+
+
+ Features +

Everything you need to get work done

+

Designed to boost productivity and drive real results.

+ +
+
+
+ +

Task management

+

Organise tasks, set deadlines, track progress.

+
+
+ +

Team collaboration

+

Communicate and work together in real time.

+
+
+ +

Smart automation

+

Automate repetitive work and save hours.

+
+
+ +

Analytics

+

Insights that help you make better calls.

+
+
+
+
+
+ + +
+
+
+ How it works +

Three steps to shipping

+
+
+
+
+
+

Connect your tools

+

Bring your existing stack in with one click. Nothing to migrate, nothing to rebuild.

+ Learn more → +
+
01
+
Illustration
+
+
+
+

Set the cadence

+

Owners, dates, and a weekly rhythm the whole team can actually follow.

+ Learn more → +
+
02
+
Illustration
+
+
+
+

Ship and measure

+

Every change tracked against the number it was meant to move.

+ Learn more → +
+
03
+
Illustration
+
+
+
+
+ + +
+
+
+
+
12K+Active users
+
240K+Tasks completed
+
99.9%Uptime
+
4.9/5User rating
+
+
+
+
+ + +
+
+
+ Pricing +

Simple, honest plans

+
+
+
+
+

Starter

+

For small teams

+
$0 / month
+
  • Up to 5 users
  • Core features
  • Community support
+ Get started +
+ +
+

Enterprise

+

For large organisations

+
Custom
+
  • Custom workflows
  • Dedicated support
  • SLA & security
+ Contact sales +
+
+
+
+ + +
+
+
+ Questions +

Everything you need to know

+
+
+
+ Is there a free plan? +

Yes — up to five users, with the core feature set and no time limit.

+
+
+ Can I use it without writing code? +

Entirely. Everything is configured from the interface.

+
+
+ Do you offer migration help? +

On Pro and Enterprise we migrate your existing projects for you.

+
+
+ What does support look like? +

Community on Starter, same-day on Pro, and a named contact on Enterprise.

+
+
+
+
+ + +
+
+
+
+

Ready to boost your team's productivity?

+

Join thousands of teams already using Flowora.

+
+ Start free trial +
+
+
+ + +
+
+
+ Flowora +

All-in-one platform to manage tasks, teams and projects.

+
+ + +
+
+
+ Switch accent theme +
+ + + + + + +
+
+
+
+ + + + + diff --git a/design-system/tokens/tokens.css b/design-system/tokens/tokens.css index b379839..d044dfb 100644 --- a/design-system/tokens/tokens.css +++ b/design-system/tokens/tokens.css @@ -132,6 +132,14 @@ --ease-enter: cubic-bezier(0, 0, 0.2, 1); --ease-exit: cubic-bezier(0.4, 0, 1, 1); + /* --- Gradient & glow -------------------------------------------------- + Decks stay flat; the web-landing family uses these for hero surfaces and + the soft orbs behind content. Never as a brand colour — see the brief. */ + --gradient-hero: linear-gradient(135deg, var(--accent-600) 0%, var(--accent-500) 55%, var(--accent-300) 100%); + --gradient-sheen: linear-gradient(135deg, var(--accent-50) 0%, var(--surface-page) 60%); + --gradient-text: linear-gradient(90deg, var(--accent-500), var(--accent-300)); + --glow-accent: radial-gradient(circle, color-mix(in srgb, var(--accent-500) 38%, transparent) 0%, transparent 70%); + /* --- Layout ---------------------------------------------------------- */ --container-max: 1200px; --section-py: 6rem; @@ -178,6 +186,27 @@ --accent-on: #FFFFFF; } +/* Added from the favourites set — the web-landing family. Violet is the + signature there; lime is the cream-ground outlier. */ + +.theme-violet { + --accent-50: #F3EEFF; --accent-100: #E2D6FF; --accent-300: #A98BFF; + --accent-500: #7C4DFF; --accent-600: #6534E0; --accent-700: #4B24A8; + --accent-on: #FFFFFF; +} + +.theme-indigo { + --accent-50: #EDF0FF; --accent-100: #D8DFFF; --accent-300: #93A5FF; + --accent-500: #4059F0; --accent-600: #2E42CC; --accent-700: #1F2E96; + --accent-on: #FFFFFF; +} + +.theme-lime { + --accent-50: #F7FBE8; --accent-100: #EAF5C2; --accent-300: #D8EC7E; + --accent-500: #C3DE4A; --accent-600: #A3BE2C; --accent-700: #758A1A; + --accent-on: #14181F; +} + /* ========================================================================== Dark surface context Any block that sits on ink flips its foreground roles here, so components diff --git a/design-system/tokens/tokens.json b/design-system/tokens/tokens.json index b105b15..80c8ecf 100644 --- a/design-system/tokens/tokens.json +++ b/design-system/tokens/tokens.json @@ -61,6 +61,35 @@ "600": "#C64720", "700": "#993518", "$signature": "500" + }, + "violet": { + "$comment": "Signature of the favourites set — the web-landing family.", + "50": "#F3EEFF", + "100": "#E2D6FF", + "300": "#A98BFF", + "500": "#7C4DFF", + "600": "#6534E0", + "700": "#4B24A8", + "$signature": "500" + }, + "indigo": { + "50": "#EDF0FF", + "100": "#D8DFFF", + "300": "#93A5FF", + "500": "#4059F0", + "600": "#2E42CC", + "700": "#1F2E96", + "$signature": "500" + }, + "lime": { + "$comment": "Pairs with the cream ground, not white. Dark text on top.", + "50": "#F7FBE8", + "100": "#EAF5C2", + "300": "#D8EC7E", + "500": "#C3DE4A", + "600": "#A3BE2C", + "700": "#758A1A", + "$signature": "500" } }, From 07aec0c9c567a30dd9f7481a0baa27de2fa5f6a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 14:09:30 +0000 Subject: [PATCH 03/18] Add intake: six questions that scope the work before designing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing gets designed until six questions are answered, and each answer maps to a fixed setting — so the same answers always produce the same starting point, whoever is designing. The questions decide layer and section set, accent and ground, whether brand colours override the recommendation, the CTA treatment, the media treatment, and light/dark plus output medium. Three deck-only follow-ups cover length, presenter and whether anyone edits it afterwards. The output is deliberately a scope rather than a restatement of the system. It names the exact files to import, the exact classes to compose from, and the single doc section to read, and says explicitly not to re-read or regenerate the tokens and components. Producing one page should not cost a pass over the whole system every time, and parallel CSS written from scratch is how a house style drifts. examples/picker.html runs the questions interactively, re-themes live as the accent answer changes, and emits the scope block ready to paste. It guards the one contradictory combination: choosing a dark ground for a deck self-corrects to light with a note, since dark is web-only here. Verified in Chromium: all example pages across all nine themes with no page or console errors and no horizontal overflow; the picker driven end to end through all six answers produces the expected scope, and the deck+dark guard fires. Fixed a usability bug the test caught, where the sticky spec panel overlaid the last question and swallowed its clicks. --- design-system/DESIGN-BRIEF.md | 6 + design-system/INTAKE.md | 188 +++++++++++++++++++++ design-system/README.md | 15 ++ design-system/examples/picker.html | 253 +++++++++++++++++++++++++++++ 4 files changed, 462 insertions(+) create mode 100644 design-system/INTAKE.md create mode 100644 design-system/examples/picker.html diff --git a/design-system/DESIGN-BRIEF.md b/design-system/DESIGN-BRIEF.md index b242983..36bbfe8 100644 --- a/design-system/DESIGN-BRIEF.md +++ b/design-system/DESIGN-BRIEF.md @@ -4,6 +4,12 @@ to keep. Anything not specified here is open to interpretation; anything specified here is not. +> **Before designing anything, run the intake.** Six questions in +> [`INTAKE.md`](INTAKE.md) (or click through `examples/picker.html`) decide the +> layer, accent, ground, CTA, imagery and mode — and produce a *scope* naming the +> exact files and classes for the job, so nobody re-reads the whole system to +> build one page. + This brief was distilled from a 32-template reference set of corporate presentation and web designs. It describes what those designs have in common — the rules that make them read as one house style rather than 32 unrelated files. diff --git a/design-system/INTAKE.md b/design-system/INTAKE.md new file mode 100644 index 0000000..bc096c5 --- /dev/null +++ b/design-system/INTAKE.md @@ -0,0 +1,188 @@ +# Intake — Ask Before You Design + +Nobody starts a design in this system until these questions are answered. The +point is not paperwork: **every question below changes a concrete setting**, and +the mapping is fixed, so two different designers given the same answers produce +the same starting point. + +Run it as a conversation, not a form. Six questions, two minutes. + +> Interactive version: [`examples/picker.html`](examples/picker.html) — answer on +> screen, get a filled brief and a live preview to paste into a designer's or an +> AI tool's prompt. + +--- + +## The six questions + +### 1. What are you making? + +> *Deck · Landing page · Report · Portfolio · Pitch deck · Product site* + +**Decides: which layer, and the section set.** + +| Answer | Layer | Starting sections | +|---|---|---| +| Presentation / deck | `slides.css` | The 14-step deck sequence (brief §8) | +| Report / whitepaper | `slides.css`, print styles | Cover, contents, sections, charts, closing | +| Pitch deck | `slides.css` | Cover, problem, solution, market, model, traction, team, ask | +| Landing page | `web.css` | Hero, trust bar, features, how-it-works, stats, pricing, FAQ, CTA | +| Product site | `web.css` | Hero with device mockup, features, integrations, pricing, FAQ, CTA | +| Portfolio | `web.css` | Hero with portrait, selected work, services, process, testimonials, contact | + +--- + +### 2. Who is the audience, and what should they feel in the first five seconds? + +> *Trusted · Energised · Reassured · Impressed · Curious* + +**Decides: the accent family and the ground.** + +| Feeling | Accent | Ground | +|---|---|---| +| Trusted, established, safe | navy or indigo | white | +| Serious about money | indigo or lime | white / cream | +| Energised, optimistic, warm | amber or coral | white | +| Modern, technical, premium | violet or electric | white, or ink for a dark page | +| Calm, healthy, sustainable | green or teal | off-white | +| Bold, creative, unmissable | coral or lime | cream | + +If the answer is "I don't know", ask what their closest competitor looks like and +go one step away from it. + +--- + +### 3. Do you already have brand colours, a logo, or fonts? + +> *Yes, locked · Yes, but flexible · No, choose for me* + +**Decides: whether Q2's answer is a recommendation or a rule.** + +- **Locked** — map the brand colour to the nearest accent family and override the + `--accent-*` ramp with the real values. Keep every other token. +- **Flexible** — use Q2's recommendation, show the brand colour as an alternate. +- **None** — Q2 decides outright. + +Whichever applies, one accent only. A brand with two colours gets the dominant +one as the accent and the second demoted to a neutral support tint. + +--- + +### 4. What is the one thing you want the reader to do? + +> *Book a call · Buy · Sign up / trial · Approve the plan · Get in touch · Just understand* + +**Decides: the CTA treatment and how often it repeats.** + +| Action | Treatment | +|---|---| +| Book a call / Get in touch | `.btn--primary` in the hero, `.cta-band` before the footer | +| Buy / Sign up / trial | Hero CTA + `.price-grid` with a featured tier + closing `.cta-band` | +| Approve the plan | No CTA band. Closing slide states the decision and the date needed. | +| Just understand | No CTA. Ends on a summary or contact slide. | + +One primary action per document. A second competing action halves both. + +--- + +### 5. What imagery do you actually have? + +> *Photography · Product screenshots · Illustration · Logos only · Nothing yet* + +**Decides: media treatment — and this is the question most often skipped, then +regretted.** + +| Have | Use | +|---|---| +| Photography | `.media--notched` / `.media--circle`, accent wash on section covers | +| Product screenshots | `.device--phone` / `.device--browser` with `.float-card` | +| Illustration | Flat vector on `.tint-card` grounds; no photo masks | +| Logos only | `.trustbar`, big type, `.deco` geometry carrying the visual load | +| Nothing yet | Type-led layout + geometry. **Do not** design around placeholders you cannot fill. | + +--- + +### 6. Light or dark — and where does this live? + +> *Light · Dark · Both · Screen only · Printed · Projected* + +**Decides: surface, contrast floor, and export.** + +- **Dark** is available on web only (portfolio, product). Decks stay light. +- **Projected** → raise contrast, no hairlines under 2px, no body text under 15pt. +- **Printed** → light ground, CMYK-safe accent, hairlines at 0.75pt minimum. +- **Both** → build light first, then apply `.on-dark`; never design them separately. + +--- + +## Three more, only if it is a deck + +7. **How long, and how long do you have to present it?** — roughly one slide per + minute of talking. If the answers don't reconcile, cut sections now, not later. +8. **Who presents it, and do they need speaker notes?** +9. **Will anyone edit it after you?** — if yes, ship a `.potx` with proper slide + master layouts, not a finished `.pptx`. + +--- + +## The output is a *scope*, not a restatement + +The answers collapse into a targeted work order that names the exact files and +classes for this one job — and explicitly rules out everything else. + +This matters for cost as much as for quality. The system is already written; a +handover that re-reads or regenerates the tokens and components on every job +pays for the same work repeatedly. **The intake output exists to stop that.** + +> **Deliverable:** landing page · **Layer:** web · **Accent:** violet (flexible) · +> **Ground:** white · **Primary action:** start free trial → hero CTA + featured +> pricing tier + closing CTA band · **Imagery:** product screenshots in browser and +> phone frames · **Mode:** light, screen · **Sections:** hero, trust bar, features, +> how-it-works, stats, pricing, FAQ, CTA. +> +> **━━ SCOPE — touch only what is listed ━━** +> +> **Import (already written, do not modify):** +> `tokens/tokens.css`, `css/base.css`, `css/components.css`, `css/web.css` +> +> **Write exactly one new file:** the page markup. +> **Do not** re-read, regenerate or restate the tokens, the components, or the +> rest of the system. They exist and they work. +> +> **Classes to compose from:** `.hero-gradient` `.orb` `.wave` `.badge-pill` +> `.trustbar` `.tint-grid` `.rail` `.stat-row` `.price-grid` `.faq` `.cta-band` +> `.device--browser` `.device--phone` `.float-card` +> +> **Theme:** `` +> +> **Read only:** `FAVORITES.md` §"Rules specific to this set". Skip the rest of +> the docs — that line is the whole brief needed. +> +> **Rules that still apply:** one accent hue; eyebrow label above each headline; +> one two-tone headline at the top; 20px card radius; charts use the accent as +> series 1 and greys after. + +### Why the scope block is worded that way + +| Line | What it prevents | +|---|---| +| "already written, do not modify" | Rewriting tokens that other work depends on | +| "write exactly one new file" | Sprawl across the system for a single page | +| "do not re-read … the rest of the system" | Re-processing thousands of lines to build one page | +| "classes to compose from" | Inventing parallel CSS that drifts from the house style | +| "read only §X" | Consuming the full brief when six lines would do | + +A designer who needs more than this list should ask for it, rather than reading +everything by default on the chance it matters. + +--- + +## Why refuse to start without this + +Every one of these questions maps to a decision that is expensive to reverse +late. Choosing an accent after the photography is picked, or discovering at +handover that the deck will be projected, means redoing work that was already +approved. Two minutes here is the cheapest part of the project. + +If someone will not answer Q5, assume "nothing yet" and design type-led. That +assumption is recoverable. Designing around imagery that never arrives is not. diff --git a/design-system/README.md b/design-system/README.md index db8a0c2..46de0d7 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -15,6 +15,7 @@ corners, oversized section numerals, and soft geometric ornament. ``` design-system/ +├── INTAKE.md ← START HERE — six questions asked before any design ├── DESIGN-BRIEF.md ← hand this to any designer or AI tool ├── FAVORITES.md ← the web-landing reference set + its extra rules ├── tokens/ @@ -28,11 +29,25 @@ design-system/ ├── powerpoint/ │ └── SPEC.md ← pt sizes and inch positions for a 13.333" × 7.5" slide └── examples/ + ├── picker.html ← the intake questions, interactive → outputs a scope ├── index.html ← corporate web page (deck layer) ├── deck.html ← 14-slide deck └── landing.html ← SaaS landing page (web layer) ``` +## Starting a new design + +Don't pick a layout first — **answer the six questions in +[`INTAKE.md`](INTAKE.md)**, or run [`examples/picker.html`](examples/picker.html) +and click through them. Each answer maps to a fixed setting, so the same answers +always produce the same starting point. + +The output is a **scope**, not a restatement: it names the exact files to import, +the exact classes to compose from, the one doc section to read — and says +explicitly not to re-read or regenerate the rest of the system. That keeps each +new page or deck cheap to produce and stops parallel CSS drifting away from the +house style. + Open any example file directly in a browser — no build step, no dependencies, no network requests. diff --git a/design-system/examples/picker.html b/design-system/examples/picker.html new file mode 100644 index 0000000..6a3aea6 --- /dev/null +++ b/design-system/examples/picker.html @@ -0,0 +1,253 @@ + + + + + +Design Intake — Corporate Deck Design System + + + + + + + +
+ +
+ Before we design anything +

Six questions

+

+ Every answer changes a concrete setting. Answer these and you get a spec you + can hand to any designer — or paste into an AI tool — and get back something + that looks like it belongs to the rest of your work. +

+
+ +
+ +
+
+
+ Your spec +
+
+
+ 0 of 6 +
+
Answer the questions above and your spec appears here.
+
+ + +
+
+
+ +
+ + + + From 14bcfbe75144c2ae88e6b4bb08241669304a0464 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 14:21:47 +0000 Subject: [PATCH 04/18] Add portfolio page built from a real intake run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs the intake end to end and keeps the result as a worked example: portfolio site, trusted/established, buy or sign up, illustration. Those answers resolve to the web layer on indigo, an illustration-led hero, work grid, services, process rail, three fixed-price packages with a featured tier, testimonial, FAQ and a gradient CTA band. The run also surfaced the collision the intake exists to catch. The portfolio archetype normally opens on a portrait hero with label pins, but the imagery answer was illustration with no photography. The imagery answer wins, so the portrait and every photo mask are dropped and inline SVG on tinted grounds takes their place. Left to a build, that would have come back as a revision round. Written as one file, as the scope block instructs: imports the system, composes from existing classes, and adds five lines of scoped CSS for a hero-specific grid fix. Nothing under css/ or tokens/ was touched — the hero column is narrower than .stat-row's auto-fit minimum, so the third figure is pinned locally rather than loosening the component for every other page that uses it. Verified in Chromium: no page or console errors, no horizontal overflow at 1440px or 390px, and the other example pages still clean across all nine themes. --- design-system/README.md | 17 +- design-system/examples/portfolio.html | 361 ++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 design-system/examples/portfolio.html diff --git a/design-system/README.md b/design-system/README.md index 46de0d7..4adee85 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -32,7 +32,8 @@ design-system/ ├── picker.html ← the intake questions, interactive → outputs a scope ├── index.html ← corporate web page (deck layer) ├── deck.html ← 14-slide deck - └── landing.html ← SaaS landing page (web layer) + ├── landing.html ← SaaS landing page (web layer) + └── portfolio.html ← built from a real intake run (see below) ``` ## Starting a new design @@ -48,6 +49,20 @@ explicitly not to re-read or regenerate the rest of the system. That keeps each new page or deck cheap to produce and stops parallel CSS drifting away from the house style. +### A worked example + +`examples/portfolio.html` was built from a real intake run — *portfolio site · +trusted/established · buy or sign up · illustration* — and shows what the process +catches. The portfolio archetype normally opens on a **portrait hero with label +pins**, but the imagery answer was *illustration, no photography*. The imagery +answer wins: no portrait, no photo masks, inline SVG on tinted grounds instead. +That collision would have surfaced as a revision round; here it surfaced before +anything was drawn. + +The whole page is one file. It imports the system, composes from existing +classes, and adds five lines of scoped CSS for one hero-specific grid fix — +nothing in `css/` or `tokens/` was touched. + Open any example file directly in a browser — no build step, no dependencies, no network requests. diff --git a/design-system/examples/portfolio.html b/design-system/examples/portfolio.html new file mode 100644 index 0000000..1f93058 --- /dev/null +++ b/design-system/examples/portfolio.html @@ -0,0 +1,361 @@ + + + + + +Portfolio — Corporate Deck Design System + + + + + + + + +
+ +
+ + +
+
+
+ +
+ Open Two slots for Q4 +

Design that earns trust, not just attention.

+

+ Twelve years designing interfaces for finance, health and public-sector + teams — where getting it wrong is expensive and being clear matters more + than being clever. +

+ +
+
12Years
+
80+Projects
+
94%Repeat clients
+
+
+ +
+
+ + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+
+ Trusted by teams at +
    +
  • Halden
  • Northbrook
  • Kestrel
  • Aven
  • Pulse
  • +
+
+
+ + +
+
+
+ Selected work +

Recent projects

+
+
+
+
+
+ + + + + + + + +
+
+

Kestrel — treasury dashboard

+

Product design · 2025

+
+
+ +
+
+ + + + + + + + + + + + +
+
+

Aven — onboarding rebuild

+

UX · 2024

+
+
+
+
+
+ + +
+
+
+ What I do +

Four ways I can help

+
+
+
+
+ +

Product design

+

End-to-end interface work, from flows to shipped screens.

+
+
+ +

Design systems

+

Tokens, components and the documentation that keeps them used.

+
+
+ +

Research & audit

+

An honest read on what is costing you conversions, with fixes ranked.

+
+
+ +

Implementation

+

Front-end build so the design ships as drawn, not as interpreted.

+
+
+
+
+ + +
+
+
+ How I work +

Three steps, no surprises

+
+
+
+
+
+

Understand

+

A week reading your data and talking to your users. You get a written diagnosis before anyone opens a design tool.

+
+
01
+
+
+ + + + + + + + +
+
+
+
+
+

Design

+

Flows first, then screens. Reviewed weekly against the diagnosis, so scope creep has somewhere to be caught.

+
+
02
+
+
+ + + + + + + + +
+
+
+
+
+

Ship

+

Built and handed over with the components documented, so your team can keep going without me.

+
+
03
+
+
+ + + + + + + +
+
+
+
+
+
+ + +
+
+
+ Packages +

Fixed scope, fixed price

+

No hourly billing and no change-request theatre. Pick a package, get a start date.

+
+
+
+
+

Audit

+

One product, two weeks

+
£4,500
+
    +
  • Heuristic and data review
  • +
  • Five user interviews
  • +
  • Ranked fix list with effort estimates
  • +
+ Book an audit +
+ + + +
+

Retained

+

Ongoing, monthly

+
£6,000 / month
+
    +
  • Two days a week
  • +
  • Design system ownership
  • +
  • Pause or stop with 30 days' notice
  • +
+ Enquire +
+
+
+
+ + +
+
+
+ Marin gave us the diagnosis before the designs, which nobody else did. + Six weeks later the drop-off we had lived with for two years was gone. +
+ Priya RamanHead of Product, Kestrel +
+
+
+
+ How soon can you start? +

Usually three to four weeks out. Two slots are open for Q4.

+
+
+ Do you work with in-house teams? +

Mostly, yes — the retained package exists for exactly that.

+
+
+ What if the audit says don't build it? +

Then that is the deliverable, and it has saved you the sprint fee.

+
+
+
+
+ + +
+
+
+
+

Two slots left for Q4

+

Tell me what is stuck. You'll get a straight answer within two days.

+
+ Book a call +
+
+
+ +
+
+
+ Marin Ellis +

Product design for teams where clarity matters more than novelty.

+
+ + +
+
+

© 2026 Marin Ellis · hello@marinellis.example

+
+
+ + + From f0bf4ad9bc35ea9bd38f2261f1568960b4d22aeb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 14:28:36 +0000 Subject: [PATCH 05/18] Add photo routes to intake: upload your own, or source to brief MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Question 5 gains two answers. Both end in the same place — masked to a house treatment, never dropped in raw. Upload my own: drop a file into the picker and it renders straight away in all four treatments (notched, circle, accent wash, dark scrim with type over it) in the current accent theme. Read with FileReader, so the file stays in the browser and is never uploaded. Once the dimensions are known the picker warns if the resolution is under the floor for the intended use, or if the crop is wrong for a hero. Find one for me: the picker builds a search brief from the other answers — subject from the deliverable, mood from what the audience should feel, plus the composition constraints the system actually needs, which are the part people forget. Headlines sit over imagery here, so the brief asks for 16:9, negative space on the side the layout needs, and no baked-in text or heavy filter. Links go to free-licence libraries with the lead phrase only, since the full comma-separated brief returns poor results. Sourcing is deliberately constrained to free-licence libraries or bought stock, and the spec grows a sourcing line either way: rights confirmation and model release for a client photo, licence and attribution for stock. PHOTOS.md carries the reasoning, the editorial-only trap, the attribution line to keep, resolution floors per use, and the treatment rules. Pulling images from a search results page or a competitor's site puts a takedown notice on the client, so it is ruled out explicitly rather than left to judgement. Verified in Chromium: both routes driven end to end. Upload produces a local data URL, renders in all four masks and emits the right warnings for a 941x1672 portrait; search generates the expected brief and three working library URLs. Example pages still clean across all nine themes. --- design-system/INTAKE.md | 9 +- design-system/PHOTOS.md | 132 +++++++++++++++++++ design-system/README.md | 20 +++ design-system/examples/picker.html | 199 ++++++++++++++++++++++++++++- 4 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 design-system/PHOTOS.md diff --git a/design-system/INTAKE.md b/design-system/INTAKE.md index bc096c5..061c6f5 100644 --- a/design-system/INTAKE.md +++ b/design-system/INTAKE.md @@ -87,7 +87,8 @@ One primary action per document. A second competing action halves both. ### 5. What imagery do you actually have? -> *Photography · Product screenshots · Illustration · Logos only · Nothing yet* +> *Photography · Upload my own · Find one for me · Product screenshots · +> Illustration · Logos only · Nothing yet* **Decides: media treatment — and this is the question most often skipped, then regretted.** @@ -95,11 +96,17 @@ regretted.** | Have | Use | |---|---| | Photography | `.media--notched` / `.media--circle`, accent wash on section covers | +| **Upload my own** | Drop it into `picker.html` and see it in all four house treatments instantly. Read in-browser; nothing is uploaded. | +| **Find one for me** | The picker builds a search brief from your other answers and links to free-licence libraries. | | Product screenshots | `.device--phone` / `.device--browser` with `.float-card` | | Illustration | Flat vector on `.tint-card` grounds; no photo masks | | Logos only | `.trustbar`, big type, `.deco` geometry carrying the visual load | | Nothing yet | Type-led layout + geometry. **Do not** design around placeholders you cannot fill. | +Either photo route adds a **sourcing line** to the spec — rights confirmation for +your own photo, licence and attribution for sourced stock. Full rules, size +floors and treatment guidance are in [`PHOTOS.md`](PHOTOS.md). + --- ### 6. Light or dark — and where does this live? diff --git a/design-system/PHOTOS.md b/design-system/PHOTOS.md new file mode 100644 index 0000000..9a09994 --- /dev/null +++ b/design-system/PHOTOS.md @@ -0,0 +1,132 @@ +# Photography — sourcing and treatment + +Two routes into the system: **your own photo**, or **one sourced to brief**. Both +end in the same place — masked to a house treatment, never dropped in raw. + +Run either from `examples/picker.html` (question 5 → "Upload my own" or "Find one +for me"). + +--- + +## Route 1 — your own photo + +Drop it into the picker and it renders immediately in the four house treatments, +in your accent theme. The file is read with `FileReader` in the browser and is +never uploaded anywhere. + +Before it ships, confirm two things: + +- **You hold the rights.** A photo taken by a contractor is usually theirs unless + the contract assigns it. Commissioned ≠ owned. +- **Anyone recognisable has signed a release**, if the page is advertising. This + covers staff photos used on a sales page, not just models. + +### Size floor + +| Use | Minimum width | +|---|---| +| Card / thumbnail | 800px | +| Section media panel | 1200px | +| Full-bleed web hero | 1600px | +| Projected slide | 2400px | + +Below the floor the image softens on retina displays. The picker flags this +automatically once it can read the dimensions. + +--- + +## Route 2 — sourced to brief + +The picker builds a search query from your other answers — subject from what +you're making, mood from what the audience should feel, plus composition +constraints the system needs: + +> *creative professional at work, studio desk, portrait — natural light, neutral +> muted tones, calm, unposed, landscape 16:9, negative space on the right for a +> headline, no text or logos in frame, no heavy filter* + +The last three clauses matter more than people expect. Headlines sit **over** +imagery in this system, so a photo with a busy centre has nowhere for type to go, +and a photo with baked-in text or a heavy colour filter fights the accent. + +### Where to source + +| Library | Licence | +|---|---| +| [Unsplash](https://unsplash.com) | Unsplash Licence — free, commercial use, no attribution required (give it anyway) | +| [Pexels](https://www.pexels.com) | Pexels Licence — free, commercial use | +| [Pixabay](https://pixabay.com) | Pixabay Content Licence — free, commercial use | + +Paid stock (Getty, Adobe Stock, Stocksy) is fine when the licence is bought and +the deliverable falls inside it. + +### Where **not** to source + +This is the part that causes real trouble, so it is stated plainly: + +- **Not** from an image search results page. Those results are indexed from + sites that own them; the search engine is not licensing them to you. +- **Not** from a competitor's site, a press page, or a social feed. +- **Not** a paid-stock preview with the watermark removed or AI-inpainted. +- **Not** an AI upscale of a low-res image you found somewhere. + +A takedown notice lands on the client, not the designer who sourced it. One +minute checking a licence is cheaper than the alternative. + +### Check the photo, not the site + +Free libraries carry a small number of **editorial-only** images — recognisable +people, branded products, trademarked buildings. Those cannot be used to sell +anything. The licence is stated on the individual photo page. + +### Record what you used + +Keep this line next to every asset, in the repo or the asset folder: + +``` +hero-workspace.jpg — Unsplash — © Jane Okonkwo — unsplash.com/photos/XXXXXXX — Unsplash Licence — 2026-08-03 +``` + +If a photo's provenance cannot be reconstructed later, it cannot be defended +later either. + +--- + +## Treatment — how a photo enters the system + +Photos are **always masked**. A bare rectangle is the single most common way this +style gets broken. + +| Class | Use | +|---|---| +| `.media--notched` | Rounded on three corners, square on one. The default section panel. | +| `.media--circle` | People. Team grids, testimonials, avatars. | +| `.media--wash` | Accent multiplied at ~55%. Section covers and dividers. | +| `.media--dim` | Dark bottom-up scrim. **Required** whenever type sits over the image. | +| `.media--xl` | Plain 32px radius, for a hero panel with no type over it. | + +### Rules + +1. **Type over a photo always gets `.media--dim`.** Carry the contrast in the + scrim, never by lightening the type — light-grey headlines fail on a + projector and fail WCAG. +2. **Crop people torso-up, eyes on the upper third.** Full-body shots read as + stock; tight crops read as real. +3. **One treatment per section.** Mixing a wash and a scrim in the same block + makes two photos look like an accident. +4. **No filters beyond the two above.** No duotone beyond `.media--wash`, no + vignettes, no grain. +5. **Object-fit is `cover`, object-position is `top`** for screenshots, `center` + for people. Never letterbox. + +--- + +## If you have no photo yet + +Answer "Nothing yet" and design type-led with geometry — `.deco-circle`, +`.deco-dots`, `.offset-block`, big display type on tinted grounds. This is a +first-class route in the system, not a fallback, and the reference set includes +pages that work this way. + +Designing around imagery that never arrives is the one imagery mistake that +cannot be recovered cheaply. diff --git a/design-system/README.md b/design-system/README.md index 4adee85..2828e1a 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -18,6 +18,7 @@ design-system/ ├── INTAKE.md ← START HERE — six questions asked before any design ├── DESIGN-BRIEF.md ← hand this to any designer or AI tool ├── FAVORITES.md ← the web-landing reference set + its extra rules +├── PHOTOS.md ← sourcing, licensing and photo treatment rules ├── tokens/ │ ├── tokens.json ← platform-agnostic tokens (Figma, Style Dictionary, builds) │ └── tokens.css ← CSS custom properties + the nine accent themes @@ -49,6 +50,25 @@ explicitly not to re-read or regenerate the rest of the system. That keeps each new page or deck cheap to produce and stops parallel CSS drifting away from the house style. +### Photos + +Question 5 offers two routes beyond "I already have some": + +- **Upload my own** — drop a file into the picker and it renders immediately in + all four house treatments (notched, circle, accent wash, dark scrim with type + over it), in your accent theme. The file is read with `FileReader` and never + leaves the browser. The picker flags it if the resolution is below the floor + for the use, or if the crop is wrong for a hero. +- **Find one for me** — the picker builds a search brief from your other answers + (subject from what you're making, mood from what the audience should feel, plus + the composition constraints the system needs — 16:9, negative space for the + headline, no baked-in text) and links straight into free-licence libraries. + +Sourced imagery goes through free-licence libraries or bought stock only. Pulling +images off a search results page, a competitor's site or a social feed is how a +takedown notice ends up with the client. [`PHOTOS.md`](PHOTOS.md) has the +licensing rules, the resolution floors, and the treatment rules. + ### A worked example `examples/portfolio.html` was built from a real intake run — *portfolio site · diff --git a/design-system/examples/picker.html b/design-system/examples/picker.html index 6a3aea6..f1b732e 100644 --- a/design-system/examples/picker.html +++ b/design-system/examples/picker.html @@ -55,6 +55,98 @@

Six questions

+ + +
@@ -122,6 +214,8 @@

Six questions

{ id:'media', q:'What imagery do you actually have?', opts:[ ['Photography', {media:'masked photos (.media--notched / .media--circle), accent wash on section covers'}], + ['Upload my own', {media:'your own photo, masked to the house treatments', photo:'upload'}], + ['Find one for me', {media:'licensed stock sourced to brief, masked to the house treatments', photo:'search'}], ['Product screenshots', {media:'device frames (.device--phone / .device--browser) with floating stat cards'}], ['Illustration', {media:'flat vector on tinted card grounds; no photo masks'}], ['Logos only', {media:'trust bar, big type, geometric ornament carrying the visual load'}], @@ -182,6 +276,8 @@

Six questions

if (accent) document.documentElement.className = 'theme-' + (A.feel.accent); + renderPhoto(); + if (n === 0) return; const css = A.kind?.layer === 'web' @@ -195,12 +291,20 @@

Six questions

// Media answer adds only the classes it actually needs. const mediaCls = { 'Photography': '.media--notched .media--circle .media--wash .avatar', + 'Upload my own': '.media--notched .media--circle .media--wash .media--dim', + 'Find one for me': '.media--notched .media--circle .media--wash .media--dim', 'Product screenshots': '.device--browser .device--phone .device__screen .float-card', 'Illustration': '.tint-card .media--xl', 'Logos only': '.trustbar .deco-circle .deco-dots', 'Nothing yet': '.deco-circle .deco-dots .offset-block', }[A.media?.label] || ''; + const photoLine = A.media?.photo === 'search' + ? `\nSourcing: free-licence stock only (Unsplash / Pexels / Pixabay) or\n bought stock. Record source, photographer and licence — see PHOTOS.md.` + : A.media?.photo === 'upload' + ? `\nSourcing: client-supplied. Confirm they hold the rights and that any\n recognisable person has signed a release before it goes live.` + : ''; + document.getElementById('spec').innerHTML = `Deliverable: ${A.kind?.label || '—'} Layer: ${A.kind?.layer === 'web' ? 'web' : A.kind?.layer === 'slides' ? 'deck / slides' : '—'} @@ -208,7 +312,7 @@

Six questions

Brand: ${A.brand?.brand || '—'} Ground: ${A.feel?.ground || '—'} Primary action: ${A.action?.cta || '—'} -Imagery: ${A.media?.media || '—'} +Imagery: ${A.media?.media || '—'}${photoLine} Mode: ${mode || '—'} Sections: ${A.kind?.sections || '—'} @@ -235,6 +339,99 @@

Six questions

the accent as series 1 and greys after.${note}`; } +/* ---------- Photo module ------------------------------------------------- */ + +const SUBJECT = { + 'Presentation / deck': 'business team meeting, boardroom, people working', + 'Pitch deck': 'founders working, startup team, whiteboard session', + 'Report / whitepaper': 'city skyline, office architecture, analyst at desk', + 'Landing page': 'team at laptops, modern workspace, product in use', + 'Product site': 'person using phone, clean desk setup, product in hand', + 'Portfolio': 'creative professional at work, studio desk, portrait', +}; +const MOOD = { + 'Trusted, established': 'natural light, neutral muted tones, calm, unposed', + 'Serious about money': 'cool tones, minimal, clean lines, low clutter', + 'Energised, warm': 'warm golden light, candid, movement, optimistic', + 'Modern, technical': 'cool high-key light, minimal, lots of white', + 'Calm, sustainable': 'soft daylight, greenery, airy, muted greens', + 'Bold, unmissable': 'high contrast, saturated, graphic, strong shapes', +}; + +function buildQuery() { + const subj = SUBJECT[A.kind?.label] || 'people at work'; + const mood = MOOD[A.feel?.label] || 'natural light, neutral tones'; + const side = A.kind?.layer === 'web' ? 'right' : 'left'; + return `${subj} — ${mood}, landscape 16:9, negative space on the ${side} for a headline, no text or logos in frame, no heavy filter`; +} + +const photoEl = document.getElementById('photo'); + +function renderPhoto() { + const mode = A.media?.photo; + photoEl.hidden = !mode; + if (!mode) return; + document.getElementById('modeUpload').hidden = mode !== 'upload'; + document.getElementById('modeSearch').hidden = mode !== 'search'; + document.getElementById('photoTitle').textContent = + mode === 'upload' ? 'Your photo' : 'Finding the right photo'; + + if (mode === 'search') { + const q = buildQuery(); + document.getElementById('query').textContent = q; + // Stock search engines return far better results for a short lead phrase + // than for the full comma-separated brief — keep the brief for the human, + // send only the first clause to the URL. + const bare = encodeURIComponent(q.split('—')[0].split(',')[0].trim()); + document.getElementById('lUnsplash').href = `https://unsplash.com/s/photos/${bare}`; + document.getElementById('lPexels').href = `https://www.pexels.com/search/${bare}/`; + document.getElementById('lPixabay').href = `https://pixabay.com/images/search/${bare}/`; + } +} + +function showImage(src, w, h) { + ['p1','p2','p3','p4'].forEach(id => { document.getElementById(id).src = src; }); + document.getElementById('previews').hidden = false; + document.getElementById('dims').textContent = `${w} × ${h}px`; + + const notes = []; + if (w < 1600) notes.push(`At ${w}px wide this will soften on a full-bleed hero — 1600px+ is the floor, 2400px for a projected slide.`); + if (h > w) notes.push('Portrait crop: use it in .media--circle or a side panel, not as a 16:9 hero.'); + if (w / h > 2.2) notes.push('Very wide crop: good for a band, too letterboxed for a card.'); + notes.push('Sitting type over it? Use .media--dim so the scrim carries the contrast — never lighten the type instead.'); + document.getElementById('advice').textContent = notes.join(' '); +} + +const drop = document.getElementById('drop'); +const fileInput = document.getElementById('file'); + +function readFile(f) { + if (!f || !f.type.startsWith('image/')) return; + const r = new FileReader(); + r.onload = e => { + const img = new Image(); + img.onload = () => showImage(e.target.result, img.naturalWidth, img.naturalHeight); + img.src = e.target.result; + }; + r.readAsDataURL(f); // stays in the browser; nothing is sent anywhere +} + +fileInput.onchange = e => readFile(e.target.files[0]); +['dragenter','dragover'].forEach(ev => drop.addEventListener(ev, e => { + e.preventDefault(); drop.style.borderColor = 'var(--accent-500)'; +})); +['dragleave','drop'].forEach(ev => drop.addEventListener(ev, e => { + e.preventDefault(); drop.style.borderColor = 'var(--ink-200)'; +})); +drop.addEventListener('drop', e => readFile(e.dataTransfer.files[0])); + +document.getElementById('copyQ').onclick = () => { + navigator.clipboard?.writeText(document.getElementById('query').textContent); + const b = document.getElementById('copyQ'); + b.textContent = 'Copied'; setTimeout(() => b.textContent = 'Copy query', 1400); +}; +document.getElementById('skipPhoto').onclick = () => { photoEl.hidden = true; }; + document.getElementById('copy').onclick = () => { const t = document.getElementById('spec').innerText; navigator.clipboard?.writeText(t); From 46226ade2260a2e8e332edf5d0c959fa66269ba8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 20:16:34 +0000 Subject: [PATCH 06/18] Add colour picker and asset slots; separate projects from examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes, all from the same idea: the intake should collect what a project actually needs, and each project should live in its own place. Colours. Answering that you have brand colours opens a colour module. Type any hex and it generates the full 50-700 ramp by anchoring lightness to fixed targets rather than offsetting from the input, so a very dark or very light brand colour still yields a usable 50 and 700. It derives the gradient, applies everything to the page live, and outputs a :root block for the project stylesheet — tokens.css is never edited for one project. The contrast check is the reason the module exists. A custom accent is the most common way this system gets broken: a mid-tone brand colour often clears 4.5:1 against neither white nor near-black, so no text can sit on it at all. The picker computes both ratios, picks the better --accent-on, and says plainly when neither passes. Assets. Three separate slots — logo, copy, existing material — because the rules differ by kind and one combined upload box loses that. The logo previews on white, ink and accent at once, since the usual failure is a logo that looks right in the navbar and disappears in the dark footer; it also flags JPG (no transparency), aspect ratio against the slot it will sit in, and raster under 400px. Copy is checked against the 62-character body measure and the ~45-character display limit. Reference files are listed by filename only and never read. Organisation. examples/ now holds only reference demos of the system; everything commissioned lives in projects/NNN-short-name/ with a required SPEC.md recording the answers and the scope they produced, and an assets/ tree split by kind. The portfolio moved there as 001 with its spec written up, including the intake conflict it resolved. Verified in Chromium: all five pages across all nine themes with no page or console errors, no horizontal overflow, and stylesheets resolving after the move. Colour, logo, copy and asset-tree modules driven end to end. Fixed an over-escaped regex that reported every copy file as "1 words". --- design-system/INTAKE.md | 53 ++ design-system/README.md | 35 +- design-system/examples/picker.html | 479 +++++++++++++++++- .../projects/001-portfolio-marin/SPEC.md | 79 +++ .../001-portfolio-marin/index.html} | 8 +- design-system/projects/README.md | 61 +++ 6 files changed, 703 insertions(+), 12 deletions(-) create mode 100644 design-system/projects/001-portfolio-marin/SPEC.md rename design-system/{examples/portfolio.html => projects/001-portfolio-marin/index.html} (98%) create mode 100644 design-system/projects/README.md diff --git a/design-system/INTAKE.md b/design-system/INTAKE.md index 061c6f5..1b23c2e 100644 --- a/design-system/INTAKE.md +++ b/design-system/INTAKE.md @@ -63,6 +63,18 @@ go one step away from it. - **Flexible** — use Q2's recommendation, show the brand colour as an alternate. - **None** — Q2 decides outright. +Answering "locked" or "flexible" opens **3a — your colours** in the picker. Type +or pick any hex and it generates the full 50→700 ramp, derives the gradient, and +checks contrast, applying it to the page live so you can see it before you commit. + +The contrast check matters more than it sounds. A custom accent is the most +common way this system gets broken: a mid-tone brand colour often clears 4.5:1 +against *neither* white nor near-black, which means no text can legally sit on +it. The picker says so and tells you to use it for fills and ornament only. + +Output is a `:root` block to paste into the **project's** stylesheet. +`tokens.css` is never edited for one project. + Whichever applies, one accent only. A brand with two colours gets the dominant one as the accent and the second demoted to a neutral support tint. @@ -122,6 +134,47 @@ floors and treatment guidance are in [`PHOTOS.md`](PHOTOS.md). --- +--- + +## 6a. Anything else to hand? *(optional)* + +> *Logo · Copy / content · Existing material* + +Three separate slots in the picker, on purpose. A combined "upload your stuff" +box loses what makes each one different. + +### Logo + +Previewed on **white, on ink, and on your accent** simultaneously — because the +usual failure is a logo that looks fine in the navbar and vanishes in the dark +footer. The picker flags: + +- **JPG** — no transparency, so it sits in a white box on every dark surface. + SVG, or PNG with an alpha channel. +- **Aspect** — a wide wordmark works in the navbar and slide furniture but not in + a square avatar slot; a square mark is the opposite. Most brands need both. +- **Resolution** — under 400px raster will blur on retina and badly when + projected. 800px+, or SVG. +- **A light (knockout) variant** — if the logo disappears on the ink or accent + preview, you need one. Most brands have it and forget to send it. + +### Copy / content + +Paste the real words or drop a `.txt` / `.md`. **Designing against real copy +instead of lorem ipsum is the cheapest quality win available** — placeholder text +hides every length problem, and length problems are what break layouts. + +The picker flags lines over 62 characters (the body measure), and headings over +~45 characters, which stop working as display lines at this weight and tracking. + +### Existing material + +Current site, old deck, brand guidelines. Filenames are listed only — nothing is +read or uploaded. This exists so the material lands in the project folder instead +of a chat thread. + +--- + ## Three more, only if it is a deck 7. **How long, and how long do you have to present it?** — roughly one slide per diff --git a/design-system/README.md b/design-system/README.md index 2828e1a..ea115ef 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -29,12 +29,16 @@ design-system/ │ └── web.css ← gradient heroes, device mockups, rails, pricing, FAQ ├── powerpoint/ │ └── SPEC.md ← pt sizes and inch positions for a 13.333" × 7.5" slide -└── examples/ - ├── picker.html ← the intake questions, interactive → outputs a scope - ├── index.html ← corporate web page (deck layer) - ├── deck.html ← 14-slide deck - ├── landing.html ← SaaS landing page (web layer) - └── portfolio.html ← built from a real intake run (see below) +├── examples/ ← reference demos of the system; these don't change +│ ├── picker.html ← the intake, interactive → outputs a scope +│ ├── index.html ← corporate web page (deck layer) +│ ├── deck.html ← 14-slide deck +│ └── landing.html ← SaaS landing page (web layer) +└── projects/ ← one folder per request — everything commissioned + ├── README.md ← the naming and asset-folder convention + └── 001-portfolio-marin/ + ├── SPEC.md ← intake answers + the scope they produced + └── index.html ``` ## Starting a new design @@ -69,9 +73,26 @@ images off a search results page, a competitor's site or a social feed is how a takedown notice ends up with the client. [`PHOTOS.md`](PHOTOS.md) has the licensing rules, the resolution floors, and the treatment rules. +### Colours and assets + +Say you have brand colours and the picker opens a **colour module**: type any hex +and it generates the full 50→700 ramp, derives the gradient, checks contrast, and +applies it to the page live. It outputs a `:root` block for the **project's** +stylesheet — `tokens.css` is never edited for one project. + +The contrast check is the point. A mid-tone brand colour often clears 4.5:1 +against neither white nor near-black, meaning no text can sit on it at all. Better +to learn that in the picker than in review. + +There's also an optional **assets module** with three separate slots — logo, copy, +and existing material. Separate because the rules differ: a logo is previewed on +white, ink *and* accent at once (the usual failure is one that looks fine in the +navbar and vanishes in the dark footer), copy is checked against the 62-character +body measure, and reference files are listed by name only. + ### A worked example -`examples/portfolio.html` was built from a real intake run — *portfolio site · +`projects/001-portfolio-marin/` was built from a real intake run — *portfolio site · trusted/established · buy or sign up · illustration* — and shows what the process catches. The portfolio archetype normally opens on a **portrait hero with label pins**, but the imagery answer was *illustration, no photography*. The imagery diff --git a/design-system/examples/picker.html b/design-system/examples/picker.html index f1b732e..3334076 100644 --- a/design-system/examples/picker.html +++ b/design-system/examples/picker.html @@ -55,6 +55,91 @@

Six questions

+ + + + + +
@@ -249,6 +428,9 @@

Your photo

row.querySelectorAll('.opt').forEach(o => o.setAttribute('aria-pressed', 'false')); b.setAttribute('aria-pressed', 'true'); A[item.id] = { label, ...data }; + // A new feeling answer reseeds the colour picker, unless the user has + // already dialled in a custom accent — theirs wins. + if (item.id === 'feel' && !C.custom) setAccentFromTheme(data.accent); render(); }; row.appendChild(b); @@ -276,7 +458,9 @@

Your photo

if (accent) document.documentElement.className = 'theme-' + (A.feel.accent); + renderColour(); renderPhoto(); + renderAssets(); if (n === 0) return; @@ -329,7 +513,10 @@

Your photo

${(A.kind?.cls || '—').split(' ').join('\n ')} ${mediaCls ? ' ' + mediaCls.split(' ').join('\n ') : ''} -Theme: <html class="theme-${A.feel?.accent || ''}">${mode.startsWith('dark') && A.kind?.layer === 'web' ? ' + .on-dark on dark sections' : ''} +Theme: <html class="theme-${A.feel?.accent || ''}">${mode.startsWith('dark') && A.kind?.layer === 'web' ? ' + .on-dark on dark sections' : ''}${C.custom ? '\n + the custom :root override from question 03a' : ''} +${AS.logo || AS.copy.trim() || AS.refs.length ? ` +Assets supplied: +${AS.logo ? ` logo/${AS.logo.filename}${AS.logo.svg ? ' (SVG)' : ` (${AS.logo.w}×${AS.logo.h})`} — needs a light variant if it vanishes on ink\n` : ''}${AS.copy.trim() ? ` content/copy.md — ${AS.copy.trim().split(/\s+/).length} words. Design against this, not lorem ipsum.\n` : ''}${AS.refs.length ? ` reference/ — ${AS.refs.length} file(s) for consistency, not for copying\n` : ''}` : ''} Read only: ${A.kind?.ref || '—'} Skip the rest of the docs — this line is the whole brief you need. @@ -339,6 +526,175 @@

Your photo

the accent as series 1 and greys after.${note}`; } +/* ---------- Colour module ------------------------------------------------ + One accent hex generates the whole ramp. Lightness steps are anchored to + fixed targets rather than offsets from the input, so a very dark or very + light accent still yields a usable 50 and 700. -------------------------- */ + +const hex2rgb = h => { + h = h.replace('#',''); + if (h.length === 3) h = h.split('').map(c => c + c).join(''); + return [0,2,4].map(i => parseInt(h.slice(i, i+2), 16)); +}; +const rgb2hex = ([r,g,b]) => + '#' + [r,g,b].map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2,'0')).join('').toUpperCase(); + +function rgb2hsl([r,g,b]) { + r/=255; g/=255; b/=255; + const mx = Math.max(r,g,b), mn = Math.min(r,g,b), d = mx-mn; + let h = 0; + if (d) { + if (mx === r) h = ((g-b)/d) % 6; + else if (mx === g) h = (b-r)/d + 2; + else h = (r-g)/d + 4; + } + h = (h*60 + 360) % 360; + const l = (mx+mn)/2; + const s = d ? d / (1 - Math.abs(2*l - 1)) : 0; + return [h, s, l]; +} +function hsl2rgb([h,s,l]) { + const c = (1 - Math.abs(2*l - 1)) * s, x = c * (1 - Math.abs((h/60) % 2 - 1)), m = l - c/2; + const t = h < 60 ? [c,x,0] : h < 120 ? [x,c,0] : h < 180 ? [0,c,x] + : h < 240 ? [0,x,c] : h < 300 ? [x,0,c] : [c,0,x]; + return t.map(v => (v + m) * 255); +} + +function buildRamp(hex) { + const [h, s] = rgb2hsl(hex2rgb(hex)); + const mk = (sat, li) => rgb2hex(hsl2rgb([h, Math.max(0, Math.min(1, sat)), li])); + return { + 50: mk(Math.min(s, 0.85), 0.96), + 100: mk(Math.min(s, 0.85), 0.89), + 300: mk(s * 0.92, 0.72), + 500: hex.toUpperCase(), + 600: mk(Math.min(s * 1.05, 1), 0.42), + 700: mk(Math.min(s * 1.05, 1), 0.30), + }; +} + +// WCAG relative luminance and contrast ratio. +const lum = hex => { + const [r,g,b] = hex2rgb(hex).map(v => { + v /= 255; + return v <= 0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4); + }); + return 0.2126*r + 0.7152*g + 0.0722*b; +}; +const ratio = (a, b) => { + const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p); + return (x + 0.05) / (y + 0.05); +}; + +const C = { accent: '#4059F0', ground: '#FFFFFF', g1: null, g2: null, on: '#FFFFFF', custom: false }; + +function renderColour() { + const show = A.brand && A.brand.label !== 'No, choose for me'; + document.getElementById('colour').hidden = !show; + if (!show) return; + + const r = buildRamp(C.accent); + const rDark = ratio(C.accent, '#14181F'), rLight = ratio(C.accent, '#FFFFFF'); + C.on = rDark >= rLight ? '#14181F' : '#FFFFFF'; + const best = Math.max(rDark, rLight); + + // Swatches + document.getElementById('cRamp').innerHTML = Object.entries(r) + .map(([k, v]) => ``).join(''); + + // Contrast badge — this is the one thing a custom accent commonly breaks. + const tag = document.getElementById('cContrast'); + const pass = best >= 4.5; + tag.textContent = `${best.toFixed(2)}:1 ${pass ? 'AA' : 'fails AA'}`; + tag.style.background = pass ? 'var(--positive)' : 'var(--negative)'; + tag.style.color = '#fff'; + document.getElementById('cContrastNote').textContent = pass + ? `Text on your accent will be ${C.on === '#FFFFFF' ? 'white' : 'near-black'}.` + : `No text colour clears 4.5:1 on this accent. Use it for fills and ornament only, and put labels beside it rather than on it — or darken it until this passes.`; + + const g1 = C.g1 || r[600], g2 = C.g2 || r[300]; + document.getElementById('cGPreview').style.background = + `linear-gradient(135deg, ${g1} 0%, ${C.accent} 55%, ${g2} 100%)`; + if (!C.g1) document.getElementById('cG1').value = g1; + if (!C.g2) document.getElementById('cG2').value = g2; + + // Apply live to this page + const root = document.documentElement; + if (C.custom) { + root.style.setProperty('--accent-50', r[50]); + root.style.setProperty('--accent-100', r[100]); + root.style.setProperty('--accent-300', r[300]); + root.style.setProperty('--accent-500', r[500]); + root.style.setProperty('--accent-600', r[600]); + root.style.setProperty('--accent-700', r[700]); + root.style.setProperty('--accent-on', C.on); + root.style.setProperty('--gradient-hero', `linear-gradient(135deg, ${g1} 0%, ${r[500]} 55%, ${g2} 100%)`); + root.style.setProperty('--surface-page', C.ground); + } + + document.getElementById('cCss').textContent = +`:root { + --accent-50: ${r[50]}; + --accent-100: ${r[100]}; + --accent-300: ${r[300]}; + --accent-500: ${r[500]}; + --accent-600: ${r[600]}; + --accent-700: ${r[700]}; + --accent-on: ${C.on}; + --gradient-hero: linear-gradient(135deg, ${g1} 0%, ${r[500]} 55%, ${g2} 100%); + --surface-page: ${C.ground}; +}`; +} + +function setAccent(hex) { + if (!/^#?[0-9a-f]{3}([0-9a-f]{3})?$/i.test(hex)) return; + C.accent = (hex[0] === '#' ? hex : '#' + hex).toUpperCase(); + C.custom = true; + document.getElementById('cAccent').value = C.accent.length === 7 ? C.accent : '#4059F0'; + document.getElementById('cHex').value = C.accent; + renderColour(); +} + +document.getElementById('cAccent').oninput = e => setAccent(e.target.value); +document.getElementById('cHex').oninput = e => setAccent(e.target.value.trim()); +document.getElementById('cG1').oninput = e => { C.g1 = e.target.value.toUpperCase(); C.custom = true; renderColour(); }; +document.getElementById('cG2').oninput = e => { C.g2 = e.target.value.toUpperCase(); C.custom = true; renderColour(); }; +document.getElementById('cGReset').onclick = () => { C.g1 = C.g2 = null; renderColour(); }; + +document.getElementById('cGround').onclick = e => { + const b = e.target.closest('[data-g]'); if (!b) return; + document.querySelectorAll('#cGround .opt').forEach(o => o.setAttribute('aria-pressed', String(o === b))); + C.ground = b.dataset.g; C.custom = true; renderColour(); +}; + +document.getElementById('cCopy').onclick = () => { + navigator.clipboard?.writeText(document.getElementById('cCss').textContent); + const b = document.getElementById('cCopy'); + b.textContent = 'Copied'; setTimeout(() => b.textContent = 'Copy CSS', 1400); +}; +document.getElementById('cRevert').onclick = () => { + C.custom = false; C.g1 = C.g2 = null; + ['--accent-50','--accent-100','--accent-300','--accent-500','--accent-600','--accent-700', + '--accent-on','--gradient-hero','--surface-page'].forEach(v => document.documentElement.style.removeProperty(v)); + if (A.feel?.accent) setAccentFromTheme(A.feel.accent); + C.custom = false; + renderColour(); +}; + +// Seed the picker from whichever preset theme the feeling answer chose. +function setAccentFromTheme(name) { + const probe = document.createElement('div'); + probe.className = 'theme-' + name; + document.body.appendChild(probe); + const v = getComputedStyle(probe).getPropertyValue('--accent-500').trim(); + probe.remove(); + if (v) { + C.accent = v.toUpperCase(); + document.getElementById('cAccent').value = v; + document.getElementById('cHex').value = v.toUpperCase(); + } +} + /* ---------- Photo module ------------------------------------------------- */ const SUBJECT = { @@ -432,6 +788,127 @@

Your photo

}; document.getElementById('skipPhoto').onclick = () => { photoEl.hidden = true; }; +/* ---------- Assets module: logo, copy, reference ------------------------- + Deliberately three separate slots. A logo has constraints a photo does not + (transparency, a light variant for dark surfaces, clear space), and copy has + constraints neither has. One combined "upload your stuff" box loses all of + that. ---------------------------------------------------------------------- */ + +const AS = { logo: null, copy: '', refs: [] }; + +function slug(s) { + return (s || 'project').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 28); +} + +function renderAssets() { + document.getElementById('assets').hidden = !A.kind; + if (!A.kind) return; + + const n = String(1).padStart(3, '0'); + const name = `${n}-${slug(A.kind.label)}`; + const lines = [`projects/${name}/`, ` SPEC.md`, ` index.html`]; + const sub = []; + if (AS.logo) sub.push(` logo/${AS.logo.filename}`); + if (A.media?.photo) sub.push(` photos/`); + if (AS.copy.trim()) sub.push(` content/copy.md`); + if (AS.refs.length) sub.push(` reference/ (${AS.refs.length} file${AS.refs.length > 1 ? 's' : ''})`); + if (sub.length) { lines.push(` assets/`); lines.push(...sub); } + document.getElementById('assetTree').textContent = lines.join('\n'); +} + +/* --- Logo --- */ +const logoIn = document.getElementById('fileLogo'); +function readLogo(f) { + if (!f) return; + const isSvg = /svg/.test(f.type) || /\.svg$/i.test(f.name); + const isJpg = /jpe?g/.test(f.type); + const r = new FileReader(); + r.onload = e => { + const src = e.target.result; + ['logoLight','logoDark','logoAccent'].forEach(id => { document.getElementById(id).src = src; }); + document.getElementById('logoOut').hidden = false; + + const img = new Image(); + img.onload = () => { + const notes = []; + if (isJpg) notes.push('JPG has no transparency, so this will sit in a white box on the ink footer and the accent band. Get an SVG, or a PNG with an alpha channel.'); + else if (isSvg) notes.push('SVG — scales to any size and stays crisp when projected. Best case.'); + else notes.push('PNG — check the three previews above; if the background is not transparent you will see it on the dark and accent panels.'); + + const ar = img.naturalWidth / (img.naturalHeight || 1); + if (ar > 2.5) notes.push(`Wide wordmark (${ar.toFixed(1)}:1). Good in the navbar and slide furniture; too wide for a square avatar slot — you will want a separate mark.`); + else if (ar < 1.3) notes.push(`Squarish mark (${ar.toFixed(1)}:1). Works as an icon chip and favicon; pair it with a wordmark for the navbar.`); + + if (!isSvg && img.naturalWidth < 400) notes.push(`Only ${img.naturalWidth}px wide — it will blur on retina and badly on a projector. 800px+ for raster, or switch to SVG.`); + notes.push('Check the ink and accent previews: if the logo disappears into either, you need a light (knockout) variant. Most brands have one.'); + document.getElementById('logoNotes').textContent = notes.join(' '); + + AS.logo = { filename: f.name, w: img.naturalWidth, h: img.naturalHeight, svg: isSvg }; + renderAssets(); render(); + }; + img.onerror = () => { // SVG without intrinsic size + document.getElementById('logoNotes').textContent = + 'SVG loaded. It scales cleanly — just confirm it has no hard-coded white background by checking the ink and accent previews above.'; + AS.logo = { filename: f.name, svg: true }; + renderAssets(); render(); + }; + img.src = src; + }; + r.readAsDataURL(f); +} +logoIn.onchange = e => readLogo(e.target.files[0]); + +/* --- Copy --- */ +const copyArea = document.getElementById('copyText'); +function analyseCopy() { + const t = AS.copy = copyArea.value; + const words = t.trim() ? t.trim().split(/\s+/).length : 0; + const lines = t.split('\n').filter(l => l.trim()); + document.getElementById('copyStats').textContent = words ? `${words} words · ${lines.length} lines` : ''; + + const notes = []; + const longest = lines.reduce((m, l) => Math.max(m, l.length), 0); + if (longest > 70) notes.push(`Your longest line is ${longest} characters. Body copy is capped at 62 characters per line in this system, so that will wrap — check it still reads well broken.`); + const heads = lines.filter(l => l.length < 60 && !/[.!?]$/.test(l.trim())); + if (heads.length) notes.push(`${heads.length} line${heads.length > 1 ? 's look' : ' looks'} like a heading. Headlines here are tightly tracked and heavy — anything over about 45 characters stops working as a display line.`); + if (words > 0 && words < 40) notes.push('Not much copy yet. That is fine, but the layout will be sized to it — add the rest before the design is locked, not after.'); + document.getElementById('copyNotes').textContent = notes.join(' '); + renderAssets(); render(); +} +copyArea.oninput = analyseCopy; +document.getElementById('pickCopy').onclick = () => document.getElementById('fileCopy').click(); +document.getElementById('fileCopy').onchange = e => { + const f = e.target.files[0]; if (!f) return; + const r = new FileReader(); + r.onload = ev => { copyArea.value = ev.target.result; analyseCopy(); }; + r.readAsText(f); +}; + +/* --- Reference files: filenames only, never read --- */ +document.getElementById('fileRef').onchange = e => { + AS.refs = [...e.target.files].map(f => `${f.name} · ${(f.size/1024).toFixed(0)} KB`); + document.getElementById('refList').innerHTML = AS.refs.map(r => `
  • ${r}
  • `).join(''); + renderAssets(); render(); +}; + +// Shared drag-and-drop wiring for the three zones. +[['dropLogo', f => readLogo(f)], + ['dropRef', null], + ['dropCopy', f => { const r = new FileReader(); r.onload = ev => { copyArea.value = ev.target.result; analyseCopy(); }; r.readAsText(f); }] +].forEach(([id, handler]) => { + const z = document.getElementById(id); + ['dragenter','dragover'].forEach(ev => z.addEventListener(ev, e => { e.preventDefault(); z.style.borderColor = 'var(--accent-500)'; })); + ['dragleave','drop'].forEach(ev => z.addEventListener(ev, e => { e.preventDefault(); z.style.borderColor = 'var(--ink-200)'; })); + z.addEventListener('drop', e => { + const fs = e.dataTransfer.files; + if (id === 'dropRef') { + AS.refs = [...fs].map(f => `${f.name} · ${(f.size/1024).toFixed(0)} KB`); + document.getElementById('refList').innerHTML = AS.refs.map(r => `
  • ${r}
  • `).join(''); + renderAssets(); render(); + } else if (handler) handler(fs[0]); + }); +}); + document.getElementById('copy').onclick = () => { const t = document.getElementById('spec').innerText; navigator.clipboard?.writeText(t); diff --git a/design-system/projects/001-portfolio-marin/SPEC.md b/design-system/projects/001-portfolio-marin/SPEC.md new file mode 100644 index 0000000..cb40fc1 --- /dev/null +++ b/design-system/projects/001-portfolio-marin/SPEC.md @@ -0,0 +1,79 @@ +# 001 — Portfolio, Marin Ellis + +Produced by `examples/picker.html` on 2026-08-03. + +## Intake answers + +| Question | Answer | +|---|---| +| What are you making? | Portfolio site | +| What should they feel in five seconds? | Trusted, established, safe | +| Brand colours / logo / fonts? | None — system chose | +| The one thing they should do? | Buy or sign up | +| Imagery available? | Illustration | +| Light or dark, where does it live? | Light, on screen | + +## Resolved spec + +| Setting | Value | +|---|---| +| **Deliverable** | Portfolio site | +| **Layer** | web | +| **Accent** | indigo `#4059F0` | +| **Ground** | white | +| **Primary action** | Hero CTA + pricing grid with a featured tier + closing CTA band | +| **Imagery** | Flat vector on tinted card grounds; no photo masks | +| **Mode** | Light, screen | +| **Sections** | Hero, trust bar, selected work, services, process rail, packages, testimonial, FAQ, CTA | + +**Accent note.** "Trusted, established" maps to navy *or* indigo. Indigo was +chosen because the page sells fixed-price packages — navy reads more +formal-consultant, indigo more modern-trustworthy. Swap by changing one class on +``. + +## Conflict resolved at intake + +The portfolio archetype normally opens on a **portrait hero with floating label +pins** (`.pin`, `.media--blob`). The imagery answer was *illustration, no +photography*. + +**Imagery wins.** No portrait, no photo masks anywhere on the page. Inline SVG +illustration on tinted grounds carries the visual load instead. + +Caught before anything was drawn. Left to the build, it would have surfaced as a +revision round. + +## Scope + +**Import (already written, do not modify):** +`tokens/tokens.css`, `css/base.css`, `css/components.css`, `css/web.css` + +**Files written:** `index.html` only. + +**Classes composed from:** +`.hero-sheen` `.badge-pill` `.orb` `.offset-block` `.stat-row` `.trustbar` +`.grid--2` `.card--raised` `.tint-grid` `.tint-card` `.rail` `.rail__num` +`.price-grid` `.price-card--featured` `.checklist` `.quote` `.faq` +`.cta-band--gradient` `.field-group` `.media--xl` + +**Theme:** `` + +**Read only:** `FAVORITES.md` §"Rules specific to this set" + +## Deviations from stock components + +Five lines of scoped CSS in `index.html`, no changes under `css/` or `tokens/`: + +- `.hero .stat-row` pinned to three columns. The hero column is narrower than + `.stat-row`'s auto-fit minimum, so the third figure wrapped. Pinned locally + rather than loosening the component for every other page that uses it. +- `.work-card` / `.illo` — padding and sizing for the illustration cards. + +## Imagery + +Inline SVG, authored for this project. No third-party assets, so no licence or +attribution obligations. + +If photography is added later, it goes through `PHOTOS.md` — and note that +adding it contradicts the intake answer, so re-run question 5 rather than +bolting photos onto an illustration-led layout. diff --git a/design-system/examples/portfolio.html b/design-system/projects/001-portfolio-marin/index.html similarity index 98% rename from design-system/examples/portfolio.html rename to design-system/projects/001-portfolio-marin/index.html index 1f93058..caa8340 100644 --- a/design-system/examples/portfolio.html +++ b/design-system/projects/001-portfolio-marin/index.html @@ -4,10 +4,10 @@ Portfolio — Corporate Deck Design System - - - - + + + + + +
    + +
    + Before we design anything +

    Six questions

    +

    + Every answer changes a concrete setting. Answer these and you get a spec you + can hand to any designer — or paste into an AI tool — and get back something + that looks like it belongs to the rest of your work. +

    +
    + +
    + + + + + + + + + + +
    +
    +
    + Your spec +
    +
    +
    + 0 of 6 +
    +
    Answer the questions above and your spec appears here.
    +
    + + +
    +
    +
    + +
    + + diff --git a/design-system/examples/picker.html b/design-system/examples/picker.html index 3334076..ed65f8b 100644 --- a/design-system/examples/picker.html +++ b/design-system/examples/picker.html @@ -115,7 +115,7 @@

    Your colours

    In context -
    +
    Section label

    A headline with emphasis

    @@ -452,9 +452,18 @@

    What else do you have?

    note = '\n\n⚠︎ You chose dark for a deck. Decks stay light-ground in this system;\n the spec has been corrected. Use a dark section slide for contrast instead.'; } - // Accent: locked brand overrides the recommendation. + // Accent: a hex dialled into the colour module is the real answer and + // supersedes the family the feeling question suggested. let accent = A.feel?.accent || ''; - if (A.brand?.label === 'Yes, locked' && accent) accent += ' (nearest ramp — override with the real brand values)'; + if (C.custom) { + accent = `${C.accent} (custom — text on it: ${C.on === '#FFFFFF' ? 'white' : 'near-black'})`; + } else if (A.brand?.label === 'Yes, locked' && accent) { + accent += ' (nearest ramp — override with the real brand values)'; + } + + const groundName = C.custom + ? ({ '#FFFFFF': 'white', '#F7F8FA': 'off-white', '#FAF6EC': 'cream' }[C.ground] || C.ground) + : (A.feel?.ground || '—'); if (accent) document.documentElement.className = 'theme-' + (A.feel.accent); @@ -494,7 +503,7 @@

    What else do you have?

    Layer: ${A.kind?.layer === 'web' ? 'web' : A.kind?.layer === 'slides' ? 'deck / slides' : '—'} Accent: ${sw}${accent || '—'} Brand: ${A.brand?.brand || '—'} -Ground: ${A.feel?.ground || '—'} +Ground: ${groundName} Primary action: ${A.action?.cta || '—'} Imagery: ${A.media?.media || '—'}${photoLine} Mode: ${mode || '—'} @@ -629,7 +638,21 @@

    What else do you have?

    root.style.setProperty('--accent-700', r[700]); root.style.setProperty('--accent-on', C.on); root.style.setProperty('--gradient-hero', `linear-gradient(135deg, ${g1} 0%, ${r[500]} 55%, ${g2} 100%)`); - root.style.setProperty('--surface-page', C.ground); + } + + // Ground is a setting for *your* page, not for this one. Applying it to + // :root would override the picker's own light/dark theme and leave light + // text on a white card. It previews scoped to the sample block instead. + const prev = document.getElementById('cPreview'); + if (prev) { + prev.style.setProperty('--surface-page', C.ground); + prev.style.setProperty('--surface-muted', C.ground); + prev.style.background = C.ground; + prev.style.setProperty('--fg-strong', '#14181F'); + prev.style.setProperty('--fg-body', '#3A4761'); + prev.style.setProperty('--fg-muted', '#8993A6'); + prev.style.setProperty('--ink-100', '#ECEFF3'); + prev.style.setProperty('--ink-200', '#D8DDE5'); } document.getElementById('cCss').textContent = @@ -652,19 +675,19 @@

    What else do you have?

    C.custom = true; document.getElementById('cAccent').value = C.accent.length === 7 ? C.accent : '#4059F0'; document.getElementById('cHex').value = C.accent; - renderColour(); + render(); } document.getElementById('cAccent').oninput = e => setAccent(e.target.value); document.getElementById('cHex').oninput = e => setAccent(e.target.value.trim()); -document.getElementById('cG1').oninput = e => { C.g1 = e.target.value.toUpperCase(); C.custom = true; renderColour(); }; -document.getElementById('cG2').oninput = e => { C.g2 = e.target.value.toUpperCase(); C.custom = true; renderColour(); }; -document.getElementById('cGReset').onclick = () => { C.g1 = C.g2 = null; renderColour(); }; +document.getElementById('cG1').oninput = e => { C.g1 = e.target.value.toUpperCase(); C.custom = true; render(); }; +document.getElementById('cG2').oninput = e => { C.g2 = e.target.value.toUpperCase(); C.custom = true; render(); }; +document.getElementById('cGReset').onclick = () => { C.g1 = C.g2 = null; render(); }; document.getElementById('cGround').onclick = e => { const b = e.target.closest('[data-g]'); if (!b) return; document.querySelectorAll('#cGround .opt').forEach(o => o.setAttribute('aria-pressed', String(o === b))); - C.ground = b.dataset.g; C.custom = true; renderColour(); + C.ground = b.dataset.g; C.custom = true; render(); }; document.getElementById('cCopy').onclick = () => { @@ -675,10 +698,10 @@

    What else do you have?

    document.getElementById('cRevert').onclick = () => { C.custom = false; C.g1 = C.g2 = null; ['--accent-50','--accent-100','--accent-300','--accent-500','--accent-600','--accent-700', - '--accent-on','--gradient-hero','--surface-page'].forEach(v => document.documentElement.style.removeProperty(v)); + '--accent-on','--gradient-hero'].forEach(v => document.documentElement.style.removeProperty(v)); if (A.feel?.accent) setAccentFromTheme(A.feel.accent); C.custom = false; - renderColour(); + render(); }; // Seed the picker from whichever preset theme the feeling answer chose. From 8b29f82168cb111040caa7812c29f27e354af78e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 20:45:41 +0000 Subject: [PATCH 08/18] Make the system usable by people who don't have the repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scope told someone to import tokens/tokens.css, which is useless if they never cloned anything. Three routes now close that gap, differing only in what the other person has to install. A link. design-system/index.html is a front door that routes the three audiences — commissioning, designing, using AI — to the right entry point, and a Pages workflow publishes the folder as a static site on every push to main. The workflow gates the build on the property that makes the site worth having: it fails if any HTML picks up an external src or href, since the whole point is that this runs offline and under a strict CSP. Enabling Pages needs one switch in repo settings, documented rather than assumed. A file. tools/build-standalone.js inlines every stylesheet and adds the token-level dark layer, producing examples/intake-standalone.html — the whole intake in one file that can be emailed or opened from a USB stick. It refuses to write a build containing external references. The dark layer lives in the build rather than tokens.css because the system is deliberately light-first for decks. A starter file. The intake now hands over a working page instead of instructions: a self-contained HTML document with the CSS inlined, the chosen accent baked in as a :root override, a scaffold of the sections the answers selected, and a header comment recording the spec. Deck answers get slide archetypes, web answers get a landing page, and pricing appears only when the action answer asks for it. Two defects fixed on the way. The starter template emitted literal , and tags, which is fragile in any consumer that parses by splitting — the closing tags are now escaped so the source never contains them. The bundler split on , which broke once a script legitimately contained that string; it uses first-index and last-index instead. SHARING.md documents all three routes and what to send whom. Verified: 58-assertion suite still green; starter files generated for both layers download, are self-contained, carry the custom accent through to render, and show no overflow; the standalone build and its dark layer pass their regression; the home page renders at 1440 and 390 with all twelve links resolving; the Pages external-asset gate passes against the tree. --- .github/workflows/pages.yml | 64 ++++ design-system/README.md | 25 ++ design-system/SHARING.md | 119 ++++++ design-system/examples/intake-standalone.html | 352 +++++++++++++++++- design-system/examples/picker.html | 329 ++++++++++++++++ design-system/index.html | 238 ++++++++++++ design-system/tools/build-standalone.js | 127 +++++++ 7 files changed, 1248 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 design-system/SHARING.md create mode 100644 design-system/index.html create mode 100644 design-system/tools/build-standalone.js diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..140ac61 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,64 @@ +name: Deploy design system to Pages + +# Publishes design-system/ as a public website, so anyone can use the intake +# and browse the examples from a URL without cloning anything. +# +# One-time setup: repo Settings → Pages → Source: "GitHub Actions". +# Until that is switched on this workflow builds but cannot deploy. + +on: + push: + branches: [main] + paths: + - 'design-system/**' + - '.github/workflows/pages.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Let a running deploy finish rather than cancelling it mid-publish. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check the site has no external dependencies + run: | + # The system is meant to be self-contained. A stray CDN link would + # break it offline and behind strict CSP, so fail the build instead. + if grep -rIn --include='*.html' -E '(src|href)="https?://' design-system/ ; then + echo "::error::Found an external asset reference in design-system/" + exit 1 + fi + echo "No external asset references." + + - name: Verify entry points exist + run: | + for f in index.html examples/picker.html examples/deck.html \ + examples/landing.html examples/intake-standalone.html; do + test -f "design-system/$f" || { echo "::error::missing design-system/$f"; exit 1; } + done + echo "All entry points present." + + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: design-system + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/design-system/README.md b/design-system/README.md index ea115ef..13dd087 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -15,6 +15,8 @@ corners, oversized section numerals, and soft geometric ornament. ``` design-system/ +├── index.html ← the site's front door — routes three audiences +├── SHARING.md ← how anyone else uses this (link / file / repo) ├── INTAKE.md ← START HERE — six questions asked before any design ├── DESIGN-BRIEF.md ← hand this to any designer or AI tool ├── FAVORITES.md ← the web-landing reference set + its extra rules @@ -29,8 +31,11 @@ design-system/ │ └── web.css ← gradient heroes, device mockups, rails, pricing, FAQ ├── powerpoint/ │ └── SPEC.md ← pt sizes and inch positions for a 13.333" × 7.5" slide +├── tools/ +│ └── build-standalone.js ← rebuilds the one-file intake after CSS changes ├── examples/ ← reference demos of the system; these don't change │ ├── picker.html ← the intake, interactive → outputs a scope +│ ├── intake-standalone.html ← the intake as ONE file, works offline │ ├── index.html ← corporate web page (deck layer) │ ├── deck.html ← 14-slide deck │ └── landing.html ← SaaS landing page (web layer) @@ -41,6 +46,26 @@ design-system/ └── index.html ``` +## Giving it to someone else + +Three routes, differing only in what the other person has to install — see +[`SHARING.md`](SHARING.md). + +| Route | They need | Best for | +|---|---|---| +| **A link** — `index.html` as a site | A browser | Clients, one-off collaborators | +| **A file** — `examples/intake-standalone.html` | A browser | Email, offline, no internet | +| **The repo** | git | Designers building with it repeatedly | + +A GitHub Pages workflow ships in `.github/workflows/pages.yml`. It needs one +switch flipped once — **Settings → Pages → Source → "GitHub Actions"** — after +which the site is live at `https://.github.io//`. + +The intake's **Download starter file** button is what makes this useful to +someone without the repo: instead of a scope telling them to import files they +don't have, they get a single self-contained HTML file with the CSS inlined, +their accent baked in, and a scaffold of their chosen sections. + ## Starting a new design Don't pick a layout first — **answer the six questions in diff --git a/design-system/SHARING.md b/design-system/SHARING.md new file mode 100644 index 0000000..c2f5a23 --- /dev/null +++ b/design-system/SHARING.md @@ -0,0 +1,119 @@ +# Sharing this system + +Three ways someone other than you can use it. They differ in what the other +person has to install — which is the only thing that matters when you're trying +to get a designer or a client to actually use something. + +| Route | They need | Best for | +|---|---|---| +| **1. A link** | A browser | Clients, one-off collaborators, anyone you don't want to onboard | +| **2. A file** | A browser | Sending in an email; working offline; no internet at all | +| **3. The repo** | git | Designers and developers building with the system repeatedly | + +--- + +## 1. A link — the website + +`design-system/` is a complete static site. `index.html` routes three audiences +(commissioning / designing / using AI) to the right starting point. + +### Publish it with GitHub Pages + +`.github/workflows/pages.yml` deploys it on every push to `main`. **One switch is +needed first**, and only you can flip it: + +> Repo **Settings → Pages → Source → "GitHub Actions"** + +After that the site is live at: + +``` +https://.github.io// +``` + +The workflow also gates the build: it fails if any HTML picks up an external +`src`/`href`, because the whole point is that this works offline and behind a +strict CSP. + +### Or host it anywhere + +It's static files with no build step. Drag `design-system/` onto Netlify, Vercel, +Cloudflare Pages, S3, or any web server. Nothing to configure. + +--- + +## 2. A file — the standalone intake + +`examples/intake-standalone.html` is the entire intake in **one file**, every +stylesheet inlined, no network requests. Email it, put it on a shared drive, open +it from a USB stick. It works. + +Regenerate it after changing any CSS: + +```bash +node tools/build-standalone.js +``` + +### And what they get out of it + +The intake's **"Download starter file"** button is the piece that makes this +useful to someone without the repo. Instead of a scope that says *"import +`tokens/tokens.css`"* — which they don't have — it hands them a single +self-contained HTML file with: + +- every stylesheet inlined +- their chosen accent baked in as a `:root` override +- a scaffold of the sections their answers selected +- a comment header recording the spec that produced it + +They open it in a browser and start editing. No clone, no build, no npm. + +> The button needs the stylesheets readable, so it works on the **hosted site** +> and in the **standalone file**. Opening `examples/picker.html` directly from +> disk (`file://`) blocks `fetch`, and the button will say so rather than fail +> silently. + +--- + +## 3. The repo — for people building with it + +Designers and developers who'll use the system repeatedly should clone it and +read [`DESIGN-BRIEF.md`](DESIGN-BRIEF.md). + +Their working loop: + +1. Run the intake, save the output as `projects/NNN-name/SPEC.md` +2. Write `index.html` against the scope in that file — **and nothing beyond it** +3. Never edit `css/` or `tokens/` for one project + +That third rule is what keeps the system a system. A project that genuinely needs +a new component is a change to the system, made deliberately and separately — not +a side effect of one page. + +--- + +## What to send whom + +**A client, or someone commissioning work** +Send the link to `index.html`, or the standalone intake file. They answer six +questions and send you back a spec. That's the whole ask. + +**A designer or agency** +Send `DESIGN-BRIEF.md` plus the filled-in spec. Add `powerpoint/SPEC.md` if the +deliverable is a deck template. §9 of the brief is your acceptance checklist. + +**An AI tool** +Paste the scope block the intake produces. It names the exact files, the exact +classes, and instructs the tool not to regenerate the system — which keeps output +consistent and stops it burning context re-deriving what already exists. + +--- + +## Licensing, if you share it publicly + +The system itself is your work. Two things to keep straight if it goes public: + +- **Fonts.** Poppins and Inter are both SIL Open Font License — free to bundle + and serve. The stacks fall back to system fonts, so nothing breaks if you + don't ship them. +- **Photography.** No images are bundled. Anything a user adds is theirs, under + the rules in [`PHOTOS.md`](PHOTOS.md). diff --git a/design-system/examples/intake-standalone.html b/design-system/examples/intake-standalone.html index 8d3ed40..726b9dd 100644 --- a/design-system/examples/intake-standalone.html +++ b/design-system/examples/intake-standalone.html @@ -1,4 +1,10 @@ + + + + + Design Intake — Corporate Deck Design System + + +
    @@ -1874,8 +1882,10 @@

    What else do you have?

    Answer the questions above and your spec appears here.
    +
    +

    One self-contained HTML file — your theme baked in, no repo or build needed.

    @@ -2468,6 +2478,333 @@

    What else do you have?

    }); }); +/* ---------- Starter file ------------------------------------------------- + The scope tells someone what to import — which is useless if they don't + have the repo. This hands them a single working file instead: every + stylesheet inlined, their accent baked in, and a scaffold of the sections + their answers chose. No clone, no build, no network. -------------------- */ + +async function collectCSS() { + // Standalone/hosted build: the CSS is already inline. + const inline = [...document.querySelectorAll('style')] + .map(s => s.textContent).filter(t => t.includes('--accent-500')).join('\n'); + if (inline) return inline; + // Served over http(s): fetch the linked sheets. + const links = [...document.querySelectorAll('link[rel="stylesheet"]')].map(l => l.href); + if (location.protocol === 'file:') return null; // fetch is blocked on file:// + try { + const parts = await Promise.all(links.map(h => fetch(h).then(r => r.text()))); + return parts.join('\n'); + } catch { return null; } +} + +function starterBody() { + const web = A.kind?.layer === 'web'; + const eyebrow = A.kind?.label || 'Your project'; + if (!web) { + return `
    + + +
    +
    +
    +
    + ${eyebrow} +

    + Your title + Here +

    +
    +

    Presented by Name
    Date · Place

    +
    +
    +
    +
    +

    01 — Cover

    +
    + + +
    +
    + +
    Contents
    +
    02
    +
    +
    +
    + Table of contents +

    What we'll cover

    +
    +
    +
    01First section
    +
    03Third section
    +
    02Second section
    +
    04Fourth section
    +
    +
    +
    +
    +

    02 — Agenda

    +
    + + +
    +
    +
    +
    + 01 +

    First section

    +
    +
    +
    +
    +

    03 — Section divider

    +
    + + +
    +
    + +
    First section
    +
    04
    +
    +
    +
    + What we do +

    Three points

    +
    +
    +

    One

    Replace this with the real point.

    +

    Two

    Replace this with the real point.

    +

    Three

    Replace this with the real point.

    +
    +
    +
    +
    +

    04 — Three cards

    +
    + + +
    +
    +
    + For your time +

    Thank you

    +

    hello@example.com

    +
    +
    +

    05 — Closing

    +
    + +
    `; + } + + const wantsPricing = /Buy|sign up/.test(A.action?.label || ''); + return `
    + +
    + +
    +
    +
    +
    + New Your announcement +

    Your headline goes here.

    +

    + One or two sentences saying what this is and who it's for. Replace with + the real words before you design around it. +

    + +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    + Trusted by +
    • Client
    • Client
    • Client
    • Client
    +
    +
    + +
    +
    +
    + Features +

    What it does

    +
    +
    +
    +

    One

    Replace with the real benefit.

    +

    Two

    Replace with the real benefit.

    +

    Three

    Replace with the real benefit.

    +

    Four

    Replace with the real benefit.

    +
    +
    +
    + +
    +
    +
    + How it works +

    Three steps

    +
    +
    +
    +
    +

    First step

    Replace with the real step.

    +
    01
    +
    +
    +
    +

    Second step

    Replace with the real step.

    +
    02
    +
    +
    +
    +

    Third step

    Replace with the real step.

    +
    03
    +
    +
    +
    +
    +
    +${wantsPricing ? ` +
    +
    +
    + Pricing +

    Simple plans

    +
    +
    +
    +
    +

    Starter

    For small teams

    +
    $0 / month
    +
    • Feature one
    • Feature two
    + Get started +
    + +
    +

    Enterprise

    For large organisations

    +
    Custom
    +
    • Everything in Pro
    • Dedicated support
    + Contact sales +
    +
    +
    +
    ` : ''} + +
    +
    +
    +
    +

    Your closing line

    +

    One sentence of reassurance.

    +
    + ${wantsPricing ? 'Start free trial' : 'Book a call'} +
    +
    +
    + +
    +
    + Your brand +

    One line about what you do.

    +
    +
    `; +} + +document.getElementById('starter').onclick = async () => { + const btn = document.getElementById('starter'); + const note = document.getElementById('starterNote'); + if (!A.kind) { note.textContent = 'Answer question 1 first — the starter depends on what you\'re making.'; return; } + + btn.textContent = 'Building…'; + const css = await collectCSS(); + if (!css) { + btn.textContent = 'Download starter file'; + note.textContent = 'Can\'t read the stylesheets from a file:// page. Open examples/intake-standalone.html instead, or serve this folder over http — then the starter builds fine.'; + return; + } + + const r = C.custom ? buildRamp(C.accent) : null; + const override = r ? ` +/* Your colours, from question 03a */ +:root { + --accent-50: ${r[50]}; + --accent-100: ${r[100]}; + --accent-300: ${r[300]}; + --accent-500: ${r[500]}; + --accent-600: ${r[600]}; + --accent-700: ${r[700]}; + --accent-on: ${C.on}; + --gradient-hero: linear-gradient(135deg, ${C.g1 || r[600]} 0%, ${r[500]} 55%, ${C.g2 || r[300]} 100%); + --surface-page: ${C.ground}; +}` : ''; + + const theme = A.feel?.accent || 'violet'; + const kind = A.kind.label; + const file = `${slug(kind)}-starter.html`; + + const doc = ` + + + + +${kind} + + + + + +
    + +
    + +
    +
    + +
    + One house style, two layers +

    Everything comes back looking like it belongs together.

    +

    + A codified design system for presentations, PowerPoint templates and + websites. Answer six questions and you get a scope precise enough to hand + to any designer — or paste into an AI tool — and get consistent work back. +

    + +
    + Nine accent themes — one class swaps the whole system + +
    +
    + +
    +
    + + + + + + + + + + + + + + + +
    +
    +
    +
    + + +
    + +
    + + +
    +
    +
    + What's in the box +

    Two layers over one set of tokens

    +
    +
    + +
    +
    + +

    Deck layer — presentations & PowerPoint

    +

    + Fourteen slide archetypes on a 16:9 canvas that composes identically as + a thumbnail or full-screen. Ships with a PowerPoint spec: theme colour + slots, the type scale in points, corner radii as adj values, + and the slide-master layouts. +

    + +
    + +
    + +

    Web layer — landing pages & portfolios

    +

    + Gradient heroes, device mockups, step rails, pricing, FAQ, marquee and a + dark variant. Everything the deck layer deliberately doesn't do, because + a printed slide and a scrolling page are not the same problem. +

    + +
    +
    +
    +
    + + +
    +
    +
    + No repo, no build +

    It's plain CSS. Nothing to install.

    +

    + No framework, no build step, no dependencies, no network requests. Every + file here opens directly in a browser. If you'd rather not clone anything, + the intake will hand you a single self-contained HTML file with your theme + already baked in. +

    + +
    +
      +
    • Nine accent themes — one class re-themes the whole system
    • +
    • Contrast checked per theme, so text on the accent always clears 4.5:1
    • +
    • Uploads stay in your browser; nothing is sent anywhere
    • +
    • Sourced photography constrained to free-licence libraries
    • +
    • Each commissioned job gets its own folder and a recorded spec
    • +
    +
    +
    + +
    +
    +
    + Design System +

    + Distilled from 44 reference templates — 32 corporate decks and 12 web + landing pages — into rules a person or a machine can follow. +

    +
    + +
    +
    + + + diff --git a/design-system/tools/build-standalone.js b/design-system/tools/build-standalone.js new file mode 100644 index 0000000..80dcddd --- /dev/null +++ b/design-system/tools/build-standalone.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node +/** + * Builds examples/intake-standalone.html — the whole intake as one file. + * + * Inlines every linked stylesheet and adds a token-level dark layer, so the + * page can be emailed, opened from a USB stick, or embedded somewhere that + * enforces a strict CSP. No dependencies; run it with plain node. + * + * node design-system/tools/build-standalone.js + * + * Re-run after changing anything in css/ or tokens/. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// The repo is ESM ("type": "module"), so no __dirname — derive it. +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.join(HERE, '..'); +const SRC = path.join(ROOT, 'examples', 'picker.html'); +const OUT = path.join(ROOT, 'examples', 'intake-standalone.html'); + +const SHEETS = [ + 'tokens/tokens.css', + 'css/base.css', + 'css/components.css', + 'css/web.css', +]; + +/* The system is light-first by design — decks stay on a light ground — so the + * dark layer lives here in the build rather than in tokens.css. Because every + * component reads from tokens, redefining surfaces, foregrounds and the ink + * steps that act as borders flips the whole page; nothing needs a dark variant. */ +const DARK = ` + --surface-page: #12161D; + --surface-muted: #191F2A; + --surface-sunken: #0D1116; + --surface-cream: #191F2A; + --surface-inverse: #FFFFFF; + --fg-strong: #F4F6F9; + --fg-body: rgba(244,246,249,0.76); + --fg-muted: rgba(244,246,249,0.52); + --ink-100: rgba(244,246,249,0.10); + --ink-200: rgba(244,246,249,0.17); + --ink-300: rgba(244,246,249,0.30); + --ink-400: rgba(244,246,249,0.48); + --ink-800: #191F2A; + --ink-900: #0D1116; + --chart-grid: rgba(244,246,249,0.12); + --chart-axis: rgba(244,246,249,0.45); + --shadow-sm: 0 1px 2px rgba(0,0,0,0.4); + --shadow-md: 0 4px 16px rgba(0,0,0,0.45); + --shadow-lg: 0 12px 32px rgba(0,0,0,0.5);`; + +const LIGHT = ` + --surface-page: #FFFFFF; --surface-muted: #F7F8FA; --surface-sunken: #EFF1F5; + --surface-cream: #FAF6EC; --surface-inverse: #14181F; + --fg-strong: #14181F; --fg-body: #3A4761; --fg-muted: #8993A6; + --ink-100:#ECEFF3; --ink-200:#D8DDE5; --ink-300:#B4BCC9; --ink-400:#8993A6; + --ink-800:#1B2333; --ink-900:#14181F; + --chart-grid:#ECEFF3; --chart-axis:#8993A6;`; + +const themeLayer = ` +/* ===== theme layer (standalone build only) ===== */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) {${DARK} + } +} +:root[data-theme="dark"] {${DARK} +} +:root[data-theme="light"] {${LIGHT} +} +input[type="color"], input[type="text"], textarea { + color: var(--fg-strong); background: var(--surface-page); +} +`; + +const html = fs.readFileSync(SRC, 'utf8'); +const css = SHEETS + .map(f => `/* ===== ${f} ===== */\n${fs.readFileSync(path.join(ROOT, f), 'utf8')}`) + .join('\n\n'); + +const ownStyle = (html.match(/ + + +${body} + + +`; + +fs.writeFileSync(OUT, out); + +// Guard the one property that makes this file worth having. +const external = out.match(/(?:src|href)="https?:\/\/[^"]+"/g) || []; +if (external.length) { + console.error('External asset references found — the build is not self-contained:'); + external.forEach(e => console.error(' ' + e)); + process.exit(1); +} + +console.log(`Wrote ${path.relative(process.cwd(), OUT)} — ${(out.length / 1024).toFixed(0)}KB, self-contained.`); From fd72dd74f153ef84f21b0963e90979c9745e42f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 04:08:55 +0000 Subject: [PATCH 09/18] Add a client-facing brief: pick a look, no code, no blockers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The intake was written for designers and AI tools — layers, accent hex, class names, a scope block. Send that to a client and you get silence. brief.html asks the same job in four plain questions with no file names, class names or CSS anywhere on the page. Colour is chosen by looking, not by typing a hex. Nine looks are shown as cards that render actual sample type, a button and the palette, named for how they feel rather than what they are — "warm and confident", "calm and trusted", "dark and premium". Two are dark. Every one inherits the contrast guarantee already in the tokens, so no choice can produce unreadable text. The two things that usually stall a project both have a way through. No logo: describe it instead — name, rough style, a sentence on the feel. Nothing written: answer six short questions about the business and the page is drafted from those words, labelled a first draft everywhere it appears because that is what it is. A supplied logo still previews on light, dark and the chosen colour at once, since the usual failure is one that looks right in the navbar and vanishes in the footer. Word documents are read in the browser. A .docx is a zip, so the central directory is walked, word/document.xml located and inflated with the platform's own DecompressionStream — no library and nothing uploaded. Uncompressed entries and corrupt files are both handled, the latter with a message naming the likely cause rather than a stack trace. The output is a finished page carrying the customer's own words, with the brief embedded as a comment for whoever builds from it. Two defects fixed. The CSS collector matched a var(--accent-500) usage rather than the token definition, so it silently inlined this page's own 40 lines instead of the system and produced an unstyled 9KB file; it now matches the definition, and the same latent bug is fixed in the picker. This page's .step class collided with the deck component of the same name and inherited its injected "01" counter above every question — renamed. Verified by a 31-assertion suite: no code strings anywhere in the visible page, nine looks including dark, both logo routes, .docx in compressed and uncompressed form plus graceful failure on a corrupt file, plain text, the drafting path, and a saved page that renders with the chosen colour and the customer's own headline at 1440 and 390. Existing 58-assertion suite, starter files, dark regression and link check all still pass. --- design-system/README.md | 15 +- design-system/SHARING.md | 29 +- design-system/brief-standalone.html | 2288 +++++++++++++++++ design-system/brief.html | 743 ++++++ design-system/examples/intake-standalone.html | 7 +- design-system/examples/picker.html | 5 +- design-system/index.html | 20 +- design-system/tools/build-standalone.js | 24 +- 8 files changed, 3106 insertions(+), 25 deletions(-) create mode 100644 design-system/brief-standalone.html create mode 100644 design-system/brief.html diff --git a/design-system/README.md b/design-system/README.md index 13dd087..be345c6 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -16,6 +16,8 @@ corners, oversized section numerals, and soft geometric ornament. ``` design-system/ ├── index.html ← the site's front door — routes three audiences +├── brief.html ← CLIENT-facing intake: four plain questions, no code +├── brief-standalone.html ← the same, as one file you can email ├── SHARING.md ← how anyone else uses this (link / file / repo) ├── INTAKE.md ← START HERE — six questions asked before any design ├── DESIGN-BRIEF.md ← hand this to any designer or AI tool @@ -54,7 +56,7 @@ Three routes, differing only in what the other person has to install — see | Route | They need | Best for | |---|---|---| | **A link** — `index.html` as a site | A browser | Clients, one-off collaborators | -| **A file** — `examples/intake-standalone.html` | A browser | Email, offline, no internet | +| **A file** — `brief-standalone.html` | A browser | Email, offline, no internet | | **The repo** | git | Designers building with it repeatedly | A GitHub Pages workflow ships in `.github/workflows/pages.yml`. It needs one @@ -66,6 +68,17 @@ someone without the repo: instead of a scope telling them to import files they don't have, they get a single self-contained HTML file with the CSS inlined, their accent baked in, and a scaffold of their chosen sections. +### Two front doors + +`brief.html` is for **clients**: four plain questions, a row of looks to choose +from, and no file names, class names or CSS anywhere on the page. It copes with +having no logo (describe it instead) and no copy (answer six questions and it +drafts the page from your own words), and it reads a `.docx` in the browser +without uploading it. + +`examples/picker.html` is the same job for **designers and AI tools** — layers, +accent hex, class names, the scope block. + ## Starting a new design Don't pick a layout first — **answer the six questions in diff --git a/design-system/SHARING.md b/design-system/SHARING.md index c2f5a23..9ef4170 100644 --- a/design-system/SHARING.md +++ b/design-system/SHARING.md @@ -41,13 +41,35 @@ Cloudflare Pages, S3, or any web server. Nothing to configure. --- +### Two front doors, on purpose + +| Page | Who it's for | What it shows | +|---|---|---| +| **`brief.html`** | Clients and anyone commissioning | Four plain questions, a row of looks to pick from, and somewhere to add a logo and words. **No file names, no class names, no CSS.** | +| **`examples/picker.html`** | Designers, developers, AI tools | The same job in technical terms — layer, accent hex, classes, the scope block. | + +They produce the same thing. Send a client the technical one and you'll get +silence; send them `brief.html` and you'll get a brief back. + +`brief.html` also handles the two cases that stall most projects: + +- **No logo?** They describe what they want instead — name, rough style, a + sentence about the feel. +- **No copy written?** They answer six short questions about the business and it + drafts the page from their own words. Labelled as a first draft everywhere it + appears, because that is what it is. +- **Got a Word file?** It reads `.docx` in the browser — the zip is unpacked and + inflated with the browser's own decompression, so nothing is uploaded. + ## 2. A file — the standalone intake `examples/intake-standalone.html` is the entire intake in **one file**, every stylesheet inlined, no network requests. Email it, put it on a shared drive, open it from a USB stick. It works. -Regenerate it after changing any CSS: +`brief-standalone.html` is the same for the client-facing version. + +Regenerate both after changing any CSS: ```bash node tools/build-standalone.js @@ -94,8 +116,9 @@ a side effect of one page. ## What to send whom **A client, or someone commissioning work** -Send the link to `index.html`, or the standalone intake file. They answer six -questions and send you back a spec. That's the whole ask. +Send `brief.html` — or `brief-standalone.html` as an attachment. Four questions, +no jargon, and a way through even if they have no logo and nothing written. They +send back a page with the brief embedded in it. **A designer or agency** Send `DESIGN-BRIEF.md` plus the filled-in spec. Add `powerpoint/SPEC.md` if the diff --git a/design-system/brief-standalone.html b/design-system/brief-standalone.html new file mode 100644 index 0000000..63bee7b --- /dev/null +++ b/design-system/brief-standalone.html @@ -0,0 +1,2288 @@ + + + + + +Tell us about your project + + + + + +
    + +
    + Project brief +

    Tell us about your project

    +

    + Four questions. There's a way through every one of them even if you don't + have a logo or your words written yet — just say so and we'll handle it. + Nothing you add here is uploaded anywhere; it stays on your computer. +

    +
    + + +
    +
    +
    1

    What do you need?

    +

    Pick the closest one. We can adjust later.

    +
    +
    + + + + +
    +
    + + +
    +
    +
    2

    Which look do you like?

    +

    + Pick whichever feels right — there's no wrong answer, and every one of + these is checked so your text stays easy to read. +

    +
    +
    +
    + + +
    +
    +
    3

    Your logo

    +

    Have one? Add it. Don't? Describe what you'd like and we'll take it from there.

    +
    +
    + + + +
    + + + + +
    + + +
    +
    +
    4

    Your words

    +

    Send us what you've written, or answer a few questions and we'll write a first draft for you.

    +
    +
    + + + +
    + + + + +
    + + +
    +
    +
    + Your brief +
    + 0 of 4 +
    +
    +

    Answer the questions above and your brief appears here.

    +
    +
    + + + +
    +

    +
    +
    + +
    + + + + + diff --git a/design-system/brief.html b/design-system/brief.html new file mode 100644 index 0000000..43ee779 --- /dev/null +++ b/design-system/brief.html @@ -0,0 +1,743 @@ + + + + + +Tell us about your project + + + + + + + +
    + +
    + Project brief +

    Tell us about your project

    +

    + Four questions. There's a way through every one of them even if you don't + have a logo or your words written yet — just say so and we'll handle it. + Nothing you add here is uploaded anywhere; it stays on your computer. +

    +
    + + +
    +
    +
    1

    What do you need?

    +

    Pick the closest one. We can adjust later.

    +
    +
    + + + + +
    +
    + + +
    +
    +
    2

    Which look do you like?

    +

    + Pick whichever feels right — there's no wrong answer, and every one of + these is checked so your text stays easy to read. +

    +
    +
    +
    + + +
    +
    +
    3

    Your logo

    +

    Have one? Add it. Don't? Describe what you'd like and we'll take it from there.

    +
    +
    + + + +
    + + + + +
    + + +
    +
    +
    4

    Your words

    +

    Send us what you've written, or answer a few questions and we'll write a first draft for you.

    +
    +
    + + + +
    + + + + +
    + + +
    +
    +
    + Your brief +
    + 0 of 4 +
    +
    +

    Answer the questions above and your brief appears here.

    +
    +
    + + + +
    +

    +
    +
    + +
    + + + + diff --git a/design-system/examples/intake-standalone.html b/design-system/examples/intake-standalone.html index 726b9dd..fd64114 100644 --- a/design-system/examples/intake-standalone.html +++ b/design-system/examples/intake-standalone.html @@ -4,7 +4,7 @@ Design Intake — Corporate Deck Design System - + @@ -1785,6 +1789,26 @@

    Tell us about your project

    + +
    +
    +
    + Your brief +
    + 0 of 4 +
    +
    +

    Answer the questions above and your brief appears here.

    +
    +
    + + + +
    +

    +
    +
    + - -
    -
    -
    - Your brief -
    - 0 of 4 -
    -
    -

    Answer the questions above and your brief appears here.

    -
    -
    - - - -
    -

    -
    -
    - + + + From 8222068979ee12406fbc4d07c3f0ee417b6da4c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 15:33:00 +0000 Subject: [PATCH 16/18] Wire the pages to accounts, add a projects list, document going live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The brief page now has two lives and asks the server which one it is in rather than guessing from the URL. Served by the Worker it belongs to a signed-in person: the project is saved to their account and the files come from the API. Opened as the standalone file there is no server, and it builds everything locally exactly as before. Added the projects page the account strip links to — without it that link was a 404 I had shipped. Looking at it rendered caught a second defect the tests had missed: the list endpoint returned raw column names while every other brief response returned mapped ones, so every project read "started Invalid Date". Both routes go through publicBrief now, and the suite asserts on the date. Routing simplified. The assets layer already strips .html, so /brief and /signin resolve on their own; /start is a redirect to the one canonical URL rather than a rewrite that left the address bar disagreeing. tests/ carries both suites, since a test that only exists in scratch is a test nobody will run. 32 assertions against a running Worker, 41 against a static server, plus the 58- and 10-assertion suites — all passing. app/README.md documents what is live, what going live costs (Apple needs a paid developer account; without an email provider nobody can sign in), how to switch charging on, and what is still missing — payment, an admin release screen, account deletion, and a privacy policy before real customers. --- app/README.md | 197 +++++++++++++++++++++ app/src/index.js | 17 +- design-system/brief-standalone.html | 145 +++++++++++++-- design-system/brief.html | 145 +++++++++++++-- design-system/projects.html | 152 ++++++++++++++++ tests/README.md | 30 ++++ tests/journey.js | 263 ++++++++++++++++++++++++++++ tests/online.js | 185 +++++++++++++++++++ 8 files changed, 1104 insertions(+), 30 deletions(-) create mode 100644 app/README.md create mode 100644 design-system/projects.html create mode 100644 tests/README.md create mode 100644 tests/journey.js create mode 100644 tests/online.js diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..35349f1 --- /dev/null +++ b/app/README.md @@ -0,0 +1,197 @@ +# The online service + +Accounts, saved projects and server-built downloads for the design system. + +Registration is live. **Downloads are free** — payment is deliberately not +built yet. Every download is counted from day one, because a usage count is the +one thing that cannot be added retrospectively: turn charging on in six months +and you still need to know what everyone already took. + +--- + +## What runs where + +| | | +|---|---| +| **Pages** | `design-system/` served as static assets | +| **API** | A Cloudflare Worker owning `/auth/*` and `/api/*` | +| **Data** | D1 (SQLite) — users, projects, downloads | +| **Files** | Built on demand from the stored answers; nothing is kept | + +Files are rebuilt rather than stored, so improving the generator improves every +past project, not only new ones. + +--- + +## Running it locally + +```bash +cd app +npm install +npm run db:local # create the tables +npm run dev # http://localhost:8787 +``` + +With no mail provider configured, the sign-in route returns the link in its own +response and the page shows it. That happens **only** when `ENVIRONMENT` is +`development` — never in production. + +--- + +## Going live + +### 1. A Cloudflare account + +```bash +npx wrangler login +npx wrangler d1 create design-system +``` + +Put the printed `database_id` into `wrangler.toml`, then create the tables: + +```bash +npm run db:remote +npm run deploy +``` + +That is already a working site: people can register by email, build a project +and download it. + +### 2. Email, so sign-in links arrive + +Without this, nobody can sign in — the link is generated but never sent. + +1. Create a [Resend](https://resend.com) account (free tier is ample) and verify + your sending domain. +2. `npx wrangler secret put RESEND_API_KEY` +3. Set `MAIL_FROM` in `wrangler.toml` to an address on that domain. + +### 3. Google sign-in *(optional)* + +1. Google Cloud console → APIs & Services → Credentials → **OAuth client ID**, + type *Web application*. +2. Authorised redirect URI: `https://yourdomain.com/auth/google/callback` +3. `npx wrangler secret put GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` + +### 4. Apple sign-in *(optional, and the fiddly one)* + +Apple needs a **paid Apple Developer account (~$99/year)**. Worth knowing before +you start, because nothing here works without it. + +1. Register an **App ID**, then a **Services ID** (this is your `APPLE_CLIENT_ID`, + e.g. `com.yourcompany.designsystem.web`). +2. Configure the Services ID: domain `yourdomain.com`, return URL + `https://yourdomain.com/auth/apple/callback`. +3. Create a **Sign in with Apple key** and download the `.p8`. **You can only + download it once.** +4. Set four secrets: + ```bash + npx wrangler secret put APPLE_CLIENT_ID # the Services ID + npx wrangler secret put APPLE_TEAM_ID # Membership page + npx wrangler secret put APPLE_KEY_ID # the key's ID + npx wrangler secret put APPLE_PRIVATE_KEY # the whole .p8 contents + ``` + +Apple only ever sends someone's **name on their first authorisation** — miss it +and it is gone permanently. That is handled, but it is why an Apple user may +have no name where a Google user does. + +**A provider with no credentials simply does not appear on the sign-in page.** +A button that leads to a configuration error is worse than no button. + +--- + +## Turning charging on later + +The check is already written. Switching it on is configuration, not a rewrite. + +```toml +PAYWALL = "on" # in wrangler.toml +``` + +That alone enforces the free allowance: `FREE_DOWNLOADS` in `src/index.js` (2), +then `402 payment_required`. What is still missing is the part that takes money: + +1. **Pick a provider.** For selling internationally, a merchant of record + (Lemon Squeezy, Paddle) is the seller of record and handles VAT in every + country. Stripe is cheaper but leaves tax registration and filing to you. +2. **Add a checkout route** that sends people to the provider's hosted page. + Do not build a card form — see [`SELLING.md`](../design-system/SELLING.md). +3. **Add a webhook** that verifies the provider's signature and writes one row + into `entitlements`: + - a subscription → `kind='subscription'` with `ends_at` + - a single template → `kind='purchase'` with the `brief_id` + +The download check already reads that table first and falls back to the free +allowance, so nothing else changes. This has been tested: with the paywall on +and the allowance spent, inserting a subscription row lets the download through +and records it as `included`. + +### Holding files until you have checked them + +You mentioned confirming a job needs no further work before the files go out. +That is built and off by default: + +```toml +REVIEW_BEFORE_RELEASE = "on" +``` + +Downloads then return `409` with "we're checking your files over" until a brief +reaches `status = 'released'`. **The admin screen to do the releasing is not +built yet** — today you would set it in SQL. Leave this `off` until it is. + +--- + +## Configuration + +| Setting | Where | Default | What it does | +|---|---|---|---| +| `PAYWALL` | `wrangler.toml` | `off` | Enforce the free-download allowance | +| `REVIEW_BEFORE_RELEASE` | `wrangler.toml` | `off` | Hold files until released | +| `SUPPORT_EMAIL` | `wrangler.toml` | — | Printed in the delivered README | +| `RESEND_API_KEY` | secret | — | Sends the sign-in email | +| `GOOGLE_CLIENT_ID` / `_SECRET` | secret | — | Enables Google | +| `APPLE_CLIENT_ID` / `_TEAM_ID` / `_KEY_ID` / `_PRIVATE_KEY` | secret | — | Enables Apple | + +Secrets never go in `wrangler.toml` — it is committed. + +--- + +## How the security holds up + +Each of these is verified by a test against a running Worker, not by argument: + +- **The download is server-only.** It is a `POST`, it assembles the package from + stored answers, and it is the only place a package exists. A page that builds + its own zip can never be gated, which is why generation moved server-side. +- **Sessions and sign-in tokens are stored as SHA-256 hashes.** A leaked backup + yields nothing presentable. +- **One-shot tokens are read and burned in one statement**, so the same link + cannot be used twice — including by two requests racing. +- **ID tokens are fully verified**: signature against the provider's JWKS, + issuer, audience, expiry and nonce. A decoded JWT proves nothing on its own. +- **Email matching requires the provider to have verified it**, so asserting + someone's address at a lax provider cannot claim their account. +- **Customer text is escaped** wherever it reaches markup, and a logo `src` must + be a `data:image` URL. Briefs are stored and later rendered where someone else + is signed in, so this is a real boundary rather than self-inflicted. +- **Cross-origin state changes are rejected** on `Origin`, backing up + `SameSite=Lax`. +- **Another account's project returns 404**, not 403 — no confirmation that an + id exists. + +--- + +## Still to build + +Named honestly, because they are the difference between this and a finished +product: + +- **Taking money.** Everything around it is in place; the provider is not. +- **An admin screen** for releasing files, needed before + `REVIEW_BEFORE_RELEASE` is any use. +- **Account deletion.** People can sign up, so they must be able to leave and + take their data with them. `ON DELETE CASCADE` is in the schema; the route is + not. +- **A privacy policy and terms**, before real customers — you are storing + personal data the moment someone registers. diff --git a/app/src/index.js b/app/src/index.js index 4415e1e..9894a6d 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -70,7 +70,9 @@ async function route(request, env, url, pathname) { if (pathname === '/api/briefs' && method === 'GET') { const { results } = await listBriefs(env.DB, user.id); - return json({ briefs: results || [] }); + // Through publicBrief like every other brief response, so callers see one + // field naming everywhere rather than raw column names from this route. + return json({ briefs: (results || []).map(publicBrief) }); } if (pathname === '/api/briefs' && post) { @@ -212,13 +214,12 @@ function withSecurityHeaders(res, { api = false } = {}) { async function serveStatic(request, env, url) { if (!env.ASSETS) return new Response('Not found', { status: 404 }); - // Pretty routes for the pages people are actually sent links to. - const rewrites = { '/': '/index.html', '/start': '/brief.html', '/signin': '/signin.html', '/projects': '/projects.html' }; - const path = rewrites[url.pathname]; - if (path) { - const rewritten = new URL(url); - rewritten.pathname = path; - return env.ASSETS.fetch(new Request(rewritten, request)); + /* The assets layer already strips .html and canonicalises, so /brief and + * /signin resolve on their own. This only covers the friendlier alias we + * hand out in links, and it redirects rather than rewriting so the address + * bar ends up showing the one canonical URL. */ + if (url.pathname === '/start') { + return new Response(null, { status: 302, headers: { Location: '/brief' } }); } return env.ASSETS.fetch(request); } diff --git a/design-system/brief-standalone.html b/design-system/brief-standalone.html index 07b216e..c84b225 100644 --- a/design-system/brief-standalone.html +++ b/design-system/brief-standalone.html @@ -1631,6 +1631,10 @@ /* Checkbox and its wording stay on one row; only the wording wraps. */ .approve-row { display: flex; gap: var(--space-3); align-items: flex-start; cursor: pointer; } .approve-row input { width: 20px; height: 20px; margin-top: 2px; flex: none; accent-color: var(--accent-500); } + .acct { padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); + background: var(--surface-page); border: 1px solid var(--ink-100); + margin-bottom: var(--space-5); } + .btn--sm { padding: .4rem .9rem; font-size: var(--fs-caption); } @@ -1638,6 +1642,14 @@
    + +
    Project brief

    Tell us about your project

    @@ -2591,6 +2603,72 @@

    ${esc(d.act)}

    } + +/* ---- The account layer ---------------------------------------------------- + This page has two lives. Served by the Worker it belongs to a signed-in + person: briefs are saved to their account and the download comes from the + server. Opened as the standalone file there is no server at all, and it + falls back to building everything locally. `ONLINE` is what decides, and it + is answered by asking the server rather than by guessing from the URL. ---- */ +const ONLINE = { ready: false, signedIn: false, user: null, usage: null, briefId: null }; + +async function api(path, options = {}) { + const res = await fetch(path, { + credentials: 'same-origin', + headers: options.body ? { 'Content-Type': 'application/json' } : {}, + ...options, + }); + const data = await res.json().catch(() => ({})); + return { ok: res.ok, status: res.status, data }; +} + +async function detectAccount() { + if (location.protocol === 'file:') return; + try { + const { ok, data } = await api('/api/me'); + if (!ok) return; + ONLINE.ready = true; + ONLINE.signedIn = Boolean(data.signedIn); + ONLINE.user = data.user || null; + ONLINE.usage = data.usage || null; + } catch { /* no server behind this page; stay in local mode */ } + paintAccount(); +} + +function paintAccount() { + const bar = document.getElementById('acct'); + if (!ONLINE.ready || !ONLINE.signedIn) { bar.hidden = true; return; } + bar.hidden = false; + document.getElementById('acctWho').textContent = ONLINE.user.email; + + const u = ONLINE.usage || {}; + document.getElementById('acctUse').textContent = !u.paywall + ? `${u.used || 0} downloaded` + : u.plan ? 'Subscribed' + : `${u.remaining} of ${u.free} free left`; +} + +/* Saving is what turns a page full of answers into something that survives + closing the tab — and it is what the server later builds the files from. */ +async function saveToAccount() { + if (!ONLINE.ready || !ONLINE.signedIn) return null; + const { ok, data, status } = await api('/api/briefs', { + method: 'POST', + body: JSON.stringify({ id: ONLINE.briefId, title: brandOf(answersOf()) || null, answers: answersOf() }), + }); + if (status === 401) { ONLINE.signedIn = false; paintAccount(); return null; } + if (!ok) return null; + ONLINE.briefId = data.brief.id; + return data.brief; +} + +document.getElementById('acctOut').onclick = async () => { + await api('/auth/signout', { method: 'POST' }); + location.reload(); +}; + +const signInHref = () => `/signin?returnTo=${encodeURIComponent(location.pathname)}`; + /* ---- Handing the answers to the generator -------------------------------- The page keeps `A` in whatever shape the form finds convenient; the module takes one fixed shape. Converting here means the form can change without @@ -2682,24 +2760,67 @@

    ${esc(d.act)}

    const note = document.getElementById('payNote'); const btn = document.getElementById('pack'); btn.textContent = 'Packing…'; - const page = await buildPage(); - if (!page) { + note.textContent = ''; + + try { + /* Online, the server builds and hands over the package — that is the only + way the count, and later the payment, can mean anything. Offline, the + page builds it itself, which is exactly what the standalone file is for. */ + if (ONLINE.ready) { + if (!ONLINE.signedIn) { + note.innerHTML = `Almost there — sign in and your files download straight away. Your first two are free.`; + return; + } + const brief = await saveToAccount(); + if (!brief) { note.textContent = 'We could not save your project. Try again in a moment.'; return; } + + const res = await fetch(`/api/briefs/${brief.id}/download`, { method: 'POST', credentials: 'same-origin' }); + + if (res.status === 402) { + const d = await res.json().catch(() => ({})); + note.textContent = d.message || 'Your free downloads are used up.'; + return; + } + if (res.status === 409) { + const d = await res.json().catch(() => ({})); + note.textContent = d.message || "We're checking your files over."; + return; + } + if (!res.ok) { note.textContent = 'That did not work. Try again in a moment.'; return; } + + const blob = await res.blob(); + const name = (res.headers.get('Content-Disposition') || '').match(/filename="([^"]+)"/)?.[1] + || packageName(answersOf()); + saveBlob(blob, name); + note.textContent = `Saved — ${(blob.size / 1024).toFixed(0)}KB. Unzip it and open index.html to see your page.`; + + const me = await api('/api/me'); + if (me.ok) { ONLINE.usage = me.data.usage; paintAccount(); } + return; + } + + const page = await buildPage(); + if (!page) { + note.textContent = 'To build your files, open this page from a web address rather than straight off your computer.'; + return; + } + const bytes = await makeZip(packageFiles(answersOf(), page, { contact: SHOP.CONTACT })); + const blob = new Blob([bytes], { type: 'application/zip' }); + saveBlob(blob, packageName(answersOf())); + note.textContent = `Saved — ${(blob.size / 1024).toFixed(0)}KB. Unzip it and open index.html to see your page.`; + } finally { btn.textContent = 'Download my files'; - note.textContent = 'To build your files, open this page from a web address rather than straight off your computer.'; - return; } - // makeZip returns bytes; the Worker streams them, the browser wraps them. - const bytes = await makeZip(packageFiles(answersOf(), page, { contact: SHOP.CONTACT })); - const blob = new Blob([bytes], { type: 'application/zip' }); +}; + +function saveBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = packageName(answersOf()); + link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); - btn.textContent = 'Download my files'; - note.textContent = `Saved — ${(blob.size/1024).toFixed(0)}KB. Unzip it and open index.html to see your page.`; -}; +} /* ---- Actions ------------------------------------------------------------ */ document.getElementById('see').onclick = async () => { @@ -2728,6 +2849,8 @@

    ${esc(d.act)}

    document.getElementById('reset').onclick = () => location.reload(); +detectAccount(); + render(); diff --git a/design-system/brief.html b/design-system/brief.html index 074ae6c..1a13892 100644 --- a/design-system/brief.html +++ b/design-system/brief.html @@ -83,11 +83,23 @@ /* Checkbox and its wording stay on one row; only the wording wraps. */ .approve-row { display: flex; gap: var(--space-3); align-items: flex-start; cursor: pointer; } .approve-row input { width: 20px; height: 20px; margin-top: 2px; flex: none; accent-color: var(--accent-500); } + .acct { padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); + background: var(--surface-page); border: 1px solid var(--ink-100); + margin-bottom: var(--space-5); } + .btn--sm { padding: .4rem .9rem; font-size: var(--fs-caption); }
    + +
    Project brief

    Tell us about your project

    @@ -541,6 +553,72 @@

    Tell us about your project

    } + +/* ---- The account layer ---------------------------------------------------- + This page has two lives. Served by the Worker it belongs to a signed-in + person: briefs are saved to their account and the download comes from the + server. Opened as the standalone file there is no server at all, and it + falls back to building everything locally. `ONLINE` is what decides, and it + is answered by asking the server rather than by guessing from the URL. ---- */ +const ONLINE = { ready: false, signedIn: false, user: null, usage: null, briefId: null }; + +async function api(path, options = {}) { + const res = await fetch(path, { + credentials: 'same-origin', + headers: options.body ? { 'Content-Type': 'application/json' } : {}, + ...options, + }); + const data = await res.json().catch(() => ({})); + return { ok: res.ok, status: res.status, data }; +} + +async function detectAccount() { + if (location.protocol === 'file:') return; + try { + const { ok, data } = await api('/api/me'); + if (!ok) return; + ONLINE.ready = true; + ONLINE.signedIn = Boolean(data.signedIn); + ONLINE.user = data.user || null; + ONLINE.usage = data.usage || null; + } catch { /* no server behind this page; stay in local mode */ } + paintAccount(); +} + +function paintAccount() { + const bar = document.getElementById('acct'); + if (!ONLINE.ready || !ONLINE.signedIn) { bar.hidden = true; return; } + bar.hidden = false; + document.getElementById('acctWho').textContent = ONLINE.user.email; + + const u = ONLINE.usage || {}; + document.getElementById('acctUse').textContent = !u.paywall + ? `${u.used || 0} downloaded` + : u.plan ? 'Subscribed' + : `${u.remaining} of ${u.free} free left`; +} + +/* Saving is what turns a page full of answers into something that survives + closing the tab — and it is what the server later builds the files from. */ +async function saveToAccount() { + if (!ONLINE.ready || !ONLINE.signedIn) return null; + const { ok, data, status } = await api('/api/briefs', { + method: 'POST', + body: JSON.stringify({ id: ONLINE.briefId, title: brandOf(answersOf()) || null, answers: answersOf() }), + }); + if (status === 401) { ONLINE.signedIn = false; paintAccount(); return null; } + if (!ok) return null; + ONLINE.briefId = data.brief.id; + return data.brief; +} + +document.getElementById('acctOut').onclick = async () => { + await api('/auth/signout', { method: 'POST' }); + location.reload(); +}; + +const signInHref = () => `/signin?returnTo=${encodeURIComponent(location.pathname)}`; + /* ---- Handing the answers to the generator -------------------------------- The page keeps `A` in whatever shape the form finds convenient; the module takes one fixed shape. Converting here means the form can change without @@ -632,24 +710,67 @@

    Tell us about your project

    const note = document.getElementById('payNote'); const btn = document.getElementById('pack'); btn.textContent = 'Packing…'; - const page = await buildPage(); - if (!page) { + note.textContent = ''; + + try { + /* Online, the server builds and hands over the package — that is the only + way the count, and later the payment, can mean anything. Offline, the + page builds it itself, which is exactly what the standalone file is for. */ + if (ONLINE.ready) { + if (!ONLINE.signedIn) { + note.innerHTML = `Almost there — sign in and your files download straight away. Your first two are free.`; + return; + } + const brief = await saveToAccount(); + if (!brief) { note.textContent = 'We could not save your project. Try again in a moment.'; return; } + + const res = await fetch(`/api/briefs/${brief.id}/download`, { method: 'POST', credentials: 'same-origin' }); + + if (res.status === 402) { + const d = await res.json().catch(() => ({})); + note.textContent = d.message || 'Your free downloads are used up.'; + return; + } + if (res.status === 409) { + const d = await res.json().catch(() => ({})); + note.textContent = d.message || "We're checking your files over."; + return; + } + if (!res.ok) { note.textContent = 'That did not work. Try again in a moment.'; return; } + + const blob = await res.blob(); + const name = (res.headers.get('Content-Disposition') || '').match(/filename="([^"]+)"/)?.[1] + || packageName(answersOf()); + saveBlob(blob, name); + note.textContent = `Saved — ${(blob.size / 1024).toFixed(0)}KB. Unzip it and open index.html to see your page.`; + + const me = await api('/api/me'); + if (me.ok) { ONLINE.usage = me.data.usage; paintAccount(); } + return; + } + + const page = await buildPage(); + if (!page) { + note.textContent = 'To build your files, open this page from a web address rather than straight off your computer.'; + return; + } + const bytes = await makeZip(packageFiles(answersOf(), page, { contact: SHOP.CONTACT })); + const blob = new Blob([bytes], { type: 'application/zip' }); + saveBlob(blob, packageName(answersOf())); + note.textContent = `Saved — ${(blob.size / 1024).toFixed(0)}KB. Unzip it and open index.html to see your page.`; + } finally { btn.textContent = 'Download my files'; - note.textContent = 'To build your files, open this page from a web address rather than straight off your computer.'; - return; } - // makeZip returns bytes; the Worker streams them, the browser wraps them. - const bytes = await makeZip(packageFiles(answersOf(), page, { contact: SHOP.CONTACT })); - const blob = new Blob([bytes], { type: 'application/zip' }); +}; + +function saveBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = packageName(answersOf()); + link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); - btn.textContent = 'Download my files'; - note.textContent = `Saved — ${(blob.size/1024).toFixed(0)}KB. Unzip it and open index.html to see your page.`; -}; +} /* ---- Actions ------------------------------------------------------------ */ document.getElementById('see').onclick = async () => { @@ -678,6 +799,8 @@

    Tell us about your project

    document.getElementById('reset').onclick = () => location.reload(); +detectAccount(); + render(); diff --git a/design-system/projects.html b/design-system/projects.html new file mode 100644 index 0000000..62fdcc9 --- /dev/null +++ b/design-system/projects.html @@ -0,0 +1,152 @@ + + + + + +My projects + + + + + + + + +
    + + + +
    + Your work +

    My projects

    +

    Everything you've started. Pick one up again, or take the files.

    +
    +
    + + +
    + + + +
    + + + + + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..59d5a0e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,30 @@ +# Tests + +Both suites drive a real browser against a really running server. Every defect +worth having found in this project was found this way rather than by reading +the code — an unreadable colour, a squashed layout, a download that silently +built nothing. + +| File | Covers | Needs | +|---|---|---| +| `journey.js` | The offline path: answer, preview, approve, build the zip in the browser | a static server | +| `online.js` | The account path: register, save, download from the API, sign out | the Worker | + +## Running them + +```bash +# offline path +cd design-system && python3 -m http.server 8899 & +DS_BASE=http://127.0.0.1:8899/ node tests/journey.js + +# account path +cd app && npm run dev & +node tests/online.js +``` + +`journey.js` also runs against the Worker origin, but the download assertions +will not pass there: served by the Worker, the page requires an account and +fetches its files from the API. That is the point of the split. + +Both need Playwright (`npm i playwright`) and resolve Chromium from +`PLAYWRIGHT_BROWSERS_PATH`. diff --git a/tests/journey.js b/tests/journey.js new file mode 100644 index 0000000..f0b780d --- /dev/null +++ b/tests/journey.js @@ -0,0 +1,263 @@ +// End-to-end client journey through brief.html: the path a paying customer walks. +const { chromium } = require('playwright'); +const fs = require('fs'); +const { execSync } = require('child_process'); +const DIR = __dirname; +const DS = process.env.DS_BASE || 'file:///home/user/first-pr-practice/design-system/'; + +let pass = 0, fail = 0; +const ok = (n, c, d = '') => { c ? (pass++, console.log(` PASS ${n}`)) : (fail++, console.log(` FAIL ${n}${d ? ' → ' + d : ''}`)); }; +const head = t => console.log(`\n=== ${t} ===`); + +(async () => { + const b = await chromium.launch({ executablePath: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome' }); + const ctx = await b.newContext({ viewport: { width: 1280, height: 1000 }, acceptDownloads: true }); + const p = await ctx.newPage(); + const errs = []; + p.on('pageerror', e => errs.push(e.message)); + p.on('console', m => { + if (m.type() !== 'error') return; + // favicon.ico is missing from the throwaway static server, and headless + // Chromium logs the mailto: hand-off as an error. Neither is the page's doing. + const t = m.text(), u = (m.location() || {}).url || ''; + // /api/me 404s here on purpose: it is how the page discovers there is no + // account layer behind it and falls back to building files locally. + if (!/favicon\.ico|\/api\/me/.test(u) && !/Launched external handler/.test(t)) errs.push(`${t} (${u})`); + }); + p.on('response', r => { if (r.status() >= 400) console.log(' HTTP', r.status(), r.url()); }); + + // Fixtures: a logo and a Word file, as a real client would arrive with. + fs.writeFileSync(DIR + '/j-logo.svg', + `Halevy`); + + await p.goto(DS + 'brief.html'); + await p.waitForTimeout(400); + + head('1. FRONT DOOR — nothing technical is shown'); + { + const txt = await p.evaluate(() => document.body.innerText); + ok('no code shown to the client', !/