diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 695bf43..44f922a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ { "name": "desk", "description": "Forty-two design desks, one command, studio quality on the plans you already have.", - "version": "1.0.2", + "version": "1.1.0", "author": { "name": "Aditya Reddy" }, "source": "./", "category": "design", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index dfd798b..283c8cb 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "desk", - "version": "1.0.2", + "version": "1.1.0", "description": "Desk Studio: a design studio inside your coding agent. Forty-two desks staffed per job, roughs, pick, polish, proof and sign-off, studio quality on the plans you already have.", "author": { "name": "Aditya Reddy", "url": "https://deskstudio.ai" }, "homepage": "https://deskstudio.ai", diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index f8e62b4..cc20ea1 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -29,7 +29,7 @@ jobs: - run: sudo apt-get update -q && sudo apt-get install -y -q ffmpeg - run: node site/build.mjs - name: Verify site body - run: echo 'e936190208d7e297e369a282ec27afcc688f8bfb93255f50c4ab332e6d5fc2cd site/dist/index.html' | sha256sum --check + run: echo '8c21ab79c0bff821d2bda925dc774eb21b1c310eb3feb896743f5a60d6751420 site/dist/index.html' | sha256sum --check - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7f692..0ab3170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.1.0](https://github.com/adityareddy-dev/deskstudio/releases/tag/v1.1.0) - 2026-10-31 + +A new dashboard, a brand to match it, and a new film on the site. + +- The dashboard shows every desk as a small bot at its own desk, in a room for its tier, awake only while an agent of that desk is working. The job moves along a track from the brief to the sign-off, with the round, score, cost and time beside it. The review board has a tab per round, a score chart against the bar, and the scorecard. The studio log says what happened in plain words. All controls and routes are unchanged. +- The logo is the bot's head with its one lens eye. The lamp mark is retired. +- A dashboard guide with screenshots in `docs/dashboard.md`, and a 50-second captioned walkthrough. +- `clips/frames.mjs` records the dashboard at 60 frames a second on a virtual clock, replaying real job records. +- The 50-second film on deskstudio.ai and in the README is new: a rendered studio floor, the 42 desks, and one job from brief to sign-off. + ## [1.0.2](https://github.com/adityareddy-dev/deskstudio/releases/tag/v1.0.2) - 2026-10-23 Export a job's event log as an OpenTelemetry trace with role timings, token usage and cost. diff --git a/README.md b/README.md index 0135f51..dd32785 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The first line is for Claude Code. The second is for Codex, Cursor, Gemini CLI, good first issues

-[deskstudio.ai](https://deskstudio.ai) · [Install](docs/install.md) · [Docs](#docs) · [Contribute](#contribute) · [Discussions](https://github.com/adityareddy-dev/deskstudio/discussions) +[deskstudio.ai](https://deskstudio.ai) · [Install](docs/install.md) · [Dashboard guide](docs/dashboard.md) · [Docs](#docs) · [Contribute](#contribute) · [Discussions](https://github.com/adityareddy-dev/deskstudio/discussions) One brief through roughs, pick, polish, pixel proof and scored sign-off @@ -49,6 +49,14 @@ Files land in `.desk/out//` with a short note on who did what, the scores a The outputs, scores and cost are in [What it makes](docs/gallery.md). +## Watch it work + +The dashboard shows the studio as it works. Every desk is a small bot at its own desk, awake while its agent works, and the job moves along a track from the brief to the sign-off with its round, score and cost. + +The floor of a real brand-kit job: desks wake as their agents start and sleep when they stop + +[Watch a whole job on the dashboard, 50 seconds with captions](site/media/walkthrough.mp4). Open it in your project with `/desk:dashboard setup` once, then `/desk:dashboard start`. How to read every part of it is in [the dashboard guide](docs/dashboard.md). + ## Why it exists Cheap models make, strong models judge. Every visual job runs the way a studio runs: roughs, a pick, polish, proof, sign-off. The desks call models through the CLI you are already signed in to (`claude`, `codex` or `agy`) or a key (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). `npx --package deskstudio desk-run doctor` shows which path it will take without making a model call. Scores come from the studio's model reviewers, they are not independent benchmarks. @@ -58,7 +66,7 @@ Cheap models make, strong models judge. Every visual job runs the way a studio r Three roughs on Sonnet, the pick on Opus, polish on Haiku, proof with no model, then the sign-off. The launch card job, retold with its real numbers. The five steps, the retry rules and the pixel checks are in [How a job runs](docs/how-it-works.md). - 42 desks: the Design Manager and 41 that run as subagents. See [the team, cost and models](docs/team-and-cost.md) and [docs/roles.md](docs/roles.md). -- A local [dashboard](docs/dashboard-tour.md) to pause, move a desk to another model, write a desk a note or pick a rough yourself. +- A local [dashboard](docs/dashboard.md) to watch the desks work, pause, move a desk to another model, write a desk a note or pick a rough yourself. - It runs on your machine. The only things that leave it are the model calls the desks make and, for the few research desks, web lookups. The dashboard is 127.0.0.1 only with a token in the URL. See [docs/privacy.md](docs/privacy.md). ## Install and use @@ -75,7 +83,15 @@ Other agents, one line in the project: npx deskstudio add ``` -It writes `.desk/`, copies the desks to `.agents/skills/desk/` and puts one marked block in the agent's instruction file. Every agent by default, one with `--agent codex`. The runner takes `design` for social cards for now; the other commands are Claude Code skills. Older Claude Code versions, the table of instruction files per agent, what each agent gets and the update rules are in [docs/install.md](docs/install.md). +Then open the dashboard and ask for something: + +``` +/desk:dashboard setup +/desk:dashboard start +/desk:design a launch card for LinkedIn +``` + +Outside Claude Code the dashboard is `npx --package deskstudio desk-dashboard setup`, then `start`. `npx deskstudio add` writes `.desk/`, copies the desks to `.agents/skills/desk/` and puts one marked block in the agent's instruction file. Every agent by default, one with `--agent codex`. The runner takes `design` for social cards for now; the other commands are Claude Code skills. Older Claude Code versions, the table of instruction files per agent, what each agent gets and the update rules are in [docs/install.md](docs/install.md). Models are set once in `.desk/project.json`, or from the dashboard for one job: diff --git a/brand/DESIGN.md b/brand/DESIGN.md index 4272d9c..2b6e672 100644 --- a/brand/DESIGN.md +++ b/brand/DESIGN.md @@ -1,180 +1,185 @@ --- name: Desk Studio colors: - background: "#12110f" - foreground: "#f4f0e8" + background: "#15100b" + foreground: "#fcf3e4" accent: "#ffaa1c" - muted: "#9a948a" - surface: "#1c1a17" - line: "#3b3830" - danger: "#ff6b5e" - background-light: "#ffffff" - foreground-light: "#17140f" - accent-light: "#c27a00" - muted-light: "#6b655b" - surface-light: "#f6f3ec" - line-light: "#d9d4c8" - danger-light: "#c4281c" + muted: "#c2b19b" + surface: "#1f1711" + line: "#3b2e23" + danger: "#ff8a4c" + bot: "#f6e9d6" + lens: "#2a1f18" + amber: "#ffaa1c" + green: "#009e73" + pink: "#cc79a7" + blue: "#0072b2" + sky: "#56b4e9" + orange: "#d55e00" + background-light: "#fbf3e6" + foreground-light: "#2b2017" + accent-light: "#9a5b00" + muted-light: "#6f604f" + surface-light: "#fffaf2" + line-light: "#eadbc4" + danger-light: "#b34700" type: family: "Inter" - headingWeight: 600 + headingWeight: 800 bodyWeight: 400 base: 16 ratio: 1.25 spacing: - unit: 8 - radius: 4 + unit: 4 + radius: 10 logo: path: "brand/mark.svg" - clearSpace: "0.5x height" + clearSpace: "0.25x height" motion: - entrance: "300ms cubic-bezier(0.16, 1, 0.3, 1)" + entrance: "320ms cubic-bezier(0.16, 1, 0.3, 1)" + pop: "360ms cubic-bezier(0.34, 1.56, 0.64, 1)" + move: "700ms cubic-bezier(0.65, 0, 0.35, 1)" exit: "200ms cubic-bezier(0.7, 0, 0.84, 0)" - move: "300ms cubic-bezier(0.65, 0, 0.35, 1)" - emphasis: "400ms cubic-bezier(0.16, 1, 0.3, 1)" sources: - - "brand-kit job 20261018-2114-desk-studio-brand-kit, direction 3" - - "pick ring drawn as direction 2's hand-drawn circle, in direction 3's amber" + - "direction of 2026-10-29: a fun dashboard where the studio is visible, the lamp dropped" + - "the dashboard redesign in dashboard/public, then this kit to match it" + - "the six trims shared with the site film (Okabe-Ito, safe for colour vision deficiency)" --- # Desk Studio ## Position -For developers and founders who ship without a designer, 42 desks working inside your agent while you run the dashboard. Warm, plain, steady. +For developers and founders who ship without a designer. A small studio of 42 desks works inside your coding agent, and you watch it work on the dashboard. Warm, plain, a bit playful. ## Voice -Plain, confident, short. Claims, not descriptions. Numbers are figures and stay exact. +Plain, confident, short. Numbers are figures and stay exact. Fun comes from the bots and the motion, never from the words: no hype, no exclamation marks. Example: "Your own design studio." -## The accent's two jobs +## The bots -Amber is a desk at work and the pick: a lit lamp, and the ring drawn round the picked desk or rough. Nothing else is amber. Amber is never a text colour, never a button fill, never a background for text. A lamp at rest is muted, and the slab is always foreground. +Every desk is a small toy bot at its own desk: a cream body, one lens eye, a laptop, and the trim colour of its tier. The bot is the brand. The logo is its head. + +| Tier | Trim | Wears | Count | +|---|---|---|---| +| Manager | amber #ffaa1c | a headset | 1 | +| Director | pink #cc79a7 | a bow tie | 3 | +| Senior | sky #56b4e9 | a lanyard and badge | 17 | +| Builder | green #009e73 | a cap | 21 | + +- On the dashboard, a bot is awake only while an agent of its desk is really working: lens open, typing, its laptop mark lit in the trim. Otherwise it sleeps, lens shut, breathing slowly. Never wake a bot on the dashboard to fill a frame. Illustrations (the banner, the site's crew) can show the crew at work. +- Blue #0072b2 is for links and information, orange #d55e00 for anything that needs you: under the bar, a failed proof, a job waiting on you. +- Amber is the Manager's trim, the job's place on the track and the pick ring. + +## The mark + +- The head of a studio bot: a rounded cream head, two ear bolts, one lens with an amber iris, a dark pupil and a white glint. It sits on a dark tile with a little dusk light in the top left corner (`logo/logo.svg`). +- On light pages use the outlined head (`logo/logo-light.svg`, `logo/mark-on-light.svg`). In one colour use `mark-mono.svg`. +- Smallest size: 16px for the tile. Clear space: a quarter of the mark's height on every side. +- Don't add a body, arms or a second eye to the mark, don't recolour the iris, don't put the cream head straight on white without the outline. ## Colour by job -Dark set (default for the dashboard and the sting) +Dark set, the default for the dashboard | Job | Token | Hex | |---|---|---| -| Page | background | #12110f | -| Text | foreground | #f4f0e8 | -| Lit lamp, pick ring | accent | #ffaa1c | -| Secondary text, unlit lamp, empty slab | muted | #9a948a | -| Desk and panel fill | surface | #1c1a17 | -| Hairlines, resting desk border | line | #3b3830 | -| Score under 8 | danger | #ff6b5e | - -Light set (hero page, sheet, card on white) +| Page, dusk | background | #15100b | +| Panels | surface | #1f1711 | +| Raised panels, inputs | surface-2 | #2a2018 | +| Hairlines | line | #3b2e23 | +| Text | foreground | #fcf3e4 | +| Secondary text | muted | #c2b19b | +| Small labels, times | faint | #a08d77 | +| Bot body | bot | #f6e9d6 | +| Lens | lens | #2a1f18 | +| Desk top, desk front | desk | #b07a4c, #8c5c37 | + +Light set, the studio by day | Job | Token | Hex | |---|---|---| -| Page | background-light | #ffffff | -| Text | foreground-light | #17140f | -| Lit lamp, pick ring | accent-light | #c27a00 | -| Secondary text, unlit lamp, empty slab | muted-light | #6b655b | -| Panel fill | surface-light | #f6f3ec | -| Hairlines | line-light | #d9d4c8 | -| Score under 8 | danger-light | #c4281c | +| Page | background-light | #fbf3e6 | +| Panels | surface-light | #fffaf2 | +| Hairlines | line-light | #eadbc4 | +| Text | foreground-light | #2b2017 | +| Secondary text | muted-light | #6f604f | + +The six trims are the same in both sets. As text they get a lighter shade on dark and a darker one on light, listed in the contrast tables. + +## Type + +Inter, the same face as the site film. 800 for the wordmark and big figures, 700 for headings, 650 for labels and names, 400 for body. Geist Mono for commands, ids and paths only. Figures are tabular. Scale and sizes are in `type.md`. -Background, foreground and accent keep the strategist's hexes in both sets. Nothing else is changed from the strategy. `line` and `line-light` are decorative separators and carry no content, so they have no ratio to meet. +## Shape and depth -## Spacing, radius, elevation +- Radius 10 for controls and cards, 14 for rooms, 18 for panels, 999 for pills and buttons. +- Buttons are pills with a 2px bottom edge, like a plastic key, and press down by 1px. +- Rooms on the floor have a wall band and plank lines. Panels can carry a soft warm glow at the top of the page, nothing else glows. +- Spacing on a 4px grid. -- Unit 8px. Scale in units: 0.5, 1, 2, 3, 4, 6, 8, 12, 16 (4, 8, 16, 24, 32, 48, 64, 96, 128px). Nothing off the scale. -- Radius 0 on the mark, slabs, bars and the dashboard's desks. Radius 4 is the default for panels, inputs and buttons. Radius 8 for cards and the hero's rough frames. Nothing round. -- No shadows. Menus and dialogs may take one 1px `line` border instead. -- Breakpoints 390, 768, 1024, 1440. +## Motion + +Bots wake with a small hop, bob while they work and blink now and then. The job ticket slides along the track. Bubbles pop in. Scorecard bars fill from the left. The pick ring draws once. Confetti falls once, when a job you are watching signs off at the bar. All of it stops under reduced motion. Details in `motion.md`. ## Do -- Set every lit lamp in amber and every other element in foreground, muted or surface. -- Keep the mark's disc 8 units across, over the right third of a square-ended slab. -- Put figures in Inter with tabular numerals wherever they sit in a row. -- Use foreground on background for headlines and body, muted for labels and secondary lines. -- Draw the pass line at 8 on every scorecard, even when every axis passes. -- Show the 42-desk grid as 7 columns by 6 rows, and only where it is the dashboard. -- Draw the pick ring as one open amber stroke round the picked desk or rough: a slightly irregular oval 1.5 times the picked box, centred on it, round caps and joins, no fill, drawn clockwise from the top arc a third in from the left, the end running past the start (8px on rings under 120px wide, 12px from 120px), its last quarter drifting outward so it passes the start with one stroke width of clear space between the two strokes. Stroke 3px under 120px wide, 4px from 120px. One ring per screen. +- Show the floor as rooms by tier, every desk a bot, and only the working ones awake. +- Draw the pass line at 8 on every scorecard and the bar at 8.5 on the score chart. - Write a desk by its job title: Art Director, Typographer. -- Respect reduced motion: show the end state, no fill, draw or stagger. +- Keep figures exact. 0.48 stays 0.48. ## Don't -- Don't set text in amber, on light or on dark. -- Don't put text on an amber fill. -- Don't use a second accent, a gradient, a glow, a shadow or a blur. -- Don't move the disc to the centre, enlarge it, curve the slab or give the unit a face. -- Don't tile the 42-desk grid behind text. -- Don't letterspace the wordmark past 0 or set it uppercase. -- Don't round, dress up or compare a figure. 0.48 stays 0.48. +- Don't fake activity: no bot awake, no score and no cost the record does not have. +- Don't use the old lamp, slab or disc anywhere. They are retired. +- Don't set long text in a trim colour, or small text in amber on light. - Don't use exclamation marks, "Introducing", or any of the house style's banned words. -- Don't close, fill, double or straighten the ring, set it in foreground, or draw it round a line of text. ## Contrast -WCAG 2.x ratios. Text pairs pass at 4.5:1. Large text and non-text pairs pass at 3:1. +WCAG 2.x ratios, measured. Text pairs pass at 4.5:1, large text and non-text at 3:1. Dark set -| Pair | Ratio | Bar | Result | +| Pair | On background | On surface | On surface-2 | |---|---|---|---| -| foreground on background | 16.6:1 | 4.5 | pass | -| foreground on surface | 15.3:1 | 4.5 | pass | -| muted on background | 6.3:1 | 4.5 | pass | -| muted on surface | 5.8:1 | 4.5 | pass | -| danger on background | 6.7:1 | 4.5 | pass | -| danger on surface | 6.2:1 | 4.5 | pass | -| accent on background (lamp, ring, non-text) | 9.9:1 | 3 | pass | -| accent on surface (lamp, ring, non-text) | 9.1:1 | 3 | pass | +| foreground | 17.2 | 16.0 | 14.5 | +| muted | 9.1 | 8.5 | 7.6 | +| faint #a08d77 | 5.9 | 5.5 | 5.0 | +| amber text #ffbf52 | 11.5 | 10.8 | 9.7 | +| green text #3fcf9f | 9.6 | 8.9 | 8.1 | +| pink text #e39cc4 | 8.8 | 8.3 | 7.4 | +| sky text #8ccdf2 | 10.9 | 10.2 | 9.2 | +| orange text #ff8a4c | 8.1 | 7.6 | 6.8 | Light set -| Pair | Ratio | Bar | Result | +| Pair | On background-light | On surface-light | On surface-2-light #f6ecdc | |---|---|---|---| -| foreground-light on background-light | 18.4:1 | 4.5 | pass | -| foreground-light on surface-light | 16.6:1 | 4.5 | pass | -| muted-light on background-light | 5.8:1 | 4.5 | pass | -| muted-light on surface-light | 5.2:1 | 4.5 | pass | -| danger-light on background-light | 5.7:1 | 4.5 | pass | -| danger-light on surface-light | 5.2:1 | 4.5 | pass | -| accent-light on background-light (lamp, ring, non-text) | 3.5:1 | 3 | pass | -| accent-light on surface-light (lamp, non-text) | 3.1:1 | 3 | pass | - -Amber on the light set is for lamps and the ring at 3:1, never text. +| foreground-light | 14.4 | 15.3 | 13.6 | +| muted-light | 5.5 | 5.8 | 5.2 | +| amber text #9a5b00 | 4.9 | 5.2 | 4.6 | +| green text #007a5a | 4.8 | 5.1 | 4.6 | +| pink text #a5497f | 4.9 | 5.2 | 4.7 | +| sky text #1a6fa3 | 5.0 | 5.3 | 4.7 | +| orange text #b34700 | 5.0 | 5.3 | 4.7 | -## Data palettes +Trims as shapes (bars, dots, rings) on the page background -Categorical six, from the accent, built on the Okabe-Ito set that holds under colour vision deficiency. Order is fixed. Every series also gets a direct label, so colour is never the only cue. - -| Slot | Dark | Light | +| Trim | Dark | Light | |---|---|---| -| 1 | #ffaa1c | #c27a00 | -| 2 | #56b4e9 | #1a78b0 | -| 3 | #009e73 | #007a5a | -| 4 | #cc79a7 | #a5497f | -| 5 | #0072b2 | #2a3f9e | -| 6 | #d55e00 | #b34700 | - -The dark slots all clear 3:1 on background (the lowest is #0072b2 at 3.6:1). The light slots all clear 3:1 on background-light. The light set is adjusted from the dark by hand and has not been run through a CVD simulation. +| amber #ffaa1c | 9.9 | 1.7, so the ring on light uses #c27a00 (3.1) | +| green #009e73 | 5.5 | 3.1 | +| pink #cc79a7 | 6.2 | 2.8, decorative only | +| blue #0072b2 | 3.6 | 4.7 | +| sky #56b4e9 | 8.2 | 2.1, the chart line on light uses #1a78b0 (4.4) | +| orange #d55e00 | 4.9 | 3.5 | -Sequential seven, one amber hue, light to heavy, low to high value. +Dark ink #1a120b on amber is 9.7, on the bot's cream 15.5. White on the green button #007a5a is 5.3. -| Step | Dark | Light | -|---|---|---| -| 1 | #3b2d12 | #f7ead2 | -| 2 | #5f4511 | #f0d3a0 | -| 3 | #85600f | #e4b668 | -| 4 | #ad7b10 | #d29a35 | -| 5 | #d49416 | #c27a00 | -| 6 | #ffaa1c | #8f5a00 | -| 7 | #ffd27a | #5c3a00 | - -Diverging pair, amber to blue through a neutral. The pair is separable for protan and deutan viewers. +## Data palettes -| End | Dark | Light | -|---|---|---| -| Low | #56b4e9 | #1a78b0 | -| Mid | #9a948a | #e8e3d8 | -| High | #ffaa1c | #c27a00 | +Categorical six, in this order, safe under colour vision deficiency: amber #ffaa1c, sky #56b4e9, green #009e73, pink #cc79a7, blue #0072b2, orange #d55e00. Every series also gets a direct label. Tiers always keep their own trim: director pink, senior sky, builder green. diff --git a/brand/README.md b/brand/README.md index 24965cc..783e6fb 100644 --- a/brand/README.md +++ b/brand/README.md @@ -1,38 +1,44 @@ -# Desk Studio +# Desk Studio brand -Position: for developers and founders who ship without a designer, 42 desks working inside your agent while you run the dashboard. Warm, plain, steady. +Position: for developers and founders who ship without a designer, a small studio of 42 desks inside your coding agent that you watch work on the dashboard. Warm, plain, a bit playful. -## The mark - -- The mark is a desk with its lamp: a flat slab and an 8-unit disc over the right third. Never move the disc, resize it, curve the slab or add a face. -- Smallest size: mark alone 16px high. Horizontal lockup 120px wide. Stacked lockup 72px wide. Wordmark alone 96px wide. -- Clear space is 0.5 mark heights on all four sides, in every lockup. -- The slab is always foreground. The disc is muted at rest and amber when lit. -- One colour per placement, no gradient, no outline version, no effects. +The brand comes from the dashboard: every desk is a toy bot at its own desk, and the logo is that bot's head. It shares the six trim colours and Inter with the site film. -## Backgrounds +## Files -- Dark set (`background` #12110f) for the dashboard, the sting and dark cards. -- Light set (`background-light` #ffffff) for the hero page, the sheet and white cards. -- Surface (`surface`, `surface-light`) for panels and desks only. Never put the mark on amber. -- Text pairs are in `DESIGN.md`. Use only those. +| File | What it is | +|---|---| +| `DESIGN.md` | Colours by job with measured contrast, type, shape, the bots, do and don't. Its front matter is what the studio's proof reads. | +| `palette.css` | The tokens, dark and light. | +| `type.md`, `motion.md` | The type scale and the moves. | +| `sheet.html` | The kit on one page: mark, wordmark, lockups, the four tiers of bots, colour, type and motion. | +| `mark.svg`, `mark-mono.svg`, `mark-clear.svg` | The bot head in colour, in one colour, and with its clear space. | +| `wordmark.svg`, `lockup-horizontal.svg`, `lockup-stacked.svg` | The words, and the tile with the words, in `currentColor`. | +| `logo/` | The app icon (`logo.svg`, `logo-light.svg`), PNGs from 32 to 1024, `favicon.ico`, the lockups on dark and light, the README banner and the cover images. | +| `brief.md` | The brief and what changed. | -## Accent +## Making the files -Amber is a desk at work and the pick: a lit lamp, and the ring drawn round the picked desk or rough. Nothing else is amber. Never text, never a button, never a fill behind text. On light, amber is 3.5:1 and is for lamps and the ring only. +``` +python brand/logo/build.py # every SVG, the words outlined from Inter 800 +node brand/logo/render.mjs # the PNGs and favicon.ico +node brand/logo/banner.mjs # the README banner, and --og, --devto, --cover variants +``` -## Type +## The mark -Inter, 600 and 400. Wordmark tracking -0.02em. Scale and sizes are in `type.md`. Geist Mono only for the install line. +- A studio bot's head: cream, two ear bolts, one lens with an amber iris. On the dark tile with a little dusk light. +- On light pages, the outlined head. In one colour, `mark-mono.svg`. +- Smallest size 16px. Clear space a quarter of the mark's height. -## The dashboard +## Colour -The 42-desk grid is 7 columns by 6 rows. It appears only as the dashboard, never as wallpaper behind text. +Dark is the default: dusk brown #15100b with cream text. Light is the studio by day, #fbf3e6. The trims: amber #ffaa1c (the Manager, the pick, where the job is), pink #cc79a7 (directors), sky #56b4e9 (seniors), green #009e73 (builders and passes), blue #0072b2 (links and information), orange #d55e00 (anything that needs you). -## Voice +## Type -Plain, confident, short. Numbers are figures. Costs are rounded to the cent and scores to one decimal, the same way everywhere. +Inter 800 for the wordmark and big figures, 700 headings, 650 labels, 400 body. Geist Mono for commands and paths. -## Motion +## Voice -Lamps fill in 300ms, the pick ring draws in 400ms, scorecard slabs fill 300ms each, 60ms apart. All ease-out. Reduced motion shows the end state. +Plain, confident, short. Figures exact. The fun is in the bots and the motion, not in the words. diff --git a/brand/brief.md b/brand/brief.md index e49a98d..bb3d87d 100644 --- a/brand/brief.md +++ b/brand/brief.md @@ -2,6 +2,15 @@ The input for the studio's own brand job (phase 4). The Brand Strategist, Typographer, Design System Lead and UI Designer work from this. Nothing here is final until Adi picks a direction. +## What changed on 2026-10-29 + +The dashboard and the site shots looked dull, nothing showed a fun dashboard, and the lamp wasn't working. So I redesigned the dashboard first and redid the brand to match it. + +- Was: a desk slab with a lamp disc as the mark, a "DS" monogram as the app icon, one amber accent, no characters, no gradients, no robots and no mascot. Now: every desk is a toy bot at its own desk, the mark is that bot's head with one lens eye, and the six trims of the site film carry the tiers (amber Manager, pink directors, sky seniors, green builders, blue for information, orange for what needs you). A soft dusk light is allowed. +- Was: Inter 600 for the wordmark. Now: Inter 800, still the same face as the film. +- The lamp, the slab and the disc are retired everywhere. The old kit's pages (`card.html`, `hero.html`, `marks.html`, `sting.html` and the old `sheet.html`) are in git history; `sheet.html` now shows the new kit. +- The visual starting points and the "Not this" list below are kept as they were written for the first brand job, on 2026-10-18. + ## What it is A design studio inside the coding agent people already use. Forty-two desks, each a real role with a brief, staffed per job by a Design Manager. Work goes through roughs, a pick, polish, proof on real pixels, and a sign-off against an eight-axis rubric. Cheap models make, strong models judge, and the user runs it all from a local dashboard: pause, swap a role, write a desk a note, pick the rough yourself. diff --git a/brand/card.html b/brand/card.html deleted file mode 100644 index bc9e3eb..0000000 --- a/brand/card.html +++ /dev/null @@ -1,38 +0,0 @@ - - - -You run the dashboard. - - -
-

You run
the dashboard.

- - - -
Desk Studio
-
diff --git a/brand/hero.html b/brand/hero.html deleted file mode 100644 index cd2dbe6..0000000 --- a/brand/hero.html +++ /dev/null @@ -1,60 +0,0 @@ - - - -Your own design studio. - - - - - - -
-Desk Studio -
-

Your own
design studio.

-

A full design studio inside the coding agent you already use. One command, free, nothing leaves your machine but the model calls you already make.

-Install the plugin -/plugin install desk@deskstudio -

Open source, MIT. No account, no sign-up.Read the code

-
- -
diff --git a/brand/lockup-horizontal.svg b/brand/lockup-horizontal.svg index 147b469..d6a2953 100644 --- a/brand/lockup-horizontal.svg +++ b/brand/lockup-horizontal.svg @@ -1,6 +1 @@ - - + diff --git a/brand/lockup-stacked.svg b/brand/lockup-stacked.svg index a222201..74ad70e 100644 --- a/brand/lockup-stacked.svg +++ b/brand/lockup-stacked.svg @@ -1,5 +1 @@ - - + diff --git a/brand/logo/banner-devto.png b/brand/logo/banner-devto.png index c25848e..3f49eae 100644 Binary files a/brand/logo/banner-devto.png and b/brand/logo/banner-devto.png differ diff --git a/brand/logo/banner.mjs b/brand/logo/banner.mjs index f92a45a..f3e33ca 100644 --- a/brand/logo/banner.mjs +++ b/brand/logo/banner.mjs @@ -1,17 +1,20 @@ #!/usr/bin/env node -// The README banner: the logo and the line on the left, a tilted wall of the studio's signed-off pieces on the right. -// node brand/logo/banner.mjs brand/logo/banner.png, 1280x400 at 2x -// node brand/logo/banner.mjs --w 1000 --h 420 --out x.png the same design at another size (dev.to cover) -// node brand/logo/banner.mjs --w 1128 --h 191 --cover --out x.png no logo, the line centred (LinkedIn page cover) +// The README banner: the logo and the line on the left, a tilted wall of the studio's signed-off pieces on the right, +// and a row of studio bots at their desks in front of it, one per tier. +// node brand/logo/banner.mjs brand/logo/banner.png, 1280x400 at 2x +// node brand/logo/banner.mjs --w 1000 --h 420 --out x.png the same design at another size (dev.to cover) +// node brand/logo/banner.mjs --w 1128 --h 191 --cover --out x.png no logo, the line centred (LinkedIn page cover) +// node brand/logo/banner.mjs --og --out site/media/og.png the site's share image, 1200x630 at 1x import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { launch } from '../../proof/browser.mjs'; +import { chromium } from 'playwright-core'; const here = dirname(fileURLToPath(import.meta.url)); const args = Object.fromEntries(process.argv.slice(2).map((a, i, all) => a.startsWith('--') ? [a.slice(2), all[i + 1] && !all[i + 1].startsWith('--') ? all[i + 1] : true] : []).filter(Boolean)); -const BW = +(args.w || 1280), BH = +(args.h || 400), k = BH / 400, COVER = !!args.cover; +const OG = !!args.og; +const BW = OG ? 1200 : +(args.w || 1280), BH = OG ? 630 : +(args.h || 400), k = BH / 400, COVER = !!args.cover; const root = join(here, '..', '..'); function pickRender(dir) { @@ -30,42 +33,53 @@ const pieces = []; for (const deliverable of readdirSync(join(root, 'evals', 'golden')).sort()) for (const n of readdirSync(join(root, 'evals', 'golden', deliverable)).sort()) { const dir = join(root, 'evals', 'golden', deliverable, n); - const image = existsSync(join(dir, 'golden.json')) && pickRender(dir); + // The studio's own early pieces carry the retired lamp mark, so the wall leaves them out. + const image = !/^desk-studio/.test(n) && existsSync(join(dir, 'golden.json')) && pickRender(dir); if (image) pieces.push(pathToFileURL(image).href); } const mixed = []; for (let i = 0; pieces.length; i = (i + 7) % pieces.length) mixed.push(pieces.splice(i, 1)[0]); const rows = [0, 1, 2, 3].map(r => [...mixed.slice(r * 5), ...mixed.slice(0, r * 5)].slice(0, 7)); -const font = f => pathToFileURL(join(root, 'fonts', f)).href; +const url = f => pathToFileURL(join(root, f)).href; const logo = readFileSync(join(here, 'logo.svg'), 'utf8').replace(/^]*>/, ' +.row img { height: ${Math.round(128 * k)}px; border-radius: 8px; display: block; box-shadow: 0 8px 22px rgba(0,0,0,.45); } +.fade { position: absolute; inset: 0; background: ${COVER ? 'rgba(21,16,11,.8)' : 'linear-gradient(90deg, #15100b 0%, #15100b 38%, rgba(21,16,11,.84) 54%, rgba(21,16,11,.25) 80%, rgba(21,16,11,.4) 100%)'}; } +.floor { position: absolute; left: ${COVER ? 0 : Math.round(BW * 0.44)}px; right: 0; bottom: 0; height: ${Math.round(botW * 0.62)}px; background: linear-gradient(180deg, rgba(43,32,23,0), rgba(43,32,23,.9) 40%); } +.crew { position: absolute; right: ${Math.round(BW * 0.04)}px; bottom: ${Math.round(BH * 0.04)}px; display: flex; gap: ${Math.round(botW * 0.12)}px; align-items: flex-end; } +.crew .bot-wrap { --bot-w: ${botW}px; } +.copy { position: absolute; ${COVER ? 'left: 0; right: 0; text-align: center;' : `left: ${Math.round(64 * (OG ? 1.1 : k))}px;`} top: ${OG ? '44%' : '50%'}; transform: translateY(-50%); } +.top { display: flex; align-items: center; gap: ${Math.round(20 * k)}px; } +.logo { width: ${Math.round((OG ? 96 : 88) * (OG ? 1 : k))}px; height: ${Math.round((OG ? 96 : 88) * (OG ? 1 : k))}px; } +h1 { font-weight: 800; font-size: ${Math.round((OG ? 66 : 62) * (OG ? 1 : k))}px; letter-spacing: -0.035em; } +p { margin-top: ${COVER ? 0 : Math.round((OG ? 30 : 20) * (OG ? 1 : k))}px; font-weight: 650; font-size: ${COVER ? Math.round(BH * 0.2) : Math.round((OG ? 40 : 30) * (OG ? 1 : k))}px; letter-spacing: -0.015em; max-width: ${OG ? 560 : 9999}px; line-height: 1.15; } +.url { margin-top: ${Math.round(14 * (OG ? 1.2 : k))}px; font: 500 ${COVER ? Math.round(BH * 0.1) : Math.round((OG ? 24 : 20) * (OG ? 1 : k))}px "Geist Mono", monospace; color: #ffbf52; } +${bots.defs}
${rows.map(r => `
${r.map(src => ``).join('')}
`).join('')}
-
-
${COVER ? '' : `
${logo}

Desk Studio

`}

Your own design studio. At your desk.

deskstudio.ai
+
${COVER ? '' : '
'} +${COVER ? '' : `
${crew.map(([t, s, seed]) => `${bots.html(t, s, seed)}`).join('')}
`} +
${COVER ? '' : `
${logo}

Desk Studio

`}

${OG ? 'Your own design studio.' : 'Your own design studio. At your desk.'}

deskstudio.ai
`; const work = mkdtempSync(join(tmpdir(), 'banner-')); const page_ = join(work, 'index.html'); writeFileSync(page_, html); -const browser = await launch(); -const page = await (await browser.newContext({ viewport: { width: BW, height: BH }, deviceScaleFactor: 2 })).newPage(); +const browser = await chromium.launch({ channel: 'chrome', headless: true, args: ['--disable-gpu', '--force-color-profile=srgb'] }); +const page = await (await browser.newContext({ viewport: { width: BW, height: BH }, deviceScaleFactor: OG ? 1 : 2 })).newPage(); await page.goto(pathToFileURL(page_).href, { waitUntil: 'load' }); await page.evaluate(async () => { await document.fonts.ready; await Promise.all([...document.images].map(i => i.decode().catch(() => {}))); }); const outFile = args.out || join(here, 'banner.png'); diff --git a/brand/logo/banner.png b/brand/logo/banner.png index 25e3847..da523fc 100644 Binary files a/brand/logo/banner.png and b/brand/logo/banner.png differ diff --git a/brand/logo/build.py b/brand/logo/build.py index 23e0af8..d2faf55 100644 --- a/brand/logo/build.py +++ b/brand/logo/build.py @@ -1,70 +1,100 @@ -"""The Desk Studio logo: DS with a gold S over the desk bar. The letters are outlined from Inter, so the SVGs need no font. - python brand/logo/build.py writes the SVGs next to this file (needs fonttools and brotli) -The PNGs and favicon.ico are renders of logo.svg and the lockups.""" -import sys +"""The Desk Studio logo: a studio bot's head with its one lens eye, the same bot that works the dashboard's desks. +The wordmark is outlined from Inter 800, so no SVG here needs a font. + python brand/logo/build.py writes brand/*.svg and brand/logo/*.svg (needs fonttools, brotli and uharfbuzz) + node brand/logo/render.mjs then renders the PNGs and favicon.ico from them.""" +import io from pathlib import Path from fontTools.ttLib import TTFont from fontTools.varLib import instancer from fontTools.pens.svgPathPen import SVGPathPen from fontTools.pens.transformPen import TransformPen +import uharfbuzz as hb HERE = Path(__file__).resolve().parent -FONT = HERE.parents[1] / 'fonts' / 'inter' / 'Inter-Variable.woff2' -OUT = Path(sys.argv[1]) if len(sys.argv) > 1 else HERE -OUT.mkdir(parents=True, exist_ok=True) -BG, CREAM, GOLD = '#12110f', '#f4f0e8', '#ffaa1c' +BRAND = HERE.parent +FONT = BRAND.parent / 'fonts' / 'inter' / 'Inter-Variable.woff2' +TILE, PAPER = '#1d150e', '#fbf3e6' +CREAM, CREAM_HI, SHADE = '#f6e9d6', '#fff8ec', '#dcc6a6' +LENS, PUPIL, AMBER, INK = '#2a1f18', '#120d09', '#ffaa1c', '#2b2017' +WORD_WEIGHT, TRACK = 800, -0.03 -_cache = {} -def font(weight): - if weight not in _cache: - _cache[weight] = instancer.instantiateVariableFont(TTFont(FONT), {'wght': weight}) - return _cache[weight] -def text_paths(s, weight, size, x_center, baseline, tracking, colours): - """One SVG path per character, laid out like SVG text with text-anchor middle and letter-spacing.""" - f = font(weight); upm = f['head'].unitsPerEm; sc = size / upm - cmap = f.getBestCmap(); gs = f.getGlyphSet(); hmtx = f['hmtx'] - names = [cmap[ord(c)] for c in s] - total = sum(hmtx[n][0] * sc + tracking for n in names) - x = x_center - total / 2 if x_center is not None else 0 - out = [] - for n, col in zip(names, colours): - pen = SVGPathPen(gs) - gs[n].draw(TransformPen(pen, (sc, 0, 0, -sc, x, baseline))) - if pen.getCommands(): - out.append(f'') - x += hmtx[n][0] * sc + tracking - return ''.join(out), total +def wordmark(size): + """'Desk Studio' as one path, baseline at y=0, kerned by HarfBuzz and tracked at -0.03em. Returns (d, width).""" + font = instancer.instantiateVariableFont(TTFont(FONT), {'wght': WORD_WEIGHT}) + upem = font['head'].unitsPerEm + buf = io.BytesIO(); font.flavor = None; font.save(buf) + hbfont = hb.Font(hb.Face(hb.Blob(buf.getvalue()))) + b = hb.Buffer(); b.add_str('Desk Studio'); b.guess_segment_properties() + hb.shape(hbfont, b, {'kern': True, 'liga': False}) + glyphs, order, sc = font.getGlyphSet(), font.getGlyphOrder(), size / upem + pen = SVGPathPen(glyphs, ntos=lambda v: ('%.2f' % v).rstrip('0').rstrip('.')) + x = 0.0 + for info, pos in zip(b.glyph_infos, b.glyph_positions): + glyphs[order[info.codepoint]].draw(TransformPen(pen, (sc, 0, 0, -sc, x + pos.x_offset * sc, -pos.y_offset * sc))) + x += pos.x_advance * sc + TRACK * size + return pen.getCommands(), x - TRACK * size -def mark(dark=True): - """The DS mark on a 512 canvas, no tile.""" - d = CREAM if dark else BG - letters, _ = text_paths('DS', 800, 230, 256, 330, -12, [d, GOLD]) - bar = f'' - return letters + bar -def svg(w, h, body): - return f'{body}\n' +GRAD = (f'' + f'' + f'' + f'') -files = { - 'logo.svg': svg(512, 512, f'' + mark(True)), - 'logo-light.svg': svg(512, 512, f'' + mark(False)), - 'mark-on-dark.svg': svg(512, 512, mark(True)), - 'mark-on-light.svg': svg(512, 512, mark(False)), -} -# Lockups: the tile at 200 high, then "Desk Studio" in Inter 600. -for name, tile, ink in (('lockup-dark.svg', BG, CREAM), ('lockup-light.svg', CREAM, BG)): - words, width = text_paths('Desk Studio', 600, 120, None, 0, -3, [ink] * 11) - W = int(200 + 48 + width + 64); H = 240 - icon = f'{mark(True)}' - body = f'' + icon + f'{words}' - files[name] = svg(W, H, body) -# Transparent lockups for pages that bring their own background (the README on GitHub's light and dark themes). -for name, ink in (('lockup-on-dark.svg', CREAM), ('lockup-on-light.svg', BG)): - words, width = text_paths('Desk Studio', 600, 120, None, 0, -3, [ink] * 11) - W = int(200 + 48 + width + 24); H = 200 - icon = f'{mark(True)}' - files[name] = svg(W, H, icon + f'{words}') -for n, s in files.items(): - (OUT / n).write_text(s, encoding='utf-8') -print('\n'.join(f'{n} {len(s)} bytes' for n, s in files.items())) + +def head(outline=None): + """The head on a 512 canvas: two ear bolts, the head, the lens with its amber iris, the pupil and the glint.""" + s = f' stroke="{outline}" stroke-width="16"' if outline else '' + fill = '#fffaf1' if outline else 'url(#dsHead)' + ear = SHADE if not outline else '#efdcc0' + return (f'' + f'' + f'' + f'') + + +def head_mono(): + """One colour: the head in currentColor with the lens cut out, the iris a ring inside it.""" + return ('' + '' + '') + + +def tile(bg=TILE, outline=None): + dusk = '' if bg == TILE else '' + return f'{dusk}{head(outline)}' + + +def svg(w, h, body, view=None, extra=''): + return (f'' + f'{GRAD}{body}\n') + + +d, wmw = wordmark(120) +CAP = 120 * 0.727 # Inter's cap height, so the words sit on the tile's centre line +files = {} +# brand/: the kit +files[BRAND / 'mark.svg'] = svg(512, 512, f'{head()}', extra=' role="img" aria-label="Desk Studio"') +files[BRAND / 'mark-mono.svg'] = svg(512, 512, f'{head_mono()}', extra=' role="img" aria-label="Desk Studio"') +files[BRAND / 'mark-clear.svg'] = svg(768, 768, f'{head()}', extra=' role="img" aria-label="Desk Studio, with its clear space"') +files[BRAND / 'wordmark.svg'] = svg(round(wmw + 8), 150, f'', extra=' role="img" aria-label="Desk Studio"') +LW = round(200 + 48 + wmw + 8) +files[BRAND / 'lockup-horizontal.svg'] = svg(LW, 200, f'{tile()}', extra=' role="img" aria-label="Desk Studio"') +SW = round(max(wmw, 240) + 16) +files[BRAND / 'lockup-stacked.svg'] = svg(SW, 400, f'{tile()}', extra=' role="img" aria-label="Desk Studio"') +# brand/logo/: the app icon and the lockups the README, the site and the listings use +L = HERE +files[L / 'logo.svg'] = svg(512, 512, tile()) +files[L / 'logo-light.svg'] = svg(512, 512, tile(PAPER, LENS)) +files[L / 'mark-on-dark.svg'] = svg(512, 512, f'{head()}') +files[L / 'mark-on-light.svg'] = svg(512, 512, f'{head(LENS)}') +for name, bg, ink in (('lockup-dark.svg', TILE, CREAM), ('lockup-light.svg', PAPER, INK)): + W = round(20 + 200 + 48 + wmw + 56) + files[L / name] = svg(W, 240, f'{tile()}') +for name, ink in (('lockup-on-dark.svg', CREAM), ('lockup-on-light.svg', INK)): + W = round(200 + 48 + wmw + 24) + files[L / name] = svg(W, 200, f'{tile()}') +for p, s in files.items(): + p.write_text(s, encoding='utf-8', newline='\n') + print(p.relative_to(BRAND.parent), len(s), 'bytes') diff --git a/brand/logo/cover-linkedin.png b/brand/logo/cover-linkedin.png index da4d3af..ac720b7 100644 Binary files a/brand/logo/cover-linkedin.png and b/brand/logo/cover-linkedin.png differ diff --git a/brand/logo/favicon.ico b/brand/logo/favicon.ico index ef43eb6..801dfd7 100644 Binary files a/brand/logo/favicon.ico and b/brand/logo/favicon.ico differ diff --git a/brand/logo/lockup-dark.png b/brand/logo/lockup-dark.png index 194b505..8e2c7bc 100644 Binary files a/brand/logo/lockup-dark.png and b/brand/logo/lockup-dark.png differ diff --git a/brand/logo/lockup-dark.svg b/brand/logo/lockup-dark.svg index 068ff15..8fd6791 100644 --- a/brand/logo/lockup-dark.svg +++ b/brand/logo/lockup-dark.svg @@ -1 +1 @@ - + diff --git a/brand/logo/lockup-light.png b/brand/logo/lockup-light.png index 775fd66..d07169e 100644 Binary files a/brand/logo/lockup-light.png and b/brand/logo/lockup-light.png differ diff --git a/brand/logo/lockup-light.svg b/brand/logo/lockup-light.svg index beb0a28..1ee6c10 100644 --- a/brand/logo/lockup-light.svg +++ b/brand/logo/lockup-light.svg @@ -1 +1 @@ - + diff --git a/brand/logo/lockup-on-dark.png b/brand/logo/lockup-on-dark.png index 3f86199..22001a5 100644 Binary files a/brand/logo/lockup-on-dark.png and b/brand/logo/lockup-on-dark.png differ diff --git a/brand/logo/lockup-on-dark.svg b/brand/logo/lockup-on-dark.svg index bd5cc2b..2a53794 100644 --- a/brand/logo/lockup-on-dark.svg +++ b/brand/logo/lockup-on-dark.svg @@ -1 +1 @@ - + diff --git a/brand/logo/lockup-on-light.png b/brand/logo/lockup-on-light.png index 306143e..8551bb8 100644 Binary files a/brand/logo/lockup-on-light.png and b/brand/logo/lockup-on-light.png differ diff --git a/brand/logo/lockup-on-light.svg b/brand/logo/lockup-on-light.svg index ea0b183..59d20b8 100644 --- a/brand/logo/lockup-on-light.svg +++ b/brand/logo/lockup-on-light.svg @@ -1 +1 @@ - + diff --git a/brand/logo/logo-1024.png b/brand/logo/logo-1024.png index 1631a0f..bde760c 100644 Binary files a/brand/logo/logo-1024.png and b/brand/logo/logo-1024.png differ diff --git a/brand/logo/logo-180.png b/brand/logo/logo-180.png index c795143..fcc33b5 100644 Binary files a/brand/logo/logo-180.png and b/brand/logo/logo-180.png differ diff --git a/brand/logo/logo-240.png b/brand/logo/logo-240.png index 25d2772..492038f 100644 Binary files a/brand/logo/logo-240.png and b/brand/logo/logo-240.png differ diff --git a/brand/logo/logo-32.png b/brand/logo/logo-32.png index a3f7255..b2af340 100644 Binary files a/brand/logo/logo-32.png and b/brand/logo/logo-32.png differ diff --git a/brand/logo/logo-512.png b/brand/logo/logo-512.png index b103d18..7c99e11 100644 Binary files a/brand/logo/logo-512.png and b/brand/logo/logo-512.png differ diff --git a/brand/logo/logo-light-512.png b/brand/logo/logo-light-512.png index 2ec830a..b2e5f1c 100644 Binary files a/brand/logo/logo-light-512.png and b/brand/logo/logo-light-512.png differ diff --git a/brand/logo/logo-light.svg b/brand/logo/logo-light.svg index 0fce611..6043cfa 100644 --- a/brand/logo/logo-light.svg +++ b/brand/logo/logo-light.svg @@ -1 +1 @@ - + diff --git a/brand/logo/logo.svg b/brand/logo/logo.svg index f0b2685..e7738f0 100644 --- a/brand/logo/logo.svg +++ b/brand/logo/logo.svg @@ -1 +1 @@ - + diff --git a/brand/logo/mark-on-dark.png b/brand/logo/mark-on-dark.png index e20e453..6c5ed20 100644 Binary files a/brand/logo/mark-on-dark.png and b/brand/logo/mark-on-dark.png differ diff --git a/brand/logo/mark-on-dark.svg b/brand/logo/mark-on-dark.svg index b9a703a..72c2f23 100644 --- a/brand/logo/mark-on-dark.svg +++ b/brand/logo/mark-on-dark.svg @@ -1 +1 @@ - + diff --git a/brand/logo/mark-on-light.png b/brand/logo/mark-on-light.png index 3d36d48..baa61e0 100644 Binary files a/brand/logo/mark-on-light.png and b/brand/logo/mark-on-light.png differ diff --git a/brand/logo/mark-on-light.svg b/brand/logo/mark-on-light.svg index 4eef7c6..4204e6f 100644 --- a/brand/logo/mark-on-light.svg +++ b/brand/logo/mark-on-light.svg @@ -1 +1 @@ - + diff --git a/brand/logo/render.mjs b/brand/logo/render.mjs new file mode 100644 index 0000000..acd4910 --- /dev/null +++ b/brand/logo/render.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// Renders the logo PNGs and favicon.ico from the SVGs that build.py writes. Chrome draws them, without the GPU. +// node brand/logo/render.mjs +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { chromium } from 'playwright-core'; + +const here = dirname(fileURLToPath(import.meta.url)); +// [svg, png, width]: the height follows the SVG's own aspect. +const JOBS = [ + ['logo.svg', 'logo-32.png', 32], ['logo.svg', 'logo-180.png', 180], ['logo.svg', 'logo-240.png', 240], + ['logo.svg', 'logo-512.png', 512], ['logo.svg', 'logo-1024.png', 1024], ['logo-light.svg', 'logo-light-512.png', 512], + ['mark-on-dark.svg', 'mark-on-dark.png', 512], ['mark-on-light.svg', 'mark-on-light.png', 512], + ['lockup-dark.svg', 'lockup-dark.png', null], ['lockup-light.svg', 'lockup-light.png', null], + ['lockup-on-dark.svg', 'lockup-on-dark.png', null], ['lockup-on-light.svg', 'lockup-on-light.png', null], +]; +const size = svg => { const m = /viewBox="0 0 ([\d.]+) ([\d.]+)"/.exec(svg); return [Number(m[1]), Number(m[2])]; }; + +const browser = await chromium.launch({ channel: 'chrome', headless: true, args: ['--disable-gpu', '--force-color-profile=srgb'] }); +const page = await (await browser.newContext({ deviceScaleFactor: 1 })).newPage(); +async function render(svg, w) { + const [vw, vh] = size(svg); + const W = w || vw, H = Math.round(W * vh / vw); + await page.setViewportSize({ width: W, height: H }); + await page.setContent(`${svg}`); + return page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: W, height: H } }); +} +for (const [src, out, w] of JOBS) { + const png = await render(readFileSync(join(here, src), 'utf8'), w); + writeFileSync(join(here, out), png); + console.log(out, png.length, 'bytes'); +} +// favicon.ico: 16, 32 and 48 pixel PNGs in one ICO. +const logo = readFileSync(join(here, 'logo.svg'), 'utf8'); +const icons = []; +for (const s of [16, 32, 48]) icons.push([s, await render(logo, s)]); +const head = Buffer.alloc(6 + 16 * icons.length); +head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(icons.length, 4); +let offset = head.length; +icons.forEach(([s, png], i) => { + const o = 6 + i * 16; + head.writeUInt8(s, o); head.writeUInt8(s, o + 1); head.writeUInt8(0, o + 2); head.writeUInt8(0, o + 3); + head.writeUInt16LE(1, o + 4); head.writeUInt16LE(32, o + 6); head.writeUInt32LE(png.length, o + 8); head.writeUInt32LE(offset, o + 12); + offset += png.length; +}); +writeFileSync(join(here, 'favicon.ico'), Buffer.concat([head, ...icons.map(([, p]) => p)])); +console.log('favicon.ico', offset, 'bytes'); +await browser.close(); diff --git a/brand/mark-clear.svg b/brand/mark-clear.svg index 4ede6cd..a6ea1e6 100644 --- a/brand/mark-clear.svg +++ b/brand/mark-clear.svg @@ -1 +1 @@ - + diff --git a/brand/mark-mono.svg b/brand/mark-mono.svg index fc99bce..c667534 100644 --- a/brand/mark-mono.svg +++ b/brand/mark-mono.svg @@ -1 +1 @@ - + diff --git a/brand/mark.svg b/brand/mark.svg index 58346ab..95380d3 100644 --- a/brand/mark.svg +++ b/brand/mark.svg @@ -1 +1 @@ - + diff --git a/brand/marks.html b/brand/marks.html deleted file mode 100644 index 8983cfe..0000000 --- a/brand/marks.html +++ /dev/null @@ -1,18 +0,0 @@ -marks 3

dark

16
24
32
64
400
-
Desk Studio
Desk Studio
-

7 desks edge to edge, 3 lit

same, 4-unit gutter

light

16
24
32
64
400
-
Desk Studio
Desk Studio
-

7 desks edge to edge, 3 lit

same, 4-unit gutter

\ No newline at end of file diff --git a/brand/motion.md b/brand/motion.md index d9569d9..a1188ae 100644 --- a/brand/motion.md +++ b/brand/motion.md @@ -1,31 +1,40 @@ # Motion, Desk Studio -Source of truth is the `motion:` block in DESIGN.md. The table copies it. Where this file and DESIGN.md differ, DESIGN.md wins. +Motion says what the studio is doing. Every move is tied to something in the job's record: a desk starting or stopping, a phase changing, a pick, a score. Nothing moves to look busy. -## The four moves +## The moves | Move | Duration | Easing | Used for | |---|---|---|---| -| entrance | 300ms | cubic-bezier(0.16, 1, 0.3, 1) | a thing arriving: a lamp lighting, a line landing (opacity plus a 16 to 24px rise) | -| settle | 300ms | cubic-bezier(0.65, 0, 0.35, 1) | a thing moving into place (DESIGN.md calls this `move`) | +| entrance | 320ms | cubic-bezier(0.16, 1, 0.3, 1) | a panel, a log line, a card arriving | +| pop | 360ms | cubic-bezier(0.34, 1.56, 0.64, 1) | a speech bubble, a toast, a bot's hop | +| move | 700ms | cubic-bezier(0.65, 0, 0.35, 1) | the job ticket along the track, the cost line | | exit | 200ms | cubic-bezier(0.7, 0, 0.84, 0) | a thing leaving | -| emphasis | 400ms | cubic-bezier(0.16, 1, 0.3, 1) | the one thing to look at: the pick ring | -Linear only for opacity under 150ms and for progress. No bounce. Stagger between items is 60 to 80ms, six items at most per group, except the scorecard's eight slabs at 60ms and the sting's 42-lamp wave at 20ms. +## The bots -## The three dashboard moves +| State | When | What moves | +|---|---|---| +| Asleep | no agent of the desk is working | lens shut, a 4.8s breath, a small "z" now and then, dimmed in the big rooms | +| Waking | an agent of the desk starts | a 520ms hop, the lens opens in 260ms | +| Awake | while it works | a 1.1s bob, typing arms, a blink about every 4s, the pupil looking round, the laptop mark lit in the trim | +| Half awake | the job lists the desk as working but no agent is live | lens half open, no bob | -| Move | Property | Duration | Easing | Stagger | -|---|---|---|---|---| -| A lamp lights | lamp disc `background-color` from muted `#9a948a` to amber `#ffaa1c`. The slab holds still. Flat, no glow, no flash. | 300ms | entrance | 20ms in the sting wave, none for a single desk | -| The pick ring draws | one open amber stroke (`#ffaa1c` on dark, `#c27a00` on light), `pathLength` 1, `stroke-dasharray` 1, `stroke-dashoffset` 1 to 0. Clockwise from the top arc a third in from the left, the end running 8px past the start (12px on rings 120px and wider). Round caps and joins, no fill. It starts once the thing it circles has landed. | 400ms | emphasis | none | -| The scorecard slabs fill | eight slabs, each `transform: scaleX(0)` to `scaleX(1)` from its left edge, left to right. The pass line at 8 is drawn from the first frame and never animates. A slab under 8 stops at its value and stays muted, its figure turns danger `#ff6b5e`. | 300ms each | entrance | 60ms between slabs | -| The cost line grows | one line in three segments, director then senior then builder, in the foreground colour at 100%, 60% and 30%, measured against the next whole dollar. Each segment's `width` moves from its last value to its new one when a desk reports. No amber, no stagger. | 300ms | settle | none | +Each bot gets its own phase offset, so a room never moves in step. -The pick ring appears fully drawn on the card, and it draws on the dashboard and on the sheet. It is not in the hero or the sting, where amber stays the lamps. One ring per screen. +## The job -The sting lights all 42 desks in reading order, 20ms apart, from 200ms (the last starts at 1020ms and is lit by 1320ms), then the end line lands with the entrance move at 1333ms and holds to 3000ms. +- The ticket slides to the next station in 700ms with a small hop, the rail behind it fills green to blue. +- The current station pings once every 1.8s while the job runs, and stops when it is paused or closed. +- Score and cost figures count to their new value in 600ms. +- Scorecard bars fill from the left, 420ms each, 60ms apart. The pass line at 8 never moves. +- The pick ring draws once, clockwise, in 600ms, when the pick lands. +- Confetti in the six trims falls once, about 2s, when a job you are watching signs off at the bar. ## Reduced motion -Under `prefers-reduced-motion: reduce` show the end state with no fill, draw or stagger. Lamps are amber from the first frame, the pick ring is already drawn on the first frame, scorecard slabs are already filled. The only movement left is a 150ms linear crossfade on text that would have landed. No loops, no autoplay. +Under `prefers-reduced-motion: reduce` everything shows its end state: bots are drawn awake or asleep without moving, bars are full, the ring is drawn, no confetti, no pings. + +## Recording + +The dashboard clips are recorded by `clips/frames.mjs` on a virtual clock, one screenshot per frame at 60 fps, so the motion in the clips is the motion in the product. diff --git a/brand/palette.css b/brand/palette.css index 94b526c..a64e338 100644 --- a/brand/palette.css +++ b/brand/palette.css @@ -1,91 +1,80 @@ -/* Desk Studio. Dark is the default, light is designed, not inverted. */ +/* Desk Studio. Dark is the default (the studio at dusk), light is the studio by day. The six trims are shared. */ :root, [data-theme="dark"] { - --background: #12110f; - --foreground: #f4f0e8; - --accent: #ffaa1c; - --muted: #9a948a; - --surface: #1c1a17; - --line: #3b3830; - --danger: #ff6b5e; + --background: #15100b; + --surface: #1f1711; + --surface-2: #2a2018; + --line: #3b2e23; + --foreground: #fcf3e4; + --muted: #c2b19b; + --faint: #a08d77; + --bot: #f6e9d6; + --bot-shade: #dcc6a6; + --lens: #2a1f18; + --desk-top: #b07a4c; + --desk-front: #8c5c37; - --data-1: #ffaa1c; - --data-2: #56b4e9; - --data-3: #009e73; - --data-4: #cc79a7; - --data-5: #0072b2; - --data-6: #d55e00; + --amber: #ffaa1c; + --green: #009e73; + --pink: #cc79a7; + --blue: #0072b2; + --sky: #56b4e9; + --orange: #d55e00; - --seq-1: #3b2d12; - --seq-2: #5f4511; - --seq-3: #85600f; - --seq-4: #ad7b10; - --seq-5: #d49416; - --seq-6: #ffaa1c; - --seq-7: #ffd27a; + --amber-text: #ffbf52; + --green-text: #3fcf9f; + --pink-text: #e39cc4; + --sky-text: #8ccdf2; + --orange-text: #ff8a4c; + --ring: #ffaa1c; - --div-low: #56b4e9; - --div-mid: #9a948a; - --div-high: #ffaa1c; + --tier-manager: var(--amber); + --tier-director: var(--pink); + --tier-senior: var(--sky); + --tier-builder: var(--green); } [data-theme="light"] { - --background: #ffffff; - --foreground: #17140f; - --accent: #c27a00; - --muted: #6b655b; - --surface: #f6f3ec; - --line: #d9d4c8; - --danger: #c4281c; + --background: #fbf3e6; + --surface: #fffaf2; + --surface-2: #f6ecdc; + --line: #eadbc4; + --foreground: #2b2017; + --muted: #6f604f; + --faint: #74634f; + --bot: #fffaf1; + --bot-shade: #e6d2b3; + --desk-top: #d29a63; + --desk-front: #b47c4b; - --data-1: #c27a00; - --data-2: #1a78b0; - --data-3: #007a5a; - --data-4: #a5497f; - --data-5: #2a3f9e; - --data-6: #b34700; - - --seq-1: #f7ead2; - --seq-2: #f0d3a0; - --seq-3: #e4b668; - --seq-4: #d29a35; - --seq-5: #c27a00; - --seq-6: #8f5a00; - --seq-7: #5c3a00; - - --div-low: #1a78b0; - --div-mid: #e8e3d8; - --div-high: #c27a00; + --amber-text: #9a5b00; + --green-text: #007a5a; + --pink-text: #a5497f; + --sky-text: #1a6fa3; + --orange-text: #b34700; + --ring: #c27a00; } @media (prefers-color-scheme: light) { :root:not([data-theme]) { - --background: #ffffff; - --foreground: #17140f; - --accent: #c27a00; - --muted: #6b655b; - --surface: #f6f3ec; - --line: #d9d4c8; - --danger: #c4281c; - - --data-1: #c27a00; - --data-2: #1a78b0; - --data-3: #007a5a; - --data-4: #a5497f; - --data-5: #2a3f9e; - --data-6: #b34700; - - --seq-1: #f7ead2; - --seq-2: #f0d3a0; - --seq-3: #e4b668; - --seq-4: #d29a35; - --seq-5: #c27a00; - --seq-6: #8f5a00; - --seq-7: #5c3a00; + --background: #fbf3e6; + --surface: #fffaf2; + --surface-2: #f6ecdc; + --line: #eadbc4; + --foreground: #2b2017; + --muted: #6f604f; + --faint: #74634f; + --bot: #fffaf1; + --bot-shade: #e6d2b3; + --desk-top: #d29a63; + --desk-front: #b47c4b; - --div-low: #1a78b0; - --div-mid: #e8e3d8; - --div-high: #c27a00; + --amber-text: #9a5b00; + --green-text: #007a5a; + --pink-text: #a5497f; + --sky-text: #1a6fa3; + --orange-text: #b34700; + --ring: #c27a00; } } diff --git a/brand/sheet.html b/brand/sheet.html index cf3dd27..5022070 100644 --- a/brand/sheet.html +++ b/brand/sheet.html @@ -2,291 +2,114 @@ -Desk Studio - + +Desk Studio brand sheet + + - -
- -
-
-

Brand kit

-
Desk Studio
-

Desk Studio

-

For developers and founders who ship without a designer, 42 desks working inside your agent while you run the dashboard. Warm, plain, steady.

-
-
-
-
-
-

02

Mark

- +
+
+ +

Desk Studio

The brand kit. Every desk is a toy bot at its own desk, and the logo is that bot's head. Same face and trims as the site film.

- -

A flat slab 24 by 6 units with square ends, and a disc 8 units across, 4 units above it over the right third. The slab is always foreground. The disc is amber when lit and muted at rest.

-
-
-
-

400px, lit

-
-

64px, lit and at rest

32px, lit and at rest

16px, lit and at rest

+ +
+

Mark and wordmark

+
+
The app icon on the dark tilelogo/logo.svg, the app icon, 16px and up
+
The app icon on lightlogo/logo-light.svg, on light pages
+
The mark in one colourmark-mono.svg, one colour
-
-
-
-

400px, at rest

-
-

64px, lit and at rest

32px, lit and at rest

16px, lit and at rest

+
+
Desk Studio lockup on darklogo/lockup-on-dark.svg
+
Desk Studio lockup on lightlogo/lockup-on-light.svg
-
-
-
-
-
- 0.5x0.5x0.5x0.5x -
-
-

Clear space is 0.5 mark heights on all four sides, in every lockup. x is the mark height.

-
-
-

Minimum sizes

-

Mark alone 16px high

-
Desk Studio

Horizontal lockup 120px wide

-
Desk Studio

Stacked lockup 72px wide

-
Desk Studio

Wordmark alone 96px wide

-
-
-
-
-

03

Lockups and wordmark

- -
- -
-
Desk Studio

Horizontal, light set

-
Desk Studio

Horizontal, dark set

-
-
-
Desk Studio

Stacked, light set

-
Desk Studio

Stacked, dark set

-
-
-

Desk Studio

Wordmark in Inter 600, tracking -0.02em. This is Inter.

-

Desk Studio

Wordmark in the fallback stack: Inter Fallback (Arial, size-adjust 107.4%), ui-sans-serif, Segoe UI, Roboto, Helvetica Neue, Arial. This is the fallback.

-
-
-
-
-

04

Palette by job

- -
- -

Amber is a desk at work and the pick: a lit lamp, and the ring drawn round the picked desk or rough. Nothing else is amber.

-

Dark set, the dashboard and the sting

-

Page

background #12110f

16.6:1

with text, passes 4.5

-

Text

foreground #f4f0e8

16.6:1

on page, passes 4.5

-

Lit lamp, pick ring

accent #ffaa1c

9.9:1

on page (lamp, ring, non-text), passes 3

-

Secondary text

muted #9a948a

6.3:1

on page, passes 4.5

-

Desk and panel fill

surface #1c1a17

15.3:1

with text, passes 4.5

-

Hairline

line #3b3830

No ratio

decorative, no content

-

Score under 8

danger #ff6b5e

6.7:1

on page, passes 4.5

-

Light set, the hero page, the sheet and white cards

-

Page

background-light #ffffff

18.4:1

with text, passes 4.5

-

Text

foreground-light #17140f

18.4:1

on page, passes 4.5

-

Lit lamp, pick ring

accent-light #c27a00

3.5:1

on page (lamp, ring, non-text), passes 3

-

Secondary text

muted-light #6b655b

5.8:1

on page, passes 4.5

-

Panel fill

surface-light #f6f3ec

16.6:1

with text, passes 4.5

-

Hairline

line-light #d9d4c8

No ratio

decorative, no content

-

Score under 8

danger-light #c4281c

5.7:1

on page, passes 4.5

-
-

Data palettes, dark then light

-

Categorical six

-

Sequential seven

-

Diverging pair

-
-
-
-
-

05

Type scale

- -
- -

Inter, 600 and 400. Base 16px, ratio 1.25. Headlines are tight, body is open. Figures use tabular numerals wherever they sit in a row.

-
- -

Display

95.5px, line 1

Weight 600, tracking -0.03em

You run the dashboard.
- -

Headline

76.5px, line 1.05

Weight 600, tracking -0.025em

Your own design studio.
- -

Step 4

39px, line 1.1

Weight 600, tracking -0.015em

It reaches studio quality and shows you the score.
+

Clear space a quarter of the mark's height. Never recolour the iris, never add a body or a second eye to the mark, never set the cream head on white without its outline.

+
-

Title

25px, line 1.2

Weight 600, tracking 0

Open source, MIT. No account, no sign-up.
+
+

The bots

+
+

A bot is awake only while an agent of its desk is really working: lens open, typing, laptop mark lit in its trim. Otherwise it sleeps.

+
-

Step 1

20px, line 1.45

Weight 400, tracking 0

A full design studio inside the coding agent you already use. One command, free, nothing leaves your machine but the model calls you already make.
+
+

Colour

+
+

Dark is the default, the studio at dusk. Light is the studio by day, #fbf3e6. Ratios for every text pair are in DESIGN.md.

+
-

Body

16px, line 1.5

Weight 400, tracking 0

On the plans you already have, cheap models make and strong models judge, and you see what each desk cost.
+
+

Type

+
Wordmark, 800Desk Studio
+
Headline, 800Your own design studio.
+
Figure, 8008.9   $1.34   33m 07s
+
Body, 400A full design studio inside the coding agent you already use.
+
Mono, commands/desk:design a launch card for LinkedIn
+
-

Small

13px, line 1.4

Weight 400, tracking 0

42 desks. 8-axis rubric. Pass bar: every axis 8, average 8.5.
- -

Label

13px, line 1.2

Weight 600, caps, tracking +0.06em

Art Director
- -

Mono

16px, line 1.5

Weight 400, Geist Mono, tracking 0

/plugin install desk@deskstudio
- -

Geist Mono 400 appears only on the install line and the inline desk command, so they read as a command. Figures stay in Inter with tabular numerals.

- -
-
-

06

Spacing and radii

- -
- -
-
-

Spacing, unit 8px

-

0.5 unit, 4px

1 unit, 8px

2 unit, 16px

3 unit, 24px

4 unit, 32px

6 unit, 48px

8 unit, 64px

12 unit, 96px

16 unit, 128px

-

Dashboard desks sit 32px apart on the 8px grid, as on the card and the sting.

-
-
-

Radius, 4 is the default

-
-

Radius 0, the mark, slabs, bars and the dashboard's desks

-
Install the plugin

Radius 4, panels, inputs and buttons

-
You run the dashboard.

Radius 8, cards and the hero's rough frames

-
-
-
-
-
-
-

07

Motion

- -
- - - - - - -
MoveDurationEasingUse
Entrance300mscubic-bezier(0.16, 1, 0.3, 1)A thing arriving: a lamp lighting, a line landing.
Exit200mscubic-bezier(0.7, 0, 0.84, 0)A thing leaving.
Move300mscubic-bezier(0.65, 0, 0.35, 1)A thing moving into place.
Emphasis400mscubic-bezier(0.16, 1, 0.3, 1)The one thing to look at: the pick ring.
-

Under reduced motion, show the end state with no fill, draw or stagger. Lamps are amber from the first frame, the pick ring is already drawn on the first frame, and scorecard slabs are already filled. The only movement left is a 150ms linear crossfade on text that would have landed. No loops, no autoplay.

-
-
-
-

Desk lights

-

The lamp disc fills from muted to amber in 300ms, entrance ease-out. Flat, no glow, no flash, and the slab holds still.

-
-
-

400ms, emphasis, clockwise from the top arc

-

Pick ring

-

One open amber stroke draws round the picked desk or rough, clockwise from the top arc, in 400ms emphasis ease-out, once the thing it circles has landed. Fully drawn on the card, drawn on the dashboard and the sheet, and absent from the hero and the sting.

-
-
-
-

Scorecard

-

Eight slabs fill left to right, 60ms apart and 300ms each, entrance ease-out, against a pass line at 8 that is always drawn. A slab under 8 stops at its value and stays muted, and its figure turns danger.

-
-
-
-
-
-

08

Applications

- -
- -
Hero, desktop
hero.html, desktop 1440x900, shown at 960x600
Hero, phone
hero.html, phone 390x844, shown at 260x563
Card, linkedin
card.html, linkedin 1200x627, shown at 900x470
Card, ig-post
card.html, ig-post 1080x1350, shown at 376x470
Sting at 0s
sting.html, 0s, 1920x1080, shown at 421x237
Sting at 1.33s
sting.html, 1.33s, 1920x1080, shown at 421x237
Sting at 3.0s
sting.html, 3.0s, 1920x1080, shown at 421x237
-
-
-
-

09

Rules

- -
- -
-

Do

  • Set every lit lamp in amber and every other element in foreground, muted or surface.
  • Keep the mark’s disc 8 units across, over the right third of a square-ended slab.
  • Put figures in Inter with tabular numerals wherever they sit in a row.
  • Use foreground on background for headlines and body, muted for labels and secondary lines.
  • Draw the pass line at 8 on every scorecard, even when every axis passes.
  • Show the 42-desk grid as 7 columns by 6 rows, and only where it is the dashboard.
  • Draw the pick ring as one open amber stroke round the picked desk or rough: a slightly irregular oval 1.5 times the picked box, centred on it, round caps and joins, no fill, drawn clockwise from the top arc a third in from the left, the end running past the start (8px on rings under 120px wide, 12px from 120px), its last quarter drifting outward so it passes the start with one stroke width of clear space between the two strokes. Stroke 3px under 120px wide, 4px from 120px. One ring per screen.
  • Write a desk by its job title: Art Director, Typographer.
  • Respect reduced motion: show the end state, no fill, draw or stagger.
-

Don’t

  • Don’t set text in amber, on light or on dark.
  • Don’t put text on an amber fill.
  • Don’t use a second accent, a gradient, a glow, a shadow or a blur.
  • Don’t move the disc to the centre, enlarge it, curve the slab or give the unit a face.
  • Don’t tile the 42-desk grid behind text.
  • Don’t letterspace the wordmark past 0 or set it uppercase.
  • Don’t round, dress up or compare a figure. 0.48 stays 0.48.
  • Don’t use exclamation marks, “Introducing”, or any of the house style’s banned words.
  • Don’t close, fill, double or straighten the ring, set it in foreground, or draw it round a line of text.
-
-
+
+

Motion

+ + + + + + + +
MoveTimeWhere
Wake520ms hop, lens opens in 260msa desk's agent starts
Work1.1s bob, typing, a blink about every 4swhile it works
Sleep4.8s breath, a "z" now and thenno agent at the desk
Ticket700ms slide with a hopthe job moves to the next station
Scorecard420ms per bar, 60ms aparta round is scored
Pick ring600ms, drawn oncethe pick lands
+
+ + - \ No newline at end of file + diff --git a/brand/sting.html b/brand/sting.html deleted file mode 100644 index 4ce585f..0000000 --- a/brand/sting.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - -Desk Studio sting - - - -
- -

Your own
design studio.

- - - - Desk Studio - -
- - diff --git a/brand/type.md b/brand/type.md index 23d86d7..369167d 100644 --- a/brand/type.md +++ b/brand/type.md @@ -2,9 +2,16 @@ ## Family -Inter, variable. One typeface, two weights: 600 for headlines, desk names and labels, 400 for body. Nothing else is bold, light or italic. Inter is the plainest face that holds at 11 to 13px on 42 small desks, so it stays out of the pattern's way. +Inter, variable, the same face as the site film. Geist Mono for commands, job ids and file paths only. -Mono: Geist Mono 400 on the install line and the inline `desk` command only, at 16px, so they read as a command. Figures never use it. Figures stay in Inter with tabular numerals. +| Use | Weight | +|---|---| +| Wordmark, big figures (score, cost, time) | 800 | +| Headings, room signs | 700 | +| Desk names, labels, buttons, chips | 650 | +| Body | 400 | + +Figures are tabular everywhere they sit in a row: `font-variant-numeric: tabular-nums`. ## Fallback stack @@ -21,42 +28,22 @@ font-family: "Inter", "Inter Fallback", ui-sans-serif, -apple-system, "Segoe UI" } ``` -The override values are unverified and get checked in proof. The sheet shows the wordmark in the fallback too. - ## Scale -Base 16px, ratio 1.25, rounded to 0.5px. Headlines are tight, body is open. - -| Step | n | Size px | Line height | Weight | Tracking | -|---|---|---|---|---|---| -| display | 8 | 95.5 | 1.0 | 600 | -0.03em | -| headline | 7 | 76.5 | 1.05 | 600 | -0.025em | -| 39 | 4 | 39 | 1.1 | 600 | -0.015em | -| title | 2 | 25 | 1.2 | 600 | 0 | -| 20 | 1 | 20 | 1.45 | 400 | 0 | -| body | 0 | 16 | 1.5 | 400 | 0 | -| small | -1 | 13 | 1.4 | 400 | 0 | -| label | -1 | 13 | 1.2 | 600, caps | +0.06em | - -Measure: body 60ch, never over 70ch. Headlines run two lines at most on every surface. Nothing under 13px except desk figures on the dashboard at 11px minimum. +| Step | Size px | Line height | Weight | Tracking | +|---|---|---|---|---| +| display | 76 | 1.02 | 800 | -0.035em | +| headline | 48 | 1.08 | 800 | -0.03em | +| title | 30 | 1.15 | 700 | -0.02em | +| stat figure | 24 | 1.1 | 800 | -0.02em | +| lead | 20 | 1.45 | 400 | 0 | +| body | 16 | 1.5 | 400 | 0 | +| small | 13 | 1.45 | 400 | 0 | +| label | 11 | 1.2 | 650, caps | +0.07em | +| desk name | 10.5 to 12.5 | 1.15 | 650 | -0.01em | -## Numerals - -Tabular lining on dashboard desks, timers, costs and scores: `font-variant-numeric: tabular-nums lining-nums`. Proportional in prose and headlines. +Nothing under 10.5px. Body measure 60 to 70 characters. ## Wordmark -"Desk Studio" in Inter 600, tracking -0.02em. Never uppercase, never opened past 0. - -## Surfaces - -| Surface | Sizes | -|---|---| -| Dashboard (dark UI, 42 desks) | desk name 13px / 600, figures 13px / 400 tabular | -| Hero desktop | headline 76.5px, subline 20px, install line 16px Geist Mono, open line and Read the code 16px / 400 | -| Hero phone | headline 39px, subline, install line, open line and link 16px | -| Card linkedin 1200x627 | headline 95.5px, two lines, no support line | -| Card ig-post 1080x1350 | headline 95.5px, two lines, no support line | -| Sting 1920x1080 | end line 95.5px, two lines, held 1.37s | - -Hierarchy is made by size and weight only. Amber is never a text colour. Typographic quotes and dashes, plain figures for numbers. +"Desk Studio" in Inter 800, tracking -0.03em, outlined in `wordmark.svg` and the lockups by `logo/build.py`. Never uppercase. diff --git a/brand/wordmark.svg b/brand/wordmark.svg index 0ee3a26..e1db0a9 100644 --- a/brand/wordmark.svg +++ b/brand/wordmark.svg @@ -1 +1 @@ - + diff --git a/clips/clips.mjs b/clips/clips.mjs index d7fd796..9d20dfb 100644 --- a/clips/clips.mjs +++ b/clips/clips.mjs @@ -32,7 +32,7 @@ const polish = (t, n) => (t.final?.desks || []).find(d => String(d.task || '').t export const CLIPS = [ { - name: 'floor-idle', seconds: 10, loop: true, about: 'The 42 desks, a lamp lighting as a desk starts work.', + name: 'floor-idle', seconds: 10, loop: true, about: 'The 42 desks, a bot waking as a desk starts work.', caption: 'Forty-two desks.', fresh: true, job: null, // An empty job on the dashboard, the camera on the grid, desks starting and stopping one after another. @@ -41,7 +41,7 @@ export const CLIPS = [ const a = t.start('typographer', 'type scale', SONNET); await t.wait(1100); const b = t.start('icon-designer', 'mark on the grid', HAIKU); await t.wait(900); const c = t.start('copy-editor', 'read the headline', SONNET); await t.wait(1100); - const d = t.start('motion-designer', 'lamp timing', HAIKU); await t.wait(900); + const d = t.start('motion-designer', 'wake timing', HAIKU); await t.wait(900); t.stop('typographer', a, { model: ID[SONNET] }); const e = t.start('art-director', 'pick', OPUS); await t.wait(1200); const f = t.start('email-designer', 'dark set', HAIKU); await t.wait(900); t.stop('icon-designer', b); t.stop('copy-editor', c, { model: ID[SONNET] }); await t.wait(1000); diff --git a/clips/explainer.mjs b/clips/explainer.mjs index f9f3929..7ba958a 100644 --- a/clips/explainer.mjs +++ b/clips/explainer.mjs @@ -64,8 +64,8 @@ const F = FW === 1920 ? { term: [1120, 34], dock: [40, 36, 0.38], cap: [40, 24, : FH > FW ? { term: [940, 29], dock: [40, 70, 0.5], cap: [52, 28, 150], hook: [118, 128, 56], wall: { rows: 9, th: 190, gap: 16 } } : { term: [940, 29], dock: [28, 28, 0.44], cap: [34, 21, 44], hook: [66, 72, 32], wall: { rows: 5, th: 168, gap: 16 } }; -// The end card carries the logo from brand/logo. Its S starts cream and turns gold when the lamp used to come on. -const LOGO = readFileSync(join(root, 'brand', 'logo', 'logo.svg'), 'utf8').trim().replace(/^]*>/, ''; + document.body.append(p); + }); + // Let the page load its state and the event backlog before the first frame. + const settle = async (minMsgs = 0, minStates = 0) => { + const t0 = Date.now(); + for (;;) { + const ok = await page.evaluate(([m, s]) => window.__vc.msgs >= m && window.__vc.states >= s && window.__settled(), [minMsgs, minStates]); + if (ok) return true; + if (Date.now() - t0 > 6000) { console.error(` settle timed out at msgs ${minMsgs} states ${minStates}`); return false; } + await new Promise(r => setTimeout(r, 12)); + } + }; + let expectMsgs = prev.events.length, expectStates = 0; + await page.waitForFunction(n => window.__vc.msgs >= n, expectMsgs, { timeout: 10000 }).catch(() => {}); + for (let i = 0; i < 40; i++) { await page.evaluate(([d]) => window.__step(50, d), [T(from) + shift]); await settle(); } + + // the camera + const cam = { y: 0, from: null, zoom: null }; + // A camera target is a y, a selector to put under the header, or { sel, off } to put an element off pixels down. + const yOf = async target => typeof target === 'number' ? target : page.evaluate(([sel, off]) => { const el = document.querySelector(sel); return el ? Math.max(0, el.getBoundingClientRect().top + scrollY - off) : scrollY; }, typeof target === 'object' ? [target.sel, target.off] : [target, clip.camOffset ?? 66]); + const zoomOf = async z => { + if (!z) return { x: 0, y: 0, w: vw }; + if (typeof z.sel === 'string') return page.evaluate(([sel, w, pad]) => { const r = document.querySelector(sel).getBoundingClientRect(); const h = w * 9 / 16; return { x: Math.max(0, Math.min(innerWidth - w, r.left + r.width / 2 - w / 2)), y: Math.max(0, Math.min(innerHeight - h, r.top + r.height / 2 - h / 2 + pad)), w }; }, [z.sel, z.w, z.dy || 0]); + return { x: z.x, y: z.y, w: z.w }; + }; + const moves = (clip.camera || []).map(([a, b, target]) => ({ a, b, target, y0: null, y1: null })); + const zooms = (clip.zoom || []).map(([a, b, z]) => ({ a, b, z, r0: null, r1: null })); + let zoomNow = { x: 0, y: 0, w: vw }; + const ptr = { x: vw * 0.7, y: vh * 0.75, shown: 0 }; + const acts = (clip.actions || []).map(([at, fn]) => ({ at, fn, done: false })); + const captions = clip.captions || (clip.caption ? [[0.2, seconds - 0.2, clip.caption]] : []); + // the tools a clip's actions get + const tools = { + page, rec, + point: async (sel, dur = 0.6) => { + const r = await page.evaluate(s => { const el = document.querySelector(s); if (!el) return null; el.scrollIntoView({ block: 'nearest' }); const b = el.getBoundingClientRect(); return { x: b.left + b.width / 2, y: b.top + b.height / 2 }; }, sel); + if (r) pointer.push({ from: { x: ptr.x, y: ptr.y }, to: r, a: vNow, b: vNow + dur }); + return r; + }, + click: async sel => { await page.locator(sel).first().click({ noWaitAfter: true }); ptr.pulse = vNow; }, + type: async (sel, text, perChar = 2) => { await page.locator(sel).first().focus(); typing.push({ text, perChar, i: 0, f: 0 }); }, + select: async (sel, value) => { await page.selectOption(sel, value); }, + }; + const pointer = []; + const typing = []; + let vNow = from; + + mkdirSync(OUT, { recursive: true }); + const base = join(OUT, clip.name); + const master = `${base}.master.mp4`; + const enc = spawn('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'image2pipe', '-framerate', String(FPS), '-c:v', 'mjpeg', '-i', '-', '-c:v', 'libx264', '-preset', 'fast', '-crf', '12', '-pix_fmt', 'yuv420p', '-r', String(FPS), '-threads', '8', master], { stdio: ['pipe', 'inherit', 'inherit'] }); + const started = Date.now(); + for (let f = 0; f < frames; f++) { + const v = from + f / FPS; + vNow = v; + const t = T(v); + // the job as it stood at t + if (!jobWritten && t >= jobStart) { jobWritten = true; R.writeDesk(desk, rec, R.rebase(R.at(rec, t), shift)); prev = R.rebase(R.at(rec, t), shift); expectMsgs = prev.events.length; expectStates++; await settle(expectMsgs, expectStates); } + else if (jobWritten) { + const snap = R.rebase(R.at(rec, t), shift); + const ch = R.advance(desk, rec, prev, snap); + prev = snap; + if (ch.events) expectMsgs += ch.events; + if (ch.state) expectStates++; + if (ch.events || ch.state) await settle(expectMsgs, expectStates); + } else { + const snap = R.rebase(R.at(rec, t), shift); + const extra = snap.events.slice(prev.events.length).filter(e => !e.id || e.id !== rec.id); + if (extra.length) { R.appendEvents(desk, extra); expectMsgs += extra.length; await settle(expectMsgs, expectStates); } + prev = { ...prev, events: snap.events }; + } + for (const a of acts) if (!a.done && v >= a.at) { a.done = true; await a.fn(tools); await settle(expectMsgs, expectStates); } + for (const ty of typing) { if (ty.i < ty.text.length && ty.f++ % ty.perChar === 0) { await page.keyboard.type(ty.text[ty.i++]); } } + // camera + // The latest move that has started steers the camera. Once it lands it keeps following its target, so a panel that + // grows above the target does not push it out of frame. + const m = moves.filter(x => v >= x.a).at(-1); + if (m) { + if (m.y0 === null) { m.y0 = cam.y; m.y1 = await yOf(m.target); } + const k = ease(Math.min(1, (v - m.a) / Math.max(1e-6, m.b - m.a))); + if (k >= 1 && typeof m.target !== 'number') m.y1 = await yOf(m.target); + cam.y = m.y0 + (m.y1 - m.y0) * k; + } + for (const z of zooms) { + if (v < z.a) continue; + if (z.r0 === null) { z.r0 = { ...zoomNow }; z.r1 = await zoomOf(z.z); } + const k = ease(Math.min(1, (v - z.a) / Math.max(1e-6, z.b - z.a))); + zoomNow = { x: z.r0.x + (z.r1.x - z.r0.x) * k, y: z.r0.y + (z.r1.y - z.r0.y) * k, w: z.r0.w + (z.r1.w - z.r0.w) * k }; + } + for (const p of pointer) if (v >= p.a) { const k = ease(Math.min(1, (v - p.a) / (p.b - p.a))); ptr.x = p.from.x + (p.to.x - p.from.x) * k; ptr.y = p.from.y + (p.to.y - p.from.y) * k; ptr.shown = 1; } + const capNow = captions.find(([a, b]) => v >= a && v < b); + const capOp = capNow ? Math.min(1, (v - capNow[0]) / 0.25, (capNow[1] - v) / 0.25) : 0; + const pulse = ptr.pulse !== undefined && v - ptr.pulse < 0.45 ? (v - ptr.pulse) / 0.45 : -1; + const sy = await page.evaluate(([dv, d, y, cap, op, px, py, pshow, pulse]) => { + window.scrollTo(0, y); + const c = document.getElementById('__cap'); + if (c.dataset.t !== cap) { c.dataset.t = cap; c.innerHTML = cap; } + c.style.opacity = op; c.style.transform = `translateX(-50%) translateY(${(1 - op) * 10}px)`; + const p = document.getElementById('__ptr'); + p.style.opacity = pshow; p.style.transform = `translate(${px}px, ${py}px)`; + const ring = p.firstChild; ring.style.opacity = pulse >= 0 ? 1 - pulse : 0; ring.style.transform = `scale(${pulse >= 0 ? 0.6 + pulse * 1.2 : 1})`; + window.__step(dv, d); + return window.scrollY; + }, [f === 0 ? 0 : 1000 / FPS, t + shift, cam.y, capNow ? capNow[2] : '', capOp, ptr.x, ptr.y, ptr.shown, pulse]); + await settle(expectMsgs, expectStates); + const scale = W / zoomNow.w; + const shot = await cdp.send('Page.captureScreenshot', { format: 'jpeg', quality: 94, clip: { x: zoomNow.x, y: zoomNow.y + sy, width: zoomNow.w, height: zoomNow.w * 9 / 16, scale } }); + const buf = Buffer.from(shot.data, 'base64'); + if (f === Math.round((clip.posterAt ?? 0) * FPS)) writeFileSync(`${base}.poster.jpg`, buf); + if (!enc.stdin.write(buf)) await new Promise(r => enc.stdin.once('drain', r)); + if (f % 60 === 0) process.stdout.write(`\r ${clip.name} ${f}/${frames} frames, ${((Date.now() - started) / 1000).toFixed(0)}s`); + } + enc.stdin.end(); + await new Promise(r => enc.on('close', r)); + process.stdout.write('\n'); + await browser.close(); + await floor.close(); + rmSync(tmpRoot, { recursive: true, force: true }); + return finish(clip, base, master, seconds); +} + +const ff = a => spawnSync('ffmpeg', ['-y', '-loglevel', 'error', ...a], { encoding: 'utf8' }); +const kb = f => existsSync(f) ? `${Math.round(statSync(f).size / 1024)} KB` : 'missing'; +// The published files from the master: H.264 and VP9 at the clip's frame rate, the poster, and a light GIF. +function finish(clip, base, master, seconds) { + const loopFade = clip.loop ? Math.min(0.6, seconds / 8) : 0; + let src = master; + if (loopFade) { + // A seamless loop: the last moments cross-fade into the first, so the cut back to the start does not show. + const looped = `${base}.loop.mp4`; + const d = loopFade, len = seconds - d; + ff(['-i', master, '-filter_complex', `[0]split[a][b];[a]trim=start=${d},setpts=PTS-STARTPTS[body];[b]trim=end=${d},setpts=PTS-STARTPTS[head];[body][head]xfade=transition=fade:duration=${d}:offset=${(len - d).toFixed(3)}[v]`, '-map', '[v]', '-c:v', 'libx264', '-preset', 'fast', '-crf', '12', '-pix_fmt', 'yuv420p', '-r', String(FPS), '-threads', '8', looped]); + if (existsSync(looped)) src = looped; + } + const mp4 = `${base}.mp4`, webm = `${base}.webm`, png = `${base}.png`, gif = `${base}.gif`; + ff(['-i', src, '-c:v', 'libx264', '-preset', 'slow', '-crf', String(clip.crf ?? 23), '-tune', 'animation', '-pix_fmt', 'yuv420p', '-r', String(FPS), '-g', String(FPS * 2), '-movflags', '+faststart', '-an', '-threads', '8', mp4]); + ff(['-i', src, '-c:v', 'libvpx-vp9', '-crf', String(clip.vp9crf ?? 36), '-b:v', '0', '-row-mt', '1', '-deadline', 'good', '-cpu-used', '3', '-pix_fmt', 'yuv420p', '-r', String(FPS), '-an', '-threads', '8', webm]); + ff(['-i', `${base}.poster.jpg`, png]); + rmSync(`${base}.poster.jpg`, { force: true }); + if (clip.gif) { + // 25 fps is an exact 4/100 s frame delay, and ordered dithering keeps a GIF of moving bots light. + const [gw, gfps, colors] = [clip.gif.width || 640, clip.gif.fps || 25, clip.gif.colors || 96]; + const pal = `${base}.palette.png`; + ff(['-i', src, '-vf', `fps=${gfps},scale=${gw}:-1:flags=lanczos,palettegen=max_colors=${colors}:stats_mode=diff`, pal]); + ff(['-i', src, '-i', pal, '-lavfi', `fps=${gfps},scale=${gw}:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3:diff_mode=rectangle`, '-loop', '0', gif]); + rmSync(pal, { force: true }); + } + if (!args.keep) { rmSync(master, { force: true }); rmSync(`${base}.loop.mp4`, { force: true }); } + const row = { name: clip.name, seconds, fps: FPS, mp4: kb(mp4), webm: kb(webm), poster: kb(png), gif: clip.gif ? kb(gif) : null }; + console.log(JSON.stringify(row)); + return row; +} + +if (args.list) { for (const c of CLIPS) console.log(`${c.name.padEnd(20)} ${String(c.seconds).padStart(3)}s ${c.about}`); process.exit(0); } +const names = args.all ? CLIPS.map(c => c.name) : args._; +if (!names.length) { console.error(`usage: node clips/frames.mjs ... | --all [--out DIR] [--fps 60]\nclips: ${CLIPS.map(c => c.name).join(', ')}`); process.exit(2); } +if (spawnSync('ffmpeg', ['-version']).status !== 0) { console.error('ffmpeg is not on PATH.'); process.exit(1); } +for (const n of names) { + const clip = CLIPS.find(c => c.name === n); + if (!clip) { console.error(`No clip ${n}`); process.exit(2); } + await record(clip, { scheme: args.scheme, seconds: args.seconds, from: args.from }); +} diff --git a/clips/one-command.mjs b/clips/one-command.mjs index f0865b8..cd82391 100644 --- a/clips/one-command.mjs +++ b/clips/one-command.mjs @@ -51,8 +51,8 @@ const L = square ? { rows: 5, th: 168, gap: 16, termW: 900, cmdSize: 25, hook: 6 : vertical ? { rows: 9, th: 190, gap: 16, termW: 960, cmdSize: 30, hook: 118, mark: 128 } : { rows: 4, th: 234, gap: 20, termW: 1120, cmdSize: 34, hook: 104, mark: 104 }; -// The end card carries the logo from brand/logo. Its S starts cream and turns gold when the lamp used to come on. -const LOGO = readFileSync(join(root, 'brand', 'logo', 'logo.svg'), 'utf8').trim().replace(/^]*>/, '", "", s, flags=re.S) - s = re.sub(r']*>Desk Studio', f'', s) - s = s.replace(' a.deliverable.localeCompare(b.deliverable) || a.name.localeCompare(b.name)); +// The studio's first brand kit and the card made on it show the retired lamp mark. They stay in evals/golden as +// records, and the site's gallery leaves them out. +const RETIRED = new Set(['brand-kit/desk-studio', 'social-card/desk-studio-floor']); +for (let i = goldens.length - 1; i >= 0; i--) if (RETIRED.has(`${goldens[i].deliverable}/${goldens[i].name}`)) goldens.splice(i, 1); function pickRender(dir) { const r = join(dir, 'renders'); @@ -100,15 +104,26 @@ function frame(title, content) { return `
${content}
`; } -// Loops: site/media/.mp4 and .jpg, made by site/media.mjs from the recorded takes. +// Loops: site/media/.mp4, .webm and the .webp poster. The dashboard loops are recorded at 60 fps by +// clips/frames.mjs and published by site/media.mjs --frames. const media = join(here, 'media'); -const loop = (name, alt) => { - const mp4 = join(media, `${name}.mp4`), jpg = join(media, `${name}.webp`); +const loop = (name, alt, label) => { + const mp4 = join(media, `${name}.mp4`), webm = join(media, `${name}.webm`), jpg = join(media, `${name}.webp`); if (!existsSync(mp4)) return `

The ${esc(name)} loop is not recorded yet.

`; - cpSync(mp4, join(dist, 'media', `${name}.mp4`)); - if (existsSync(jpg)) cpSync(jpg, join(dist, 'media', `${name}.webp`)); - return frame(({ 'floor-idle': 'dashboard', 'swap-the-seat': 'change-role' })[name] || name, `${existsSync(jpg) ? `${esc(alt)}` : ''}`); + for (const f of [mp4, webm, jpg]) if (existsSync(f)) cpSync(f, join(dist, 'media', basename(f))); + return frame(label || name, `${existsSync(jpg) ? `${esc(alt)}` : ''}`); }; +// The captioned walkthrough: plays when asked, with its poster and a captions track. +const walkthrough = () => { + const files = ['walkthrough.mp4', 'walkthrough.webm', 'walkthrough.webp', 'walkthrough.vtt']; + if (!existsSync(join(media, 'walkthrough.mp4'))) return '

The walkthrough is not recorded yet.

'; + for (const f of files) if (existsSync(join(media, f))) cpSync(join(media, f), join(dist, 'media', f)); + return ``; +}; +// The studio bots, drawn by the dashboard's own bots.js, so the site's bots are the product's bots. +const bots = (() => { const g = {}; new Function('window', read('dashboard/public/bots.js'))(g); return g.DeskBots; })(); +const botsCss = read('dashboard/public/bots.css'); +const bot = (tier, state = 'awake', seed = 0) => `${bots.html(tier, state, seed)}`; const heroLoop = (name, alt) => { for (const file of [`${name}.mp4`, `${name}.webm`, `${name}-poster.png`, `${name}.vtt`]) cpSync(join(media, file), join(dist, 'media', file)); @@ -145,24 +160,26 @@ const css = ` :root { color-scheme:light; - --white:#ffffff; - --ink:#17140f; - --cream:#f6f3ec; - --muted:#6b655b; - --line:#d9d4c8; - --dark:#12110f; - --warm:#f4f0e8; - --panel:#1c1a17; - --dark-muted:#9a948a; - --dark-line:#3b3830; - --gold:#8f5a00; - --bright-gold:#ffaa1c; + /* the studio by day, and its dusk bands, from brand/palette.css */ + --white:#fffaf2; + --ink:#2b2017; + --cream:#f6ecdc; + --muted:#6f604f; + --line:#eadbc4; + --dark:#15100b; + --warm:#fcf3e4; + --panel:#1f1711; + --dark-muted:#c2b19b; + --dark-line:#3b2e23; + --gold:#9a5b00; + --bright-gold:#ffbf52; + --amber:#ffaa1c; --green:#009e73; --pink:#cc79a7; --blue:#0072b2; --sky:#56b4e9; --orange:#d55e00; } * { box-sizing:border-box; } html { scroll-padding-top:104px; } body { margin:0; - background:var(--white); + background:radial-gradient(1200px 600px at 15% -10%, rgba(255,170,28,.12), transparent 70%), var(--white); color:var(--ink); font-family:"Inter",Arial,sans-serif; font-size:20px; @@ -171,9 +188,9 @@ body { -webkit-font-smoothing:antialiased; } h1,h2,h3,p,ol,ul,figure { margin:0; } -h1,h2,h3 { font-weight:600; } -h1 { font-size:76.5px; line-height:1.05; letter-spacing:-.02em; } -h2 { font-size:39px; line-height:1.16; letter-spacing:-.02em; } +h1,h2,h3 { font-weight:700; } +h1 { font-size:76.5px; line-height:1.03; letter-spacing:-.035em; font-weight:800; } +h2 { font-size:42px; line-height:1.12; letter-spacing:-.03em; font-weight:800; } h3 { font-size:20px; line-height:1.4; } a { color:inherit; text-underline-offset:4px; } a:focus-visible, @@ -187,7 +204,7 @@ a[data-state="hover"] { text-decoration-thickness:2px; } .band { padding-block:96px; } .cream { background:var(--cream); } .dark { - background:var(--dark); + background:radial-gradient(1000px 520px at 12% 0%, rgba(255,170,28,.13), transparent 70%), radial-gradient(800px 480px at 95% 10%, rgba(204,121,167,.08), transparent 70%), var(--dark); color:var(--warm); --muted:var(--dark-muted); --line:var(--dark-line); @@ -210,25 +227,30 @@ a[data-state="hover"] { text-decoration-thickness:2px; } justify-content:center; align-items:center; min-height:48px; - padding:8px 24px; - border:1px solid var(--ink); - border-radius:4px; + padding:8px 26px; + border:0; + border-radius:999px; background:var(--ink); color:var(--white); font-size:16px; - font-weight:600; + font-weight:700; line-height:1.5; text-decoration:none; + box-shadow:0 3px 0 rgba(43,32,23,.28), inset 0 -2px 0 rgba(0,0,0,.18); + transition:transform 120ms cubic-bezier(.16,1,.3,1), box-shadow 120ms; } .button:hover, -.button[data-state="hover"] { background:var(--panel); text-decoration:underline; } +.button[data-state="hover"] { background:var(--panel); transform:translateY(-1px); box-shadow:0 4px 0 rgba(43,32,23,.28); } +.button:active { transform:translateY(1px); box-shadow:0 1px 0 rgba(43,32,23,.28); } +.dark .button { background:var(--warm); color:var(--dark); box-shadow:0 3px 0 rgba(0,0,0,.45); } .button:focus-visible, .button[data-state="focus"] { outline-color:var(--ink); } .topbar { position:sticky; top:0; z-index:10; - background:var(--white); + background:rgba(255,250,242,.88); + backdrop-filter:blur(10px); border-bottom:1px solid var(--line); } .nav { @@ -241,9 +263,11 @@ a[data-state="hover"] { text-decoration-thickness:2px; } .brand { display:inline-flex; align-items:center; - gap:16px; + gap:14px; flex-shrink:0; - font-weight:600; + font-weight:800; + letter-spacing:-.03em; + font-size:20px; text-decoration:none; } [data-ds-logo] { display:inline-flex; align-items:center; width:40px; height:40px; } @@ -272,7 +296,7 @@ a[data-state="hover"] { text-decoration-thickness:2px; } padding:24px; background:var(--cream); border:1px solid var(--line); - border-radius:4px; + border-radius:14px; color:var(--ink); text-align:left; font-family:"Geist Mono",monospace; @@ -287,9 +311,9 @@ a[data-state="hover"] { text-decoration-thickness:2px; } .media-frame { overflow:hidden; border:1px solid var(--line); - border-radius:8px; + border-radius:18px; background:var(--cream); - box-shadow:0 16px 32px #17140f14; + box-shadow:0 18px 40px rgba(84,56,26,.14); } .hero-media { margin-top:48px; } .frame-bar { @@ -351,7 +375,8 @@ a[data-state="hover"] { text-decoration-thickness:2px; } } .step-number { display:block; - color:var(--warm); + color:var(--step, var(--warm)); + font-weight:800; font-size:39px; font-weight:400; line-height:1; @@ -411,7 +436,7 @@ a[data-state="hover"] { text-decoration-thickness:2px; } .facts dt { font-weight:600; } .facts dd { font-size:13px; color:var(--muted); } .starters { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr)); gap:24px; margin-top:32px; } -.starter { display:flex; flex-direction:column; gap:12px; padding:24px; border:1px solid var(--line); text-decoration:none; min-width:0; } +.starter { display:flex; flex-direction:column; gap:12px; padding:24px; border:1px solid var(--line); border-radius:18px; background:var(--white); text-decoration:none; min-width:0; } .starter:hover { border-color:var(--ink); } .starter h3 { font-size:20px; text-decoration:underline; text-underline-offset:4px; } .starter .tag { font:12px "Geist Mono",monospace; color:var(--muted); } @@ -600,19 +625,19 @@ img, video { max-width:100%; } blockquote { border-left:2px solid var(--line); padding-left:24px; margin:24px 0; } .axes { display:grid; grid-template-columns:1fr auto; gap:4px 16px; font-variant-numeric:tabular-nums; } .axes .bar { grid-column:1 / -1; height:8px; background:var(--line); position:relative; } -.axes .bar i { position:absolute; inset:0 auto 0 0; background:currentColor; } -.axes .bar b { position:absolute; left:80%; top:-3px; bottom:-3px; width:2px; background:var(--muted); } +.axes .bar { border-radius:4px; } +.axes .bar i { position:absolute; inset:0 auto 0 0; background:var(--green); border-radius:4px; } +.axes .bar b { position:absolute; left:80%; top:-3px; bottom:-3px; width:2px; background:var(--amber); } table { border-collapse:collapse; width:100%; margin-top:24px; font-size:16px; line-height:24px; } th,td { border-top:1px solid var(--line); padding:12px 16px 12px 0; text-align:left; vertical-align:top; overflow-wrap:anywhere; } .table-scroll { overflow-x:auto; max-width:100%; } .table-scroll:focus-visible { outline:3px solid currentColor; outline-offset:4px; } pre { white-space:pre-wrap; overflow-wrap:anywhere; font-size:16px; } -.cmdblock { margin-top:8px; padding:16px; border:1px solid var(--line); background:var(--cream); } +.cmdblock { margin-top:8px; padding:16px; border:1px solid var(--line); border-radius:14px; background:var(--cream); } .install-details > p { margin-bottom:16px; } .install-details .lead { margin-top:32px; } -.costline { display:flex; height:12px; background:var(--line); margin-top:24px; } -.costline i { background:currentColor; } -.costline .senior { opacity:.6; } .costline .builder { opacity:.3; } +.costline { display:flex; height:14px; background:var(--line); margin-top:24px; border-radius:7px; overflow:hidden; } +.costline .director { background:var(--pink); } .costline .senior { background:var(--sky); } .costline .builder { background:var(--green); } .costscale { display:flex; justify-content:space-between; color:var(--muted); } .controls { padding-left:24px; } .dark .media-frame, .dark .frame-bar { background:var(--panel); } @@ -642,6 +667,53 @@ pre { white-space:pre-wrap; overflow-wrap:anywhere; font-size:16px; } .hero-video-controls button { display:none; } } +/* the studio bots, from dashboard/public/bots.css */ +${botsCss} +.crew { display:flex; justify-content:center; align-items:flex-end; gap:18px; margin:40px auto -6px; position:relative; z-index:1; } +.crew .bot-wrap { --bot-w:86px; } +.crew .tier-senior:last-child .bot-wrap { opacity:.75; } +/* on the light page the cream bots need an edge */ +.crew .bot, .tier-bot .bot { filter:drop-shadow(0 0 0.6px rgba(70,45,20,.55)) drop-shadow(0 2px 3px rgba(70,45,20,.14)); } +.watch-head { display:grid; grid-template-columns:1.1fr 1fr; gap:64px; align-items:end; } +.watch-head p { color:var(--muted); font-size:18px; line-height:28px; } +.walk-frame { margin-top:40px; } +.walk { display:block; width:100%; height:auto; aspect-ratio:16 / 9; background:var(--dark); } +.walk-links { display:flex; flex-wrap:wrap; gap:12px 28px; align-items:center; margin-top:20px; font-size:16px; } +.loops { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:32px 28px; margin-top:56px; } +.loops figcaption { margin-top:14px; font-size:16px; line-height:24px; color:var(--muted); } +.loops figcaption strong { color:inherit; display:block; font-size:18px; margin-bottom:2px; } +.dark .loops figcaption strong { color:var(--warm); } +.control-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:40px 28px; margin-top:48px; } +.control-list { padding:28px; border-radius:22px; background:var(--white); border:1px solid var(--line); font-size:16px; line-height:24px; } +.control-list ul { padding-left:20px; margin:12px 0; } .control-list li { margin:6px 0; } +.control-grid figcaption { margin-top:14px; font-size:16px; line-height:24px; color:var(--muted); } +.control-grid figcaption strong { display:block; color:var(--ink); font-size:18px; } +.start-steps { list-style:none; padding:0; margin:48px 0 0; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:24px; } +.start-steps li { padding:28px; border-radius:22px; background:var(--white); border:1px solid var(--line); display:flex; flex-direction:column; gap:14px; min-width:0; } +.start-steps .num { display:inline-grid; place-items:center; width:40px; height:40px; border-radius:50%; font-weight:800; font-size:18px; color:#1a120b; } +.start-steps li:nth-child(1) .num { background:var(--amber); } +.start-steps li:nth-child(2) .num { background:var(--sky); } +.start-steps li:nth-child(3) .num { background:var(--green); color:#fff; } +.start-steps h3 { font-size:22px; } +.start-steps p { font-size:16px; line-height:24px; color:var(--muted); } +.start-steps .cmdblock { margin:0; font-size:14px; } +.start-steps .cmdblock code { font-size:14px; } +.start-more { margin-top:32px; font-size:16px; } +.tier .tier-bot { float:right; margin:-8px 0 0 16px; } +.tier .tier-bot .bot-wrap { --bot-w:76px; } +.steps li:nth-child(1) { --step:var(--amber); } .steps li:nth-child(2) { --step:var(--pink); } .steps li:nth-child(3) { --step:var(--green); } .steps li:nth-child(4) { --step:var(--sky); } .steps li:nth-child(5) { --step:var(--bright-gold); } +@media (max-width:900px) { + .watch-head { grid-template-columns:1fr; gap:24px; } + .loops { grid-template-columns:1fr; } + .control-grid, .start-steps { grid-template-columns:1fr; } + .crew .bot-wrap { --bot-w:62px; } + .crew { gap:10px; } +} +@media (max-width:599px) { + .crew .bot-wrap { --bot-w:48px; } + .crew .tier-senior:last-child { display:none; } +} + `; const providerRows = Object.entries(models.providers).map(([key, p]) => `${esc(p.label)}${esc(p.cli)}${['director', 'senior', 'builder'].map(t => `${t} ${esc(p.tiers[t])}`).join(', ')}`).join(''); @@ -672,6 +744,7 @@ const html = ` +${bots.defs}
@@ -695,18 +769,47 @@ const html = `

DESIGN, AT YOUR DESK

Your own design studio.

A full design studio inside the coding agent you already use. One command, free, nothing leaves your machine but the model calls you already make.

- +

Read the code

/plugin install desk --marketplace adityareddy-dev/deskstudio
Free, MIT
no account, no sign-up
${registry.desks.length} desks
each one a brief you can read
${goldens.length} pieces
signed off, with score and cost
-
+ +
${heroLoop('explainer', 'Desk Studio: roughs, pick, polish, proof and sign-off')}
+
+
+

THE DASHBOARD

Watch the studio work.

+

Every desk is a small bot at its own desk. It wakes up while its agent works and goes back to sleep when it is done. The job moves along a track from the brief to the sign-off, and every rough, pick and score lands on the board. It runs on your machine, on 127.0.0.1.

+
+
${walkthrough()}
+ +
+
${loop('floor-idle', 'The floor of a real brand-kit job: desks wake as their agents start and sleep when they stop', 'the floor')}
Desks wake up when they work.A real brand-kit job at its busiest, ten agents at once. The badge counts agents working at the same desk.
+
${loop('the-pick', 'The Art Director wakes, then the ring draws round the picked rough on the review board', 'the pick')}
The Art Director picks.Three roughs on the board, then the ring goes round the one that reads first, with the reason under it.
+
${loop('before-after', 'Three polish rounds scored against the bar, 8.4, 8.5, then 8.9 at the bar', 'the rounds')}
Every round is scored.8.4 and 8.5 stay under the bar, round 3 signs off at 8.9 with every axis at 8 or above.
+
${loop('cost-meter', 'Round, score, cost and time counting up as the desks report', 'the numbers')}
Cost as it happens.Round, score, cost and time update as each desk reports, at list price.
+
+
+ +
+
+

YOU RUN IT

Steer it while it runs.

+

Every control is a write to a file the Design Manager reads before its next step: pause, continue or stop, move a tier or one desk to another model, write a desk a note, pick a rough yourself, approve a sign-off under the bar or send it back.

+
+
+
${loop('pick-it-yourself', 'Clicking Pick this on a rough, which then shows as picked by you with the ring', 'pick it yourself')}
Pick it yourself.Your pick overrides the Art Director and is recorded as your call.
+
${loop('swap-the-seat', 'The builder role moved to Sonnet for this job, then the next polish round runs on it', 'change the role')}
Change a role.The builder role moved from Haiku to Sonnet for this job. It applies at the next step.
+
${loop('note-to-the-desk', 'A note typed to the UI Designer, then the UI Designer starts the next round', 'a note to the desk')}
Leave a desk a note.It goes into that desk's next prompt. The log keeps only its length.
+

Every control

  • Pause at the next step, continue, stop.
  • Move a tier or one desk to another model, for this job or as the default.
  • Write a desk a note for its next prompt.
  • Pick a rough yourself, over the Art Director.
  • Approve a sign-off under the bar, or send it back with notes.

Nothing interrupts a desk in the middle of a step. What each control writes.

+
+
+

What a run looks like

${realRun()}
@@ -772,8 +875,7 @@ const html = `

Proof

No model in the loop. A failure goes back to the builder with the screenshot. It runs on every polished file.

Sign-off

The Creative Director scores eight axes.

Average ${cardFinal.average.toFixed(1)}, ${card.round} of ${card.maxRounds} rounds.

${img(cardRender(`round-${card.round}`), 'card-final', 'the signed-off launch card')}
${axesHtml(cardFinal.axes)}
-
-
${loop('the-pick', 'Three roughs on the board, the Art Director picks one and the ring draws round it')}
${loop('before-after', 'A rough becomes the signed-off render as the scorecard fills')}
+
@@ -784,14 +886,11 @@ const html = `

${registry.desks.length} desks.

Each desk is a written brief: what it owns, how it decides, its checklist and what it hands to the next desk. The Design Manager staffs them per job.

-
${TIERS.map(([t, label, does]) => `

${label}

${esc(cap(does))}.

${desksByTier(t).slice(0, 2).map(d => `${esc(d.name)}`).join('')}
`).join('')}
+
${TIERS.map(([t, label, does], i) => `

${label}

${esc(cap(does))}.

${desksByTier(t).slice(0, 2).map(d => `${esc(d.name)}`).join('')}
`).join('')}

Missing a desk? A desk is one written brief and one registry entry. Write it, or ask for it first and say what it should own.

All desks and the dashboard
-
-

The three roughs run side by side and the rest in turn.

-
${loop('floor-idle', 'The dashboard: forty-two desks, a lamp lighting when a desk starts')}
-
+

The three roughs run side by side and the rest in turn. Watch the floor.

${TIERS.map(([t, label, does]) => `

${label} ${desksByTier(t).length}

${esc(does)}. Default model ${esc(registry.tiers[t].default)}.

    ${desksByTier(t).map(d => `
  • ${esc(d.name)}${esc(d.owns)}
  • `).join('')}
`).join('')}

The Design Manager runs in your session on whatever model you run, and the other ${registry.desks.length - 1} desks run as subagents.

@@ -805,10 +904,9 @@ const html = `
  • Pick a rough yourself, over the Art Director.
  • Approve a sign-off under the bar, or send it back with notes.
  • -
    ${loop('pick-it-yourself', 'Clicking a rough on the review board, which then shows as picked by you')}
    +

    The loops are above, and every control is in the dashboard guide.

    -

    One real job, run from the dashboard: paused after the roughs, the pick made by hand, the builder role moved from Haiku to Sonnet, and a note written to the UI Designer. It came back signed off at ${controls.average} in ${controls.rounds} rounds, ${money(controls.cost)} at list price.

    -
    ${loop('swap-the-seat', 'The builder role moved to another model while the job holds')}
    ${loop('note-to-the-desk', 'A note typed to the UI Designer, then the next round')}
    +

    One real job, run from the dashboard: paused after the roughs, the pick made by hand, the builder role moved from Haiku to Sonnet, and a note written to the UI Designer. It came back signed off at ${controls.average} in ${controls.rounds} rounds, ${money(controls.cost)} at list price.

    @@ -847,8 +945,7 @@ const html = ` ${quill ? `

    From the ${esc(quill.brand)} card job: proof failed twice before passing. Round 1 failed on the type scale, a 248px headline off the brand scale, capped at 232px. Round 3 failed because the X card's headline sat inside the left safe zone, and round 4 fixed it. Signed off at ${quill.average}.

    ` : ''} -
    -
    ${loop('cost-meter', 'The cost line filling by tier as each desk reports')}
    +

    Cheap models make. Strong models judge.

    Roughs come from the senior roles, every polish round from the builder roles, and the pick, the notes and the sign-off from the director roles. The dashboard shows what each desk cost at list price.

    @@ -920,11 +1017,20 @@ const html = `
    -
    -
    +
    +
    +

    GET STARTED

    +

    From install to the dashboard.

    +
      +
    1. 1

      Install

      In Claude Code:

      ${cmd(INSTALL)}

      In Codex, Cursor, Gemini CLI, Copilot, Windsurf or OpenCode, in the project:

      ${cmd('npx deskstudio add')}

    2. +
    3. 2

      Run a first job

      Ask for something. In the other agents, plain words work too.

      ${cmd('/desk:design a launch card for LinkedIn')}

      The files land in .desk/out/<job>/, with a note on who did what, the scores and the cost.

    4. +
    5. 3

      Open the dashboard

      Once per project, then open the link it prints. Keep it open while the job runs.

      ${cmd('/desk:dashboard setup')}
      ${cmd('/desk:dashboard start')}

      Outside Claude Code: npx --package deskstudio desk-dashboard setup, then start. How to read it is in the dashboard guide.

    6. +
    +

    All of it, with older Claude Code versions and the runner, is in the install guide.

    +
    +
    -

    GET STARTED

    -

    Install.

    +

    Every agent, step by step.

    Claude Code, one line.

    @@ -985,7 +1091,7 @@ const html = `