Add the design system, and put it online behind accounts - #21
Open
ada33-arch wants to merge 18 commits into
Open
Add the design system, and put it online behind accounts#21ada33-arch wants to merge 18 commits into
ada33-arch wants to merge 18 commits into
Conversation
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.
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.
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.
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.
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.
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".
Testing the system as a shareable page surfaced three real defects. Ground was applied to :root. The colour module's ground control set --surface-page on the document, which overrode the page's own light/dark theme and left light text on white cards — unreadable the moment anyone picked a colour in dark mode. Ground is a setting for the page being designed, not for the tool, so it now previews scoped to the sample block. The spec ignored the colour module. Dialling in a custom hex left the spec still reporting the accent family the feeling question suggested, and the ground from that answer rather than the one actually chosen. The colour handlers called renderColour() directly, which never re-rendered the spec; they now go through render(). A custom hex supersedes the suggested family and reports which text colour clears on it. The deck page overflowed on mobile. .deck--wide asked for a 460px track minimum, which forces the grid wider than a 390px viewport instead of collapsing. Both deck track minimums are now capped with min(px, 100%). Adds examples/intake-standalone.html, a single self-contained build of the picker with every stylesheet inlined and a token-level dark layer, so the intake can be opened or shared without the repo. The layer redefines surfaces, foregrounds and the ink steps used as borders; because the system is entirely token-driven no component needed a dark variant. Source stays light-only, matching the light-ground rule for decks — the dark layer is deliberately build-only. Verified by a 58-assertion Chromium suite covering all six deliverables resolving coherent scopes, the deck+dark guard self-correcting, ramp generation and WCAG contrast across dark, light and failing mid-tone accents, local-only photo upload with resolution and crop warnings, logo previewed on three grounds, copy measured against the 62-character rule, the asset tree, the scope wording, five pages across nine themes plus mobile, and slide overflow at three viewports. Plus a dedicated regression for the ground collision across both files, both schemes and every ground.
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 </style>, </body> and </html> 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 <body>, 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.
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.
… plan The brief now ends where it should: the client looks at their page, approves it, and gets everything as one download. The package is a .zip written by hand in the browser — CRC-32, per-file headers, central directory — so the page keeps its no-dependency promise. Entries deflate through the platform's CompressionStream and fall back to stored when that doesn't help. It carries the finished page, their words as a separate document they can edit without touching markup, the brief they approved, and a plain-English guide to changing text and putting it online. Verified against unzip -t and Python's zipfile rather than trusted. There is deliberately no card form. A static page has no server, so it can neither verify a charge nor hold card details safely, and a form here would be theatre at best and PCI exposure at worst. Checkout hands off to a payment link instead — Stripe, Gumroad, Lemon Squeezy or Paddle all work from a static page. Leave the link unset and nothing pretends: the button emails the brief and says plainly that payment isn't set up yet. SELLING.md states the limitation the flow cannot engineer around: a download in a static page cannot be gated behind a payment, because anyone who can open the page can reach the file. The approval checkbox is a confirmation, not a lock. Three honest ways to handle that are documented, recommending the provider deliver the file. It also flags what to settle before charging — revision policy, that the copy is a draft, who draws a logo that was only described, and the imagery licensing that becomes your problem once you sell the page. ROADMAP.md records the marketplace plan against a trigger of ten or more finished systems, currently one. It argues for building the catalogue before the storefront, keeps a table to count against, and notes that ten genuinely different systems means differing in type personality, structure and job — not ten recolours, which would be one system with themes. Verified by a new 10-assertion suite covering the approval gate, order summary, the unlock, the download, the absence of any card field, and that an unconfigured checkout refuses to pretend. Existing 31, 58, starter, dark and link suites all still pass.
The summary panel was sticky, so "Your brief / See my page / Save my brief / Start again" floated over the questions from the moment the page loaded and stayed there all the way down. It is a recap to read once the questions are answered, not a heads-up display, so it now sits in the flow: questions, then the brief, then approve and download. That also removes the reason the sticky panel existed to work around — it previously overlaid the last question and swallowed its clicks, which an earlier commit patched with a large bottom padding. Nothing needs to dodge it now. Two layout defects the screenshot showed once the panel stopped moving. The "what you get" list put each <strong> and the text following it into separate flex items, so every line wrapped in two places and read as a column of fragments; the copy is now one item. The approval checkbox used the .bar utility, which wraps, so the box detached from its wording — it has its own non-wrapping row now, with only the wording wrapping. Verified by a new 8-assertion suite: the summary is static, starts below the fold, overlaps no question, and appears in the order questions → brief → approve, with every control still sized and the download still working. Existing 10, 31 and 58 assertion suites and the link check pass.
Routing the primary button to the client brief left the hero and the meta description behind: both still promised six questions and a "scope", which is the designer intake's shape and its vocabulary. The route card below already said four. Someone landing on the page was told one thing by the headline and taken somewhere else by the button. The hero now describes what the button actually does — a few plain questions, a page you can look at, and a brief precise enough to hand on — without borrowing the technical word for it.
ada33-arch
marked this pull request as ready for review
August 7, 2026 22:07
Testing the client journey end to end surfaced two readable-text defects that code review had missed: - Green and coral put white on --accent-500 (3.12:1 and 3.47:1). Both clear 4.5:1 with dark ink, so the ramp keeps its colour and the label flips instead. - Amber, teal and lime had an --accent-600 that failed against white (2.39:1, 3.49:1, 2.11:1) — and that token is what links, eyebrows and .text-accent are drawn in. Darkened in-hue until each clears 4.5:1, with --accent-700 pulled down to stay the darker end of the ramp. Darkening 600 broke the primary button's hover, which reused --accent-on against the new fill. Split out --accent-on-600: because every 600 now clears 4.5:1 on white by construction, white is the one value that works on it in all nine themes. Also record the client's name in their brief — the download was already named after them, but the brief itself never said whose job it was. tools/check-contrast.js checks all three pairings from the token file alone, and runs on pull requests as well as at deploy.
Going online means the download has to be built by the server: a package the browser assembles can never be gated behind a sign-in or a payment, because the browser already holds everything it needs. design-system/lib/package.js is now the single source of truth for what a customer receives, and runs unchanged in the browser (instant preview) and in a Worker (the copy actually delivered). Keeping two copies would drift, and the one that drifted would be the one customers got. Escaping came with it. Interpolating customer text raw was harmless while the only person you could attack was yourself; as a multi-user service the brief is stored and later rendered where someone else is signed in, so every value reaching markup is escaped and a logo src must be a data:image URL. Verified by rendering a hostile brief: no script elements, no alerts, payload inert. The brief block sits in an HTML comment, so runs of hyphens are collapsed rather than allowed to close it early. The bundler now inlines the module for the offline build, honouring the import alias — without that the inlined buildPage collided with the page's own wrapper and the script failed to parse. It fails the build if module syntax survives. 41-assertion journey, 58-assertion suite and the 10-assertion package test all pass, served and offline.
Registration is live and downloads are free, as asked. Payment is not built — but the tables and the check that payment needs are, because a usage count cannot be added retrospectively: you can never answer "how many did this person already take?" for anyone who signed up before you started counting. Cloudflare Workers + D1. The Worker owns /auth/* and /api/*; the design system's pages are served as static assets alongside. Three ways in, one account. Google, Apple and an emailed link all resolve through identities to a single user row, so signing in with Google and later by link is one person with one allowance rather than two. Matching on email only happens when the provider actually verified it — otherwise asserting an address at a lax provider would claim someone's account. Sign-in tokens and session cookies are stored as SHA-256 hashes, so a leaked backup yields nothing presentable. One-shot tokens are read and burned in a single statement; a second use of the same link is refused. ID tokens are verified properly — signature against JWKS, issuer, audience, expiry and nonce — since a merely decoded JWT proves nothing. The download is the point of the whole change. It is a POST, it assembles the package server-side from the stored answers, and it is the only place a package exists. Verified against a running Worker: no cookie 401, forged cookie 401, GET 404, cross-origin POST 403, another user's project 404. With the paywall flag on, the third download is refused 402 while a granted subscription entitlement lets it through as "included", and re-downloading something already taken never charges twice. Files are rebuilt from answers rather than stored, so fixing the generator improves every past project, not only new ones.
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.
Generating a real sample and looking at it showed two faults in the words every delivered page carries — neither visible in code, both visible immediately on screen. People type answers, not sentences. "Quarterly planning that survives contact with reality" has no full stop, so joining it to the next clause produced one run-on line: "...with reality Made for operations leads at growing firms." Each clause is closed before the next begins. Worse, an answer could appear twice on one page. The hero headline takes the "why us" answer, and the Why-us section below took the same string — so the page said the identical sentence twice in two type sizes. Same for "what you do" landing in both the hero subline and the section title under it. Each answer now earns exactly one slot, and whichever slot claims it releases the others; a section with nothing left to say is dropped rather than filled with a repeat. Checked across nine combinations of answered and blank fields, including none at all and copy taken from a document: no slot repeats in any of them. 141 assertions still pass.
…back Asked for a link people could try, which means the page has to survive being embedded — and two things in it did not. The preview opened a new tab via window.open. A pop-up blocker refuses that, and a sandboxed frame refuses it always, so the central moment of the whole product could simply not happen for someone following a shared link. It now renders inline in an iframe on the page, with the new tab demoted to a secondary button that says so when it is blocked. Better anyway: you can see an answer change the page without leaving it. tools/build-shareable.js produces the version that goes out as a link — the standalone file plus a banner saying what it is and who it's for, and a feedback panel. There is no server to receive an opinion, so the panel assembles what they chose and what they wrote into one block of text and puts it on the clipboard. The Clipboard API is itself blocked by permissions policy in an embedded frame, so the text appears on screen and selected either way, with a message saying which happened. tests/sandbox.js runs the whole thing inside sandbox="allow-scripts allow-forms" — no pop-ups, no clipboard, no same-origin. 15 assertions, and the rendering confirmed by screenshot, because a nested opaque-origin frame cannot be read by the driver. The fragment guard needed fixing too: it searched the whole file for </body>, which the page generator legitimately emits as text, so it failed a build that was correct. It checks the ends now.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Codifies the house style from two reference sets, adds an intake that scopes each job before anything is designed, and then puts the whole thing online: registration, saved projects, and downloads the server builds and controls.
Charging is deliberately not built. Downloads are free. Every one is counted, because a usage count is the one thing that cannot be added retrospectively — switch charging on in six months and you still need to know what each person already took.
Part one — the design system
Two reference sets, two layers
Set 1 — 32 corporate deck templates. Flat, printed, photography-led.
Set 2 — 12 web landing pages ("favourites"). Screen-native: gradients, glow, product UI as the hero visual.
Folding the second into the first would have blurred both, so the web patterns live in their own layer over shared tokens.
+ css/slides.css+ css/web.cssThe shared rules
Nothing is designed before the intake runs
INTAKE.md+examples/picker.html— six questions, each mapping to a fixed setting, so the same answers always produce the same starting point regardless of who designs it.The output is a scope, not a restatement. It names the exact files to import, the exact classes to compose from, and the one doc section to read — then says explicitly "do not re-read, regenerate or restate the tokens, the components, or the rest of the system." One page costs one file, not a pass over the whole system, and it stops parallel CSS drifting away from the house style.
Colour module
Any hex generates the full 50→700 ramp — lightness anchored to fixed targets rather than offset from the input, so a very dark or very light brand colour still yields a usable 50 and 700 — derives the gradient, applies live, and outputs a
:rootblock for the project's stylesheet.tokens.cssis never edited for one project.Imagery, logo, copy
Uploads render instantly in all four house treatments and are read with
FileReader— they never leave the browser. Sourcing is constrained to free-licence libraries;PHOTOS.mdrules out search-results pages, competitor sites and watermark-removed previews. Logo, copy and reference files get separate slots because the rules differ by kind.Organisation
examples/holds reference demos and doesn't change. Everything commissioned lives inprojects/NNN-short-name/with a requiredSPEC.mdand anassets/tree split by kind.projects/001-portfolio-marin/is a real intake run kept as a worked example — it shows the process catching a conflict between the portfolio archetype's portrait hero and an illustration-only imagery answer, which would otherwise have surfaced as a revision round.Part two — accounts, and a download the server controls
The change everything else rests on
Generation moved out of the page and into
design-system/lib/package.js, imported unchanged by both the browser (instant preview) and the Worker (the copy actually delivered).This was not tidying. While the browser assembled the package, no gate could ever hold — the browser already had everything it needed, so any check was a suggestion. Server-side generation is what makes the free allowance, and later the payment, mean anything.
Two copies of the generator would drift, and the copy that drifted would be the one customers received.
Escaping came with it
Interpolating customer text raw was harmless while the only person you could attack was yourself. As a multi-user service a brief is stored and later rendered where someone else is signed in, so every value reaching markup is escaped and a logo
srcmust be adata:imageURL. Verified by rendering a hostile brief in a browser: no script elements, no alerts, payload inert.Three ways in, one account
Google, Apple and an emailed link all resolve through an
identitiestable to a single user row. Signing in with Google today and by link next month is one person with one allowance, not two.Email addresses are only matched when the provider actually verified them — otherwise asserting someone's address at a lax provider would claim their account.
The download gate
POST /api/briefs/:id/downloadis the only place a package exists. Each of these was tested against a running Worker rather than argued for:401401GETinstead ofPOST404POST403404— not403, so no hint the id exists402200, recorded asincluded200, never charged twiceFiles are rebuilt from stored answers rather than kept, so a fix to the generator improves every past project — the contrast corrections below apply retroactively to work delivered before them.
Contrast defects found by testing the real UI
Two classes of failure, both invisible to code review:
--accent-500— 3.12:1 and 3.47:1 against a 4.5:1 minimum. Every "Your button" in those palette cards was hard to read, on the screen where customers pick their look. The ramps keep their colour; the label flips to ink.--accent-600failing against white — 2.39:1, 3.49:1, 2.11:1. That token is what every link, eyebrow and accent phrase is drawn in. Darkened in-hue until each clears, with--accent-700pulled down to stay the darker end.Darkening 600 broke the primary button's hover, which reused
--accent-onagainst the new fill. Split out--accent-on-600: because every 600 now clears 4.5:1 on white by construction, white is the one value that works on it in all nine themes.tools/check-contrast.jsnow checks all three pairings from the token file alone and runs on every pull request, not only at deploy. Verified to catch the regression by reintroducing the old green.Other defects found by rendering, not reading
position: absoluteon slide furniture.order— the numeral kept its default0and squashed the visual..deck--wideoverflowed a 390px viewport; both deck minimums capped withmin(px, 100%).Verification
141 assertions, all driving a real browser against a really running server.
tests/online.jstests/journey.jsDownloads are verified by unzipping with real
unzip -t, and the delivered page is re-rendered with the network fully blocked to prove it is self-contained.What is deliberately not built
Named plainly, because the gap between this and a finished product should be a short list rather than a vague one:
REVIEW_BEFORE_RELEASEis any use.Setup, costs and the switch-on path are documented in
app/README.md— including that Apple sign-in needs a paid developer account, and that without an email provider nobody can sign in at all.