web-design
Guidance for producing web UI that looks deliberately designed, not auto-generated. The single
failure mode this skill exists to prevent: shipping something a viewer can immediately tell an AI
made. If it reads as the AI default — purple gradients, beige hero, three equal cards, em-dashes,
Inter on slate-900 — it failed, regardless of how clean the code is.
Three ideas run through everything here:
- Taste is trained, not innate. Good design is a stack of small correct decisions, each
defensible. There is no single magic move.
- Unseen details compound. The 75ms exit animation, the 1px focus ring offset, the tinted
neutral instead of pure gray — individually invisible, collectively the difference between
"fine" and "considered."
- Restraint beats expression by default. One accent, one type scale, one corner-radius scale,
motion only where it earns its place. Reach for more only when the brief asks for it.
When to use this
Use it whenever you design or review web UI: a landing page, a dashboard, a component, a form, a
marketing section. Use it before you call a screen "done." Don't treat it as always-on — it's a
lens for visual/frontend work, not a tax on every code change.
Workflow
- Read the brief (below) — infer intent, write a one-line Design Read, set the dials.
- Apply the fundamentals — type, color, spacing, the interactive states. Pull depth from
references/ as needed.
- Avoid the anti-patterns —
references/anti-patterns.md is the load-bearing don't-list.
- Look at it in a browser — screenshot and eyeball. Do not ship on faith (see Iterate by eye).
- Run the pre-ship checklist (below) before declaring done.
The brief read
Before writing any CSS, infer from what you were asked:
- Page kind — product UI / dashboard / landing page / form / docs / editorial. This dominates
every other choice (a dashboard and an Awwwards landing page want opposite densities).
- Audience & register — internal tool / consumer / enterprise / public-sector.
- Vibe words & references — anything the brief implies about feel ("calm", "punchy", "premium").
- Hard constraints — accessibility requirements, regulated/public-sector rules, brand assets.
These override aesthetic preference, always.
Then write a one-line Design Read ("Internal analytics dashboard, dense, calm, a11y-first —
restrained/standard/dense") so the intent is explicit and checkable. Ask at most one clarifying
question, and only when the brief is genuinely ambiguous — otherwise pick sensible defaults and
proceed.
The intent dials
Three orthogonal knobs, each at three named levels. Set them from the brief; they bias every
later choice. They are set conversationally — say what they are, don't write them into a file.
| Dial |
Levels (low → high) |
What it controls |
| EXPRESSIVENESS |
restrained · balanced · expressive |
asymmetry, decoration, type personality, layout variety |
| MOTION |
minimal · standard · rich |
how much animates and how present it is |
| DENSITY |
airy · standard · dense |
information per screen, whitespace, sizing |
Default = restrained / standard / standard. This is restraint-biased on purpose: most real
work is product UI and dashboards, where high expressiveness actively harms usability. Move a dial up
only when the brief earns it — an expressive marketing hero is legitimate; an expressive settings
page is not. Whatever the dials, a mobile single-column override and the accessibility constraints
still apply.
The fundamentals (the spine)
The corroborated core. Compact here; full treatment in references/.
- Type — one modular scale, one ratio ≥1.25 (1.25 / 1.333 / 1.5); ~5 sizes cover most needs;
body ≥16px; measure 45–75ch (cap 65–75ch); heading line-height 1.1–1.2, body 1.5–1.7; ≤3 font
families. →
references/typography.md
- Color — work in OKLCH, not HSL; 60-30-10 weight split; tinted neutrals, never pure gray;
dark mode is a surface-lightness scale, not an inversion; commit to one accent. →
references/color.md
- Layout & space — a 4pt spacing scale (4/8/12/16/24/32/48/64/96); tight grouping 8–12px,
section separation 48–96px; deliberate hierarchy (combine size + weight + space); a semantic
z-index scale (never
9999); ≥44×44px touch targets; pass the squint test. → references/layout-spacing.md
- Interaction — design all eight states (default / hover / focus / active / disabled /
loading / error / success); a visible
:focus-visible ring always; placeholders aren't labels;
undo beats confirm; skeletons beat spinners. → references/interaction.md
- Motion — short and motivated. UI animations <300ms, exits ~75% of the enter; custom
decelerating easing, never bounce/elastic, never
ease-in for UI; animate only transform/opacity;
respect prefers-reduced-motion. Decide whether to animate before how — and motion you commit to
must actually ship: a page that reads as animated but sits static is broken, so if you can't land it
cleanly, dial motion down and ship clean static. → references/motion.md
- Copy — buttons are verb+object (never OK/Submit/Yes); errors say what happened, why, and how to
fix; no em-dashes; no marketing buzzwords. →
references/ux-writing.md
- Accessibility — WCAG AA minimum (4.5:1 body text, 3:1 large text and UI); this is a floor, not
a goal.
Avoid the AI default
The highest-value content in this skill is the don't-list in references/anti-patterns.md — a
cross-corroborated catalogue of the tells that mark generated UI (AI-purple gradients, reflexive
beige/cream, gradient text, eyebrow chips, numbered section markers, three equal feature cards,
em-dashes, overused fonts). Read it before building and check against it before shipping. The framing:
if you can tell an AI made it, it failed.
If a real design system fits, use it
If the project already uses — or the brief clearly calls for — an established design system, adopt its
official package rather than hand-rolling CSS that approximates it. One system per project; don't
recreate its tokens by hand, and don't import a system and then override most of it. Match the brief to
the ecosystem it lives in:
| Brief reads as… |
Reach for its official system |
| Microsoft / enterprise SaaS / dashboards |
Fluent UI |
| Google-flavored / Material product |
Material 3 (Material Web) |
| IBM-style B2B / data-dense analytics |
Carbon |
| Shopify app surface |
Polaris |
| Atlassian / Jira-style product |
Atlaskit |
| GitHub-style devtool or marketing |
Primer (Brand variant for marketing) |
| UK / US public-sector service |
GOV.UK Frontend / USWDS — often legally expected |
(This skill is stack-agnostic and prescribes no framework or package beyond naming these systems.) If the
brief is an aesthetic (glassmorphism, bento, brutalist, editorial) rather than a system, there's no
official package — build it with native CSS and be honest about what's borrowed inspiration vs. official
material. If nothing fits, build restrained native CSS on the fundamentals above.
Output modes
- Build / enhance — apply the rules directly to the work.
- Review / fix — emit a focused
| Before | After | Why | table, one row per change. This is the
default review format: concrete, scannable, each change justified.
- Full critique (on request) — a deeper pass using the compressed review lens in
references/anti-patterns.md (heuristics + cognitive-load + persona lenses).
Redesigning an existing UI
A redesign is not a greenfield build — misclassifying the mode is the biggest source of bad redesign
output. First decide: preserve (modernise without breaking the brand) or overhaul (a new visual
language over the same content and IA). If genuinely ambiguous, ask once.
Audit before you touch anything. Record the current brand tokens (accent, type stack, radii), the
information architecture (page tree, nav labels, conversion paths), and the SEO/analytics baseline —
SEO and analytics migration is the #1 redesign risk. Extract the existing brand accent before
applying the color rules: a brand that is already purple stays purple (the anti-AI-purple rule governs
your defaults, not an established identity).
Apply the levers in priority order and stop when the brief is satisfied — each is more risk than the
last: (1) typography, (2) spacing & rhythm, (3) color recalibration (unify neutrals, keep the brand
accent), (4) a motion layer, (5) hero / key-section recomposition, (6) full block replacement (only for a
block that's unsalvageable). Targeted evolution (1–4) is most of the value at a fraction of the risk.
Never change silently — get explicit sign-off before touching URL slugs / anchor IDs, primary nav
labels, form field names or order (breaks analytics + autofill), the brand logo/wordmark, or legal /
consent copy.
Iterate by eye
Looking at the rules is not the same as looking at the result. If this session has browser
testing available (a browser-testing-capable profile — a QA or web-design rig with a Playwright/browser
MCP — spawns with one), use it:
- Render the page or component in the browser — at the dev server's actual bound URL. Assert the
bound port from the tracked server process itself, by whatever means your OS exposes (the framework's
own startup banner, when captured — e.g. vite's
Local: http://…:PORT — is one way but not the only
one); never assume a default port. If that port is already held by another process, the dev server
binds another port or fails, so eyeballing the default would render the wrong, stale server.
For a static on-disk HTML file with no dev server (a CV, a rendered report), don't navigate
file:// — Playwright's browser_navigate blocks it outright — and don't hand-roll a web server
per render cycle. Serve its directory over loopback with the bundled helper and open the printed
URL instead: node .claude/skills/web-design/scripts/serve-static.mjs <dir>.
- Screenshot it and actually look — squint test, hierarchy, spacing rhythm, contrast, the eight
states, dark mode, a narrow (mobile) viewport. For a throwaway verification shot, take it with
no filename/path — the screenshot tool auto-names it into your session's out-of-tree
scratch/output dir, so the working tree stays clean. Only pass a path when you deliberately persist
a shot (to attach or diff), and then make it an absolute path under the per-session scratch
directory the Playwright client itself allows writes to — this is not necessarily the same as your
generic session scratch/temp dir, and a path outside Playwright's own allowed roots is rejected ("…
is outside allowed roots"); a bare or relative name also resolves against the browser tool's client
workspace (the repo root), litters the tree, and risks an accidental commit (
git status will flag
it). The auto-name-into-scratch and $LOOM_SCRATCH_DIR mechanics here are @playwright/mcp-specific:
only a session that mounts Playwright gets that allowed root exposed as the $LOOM_SCRATCH_DIR
environment variable — persist a shot there rather than your generic session scratch/temp dir. A
session on a DIFFERENT browser tool (e.g. claude-in-chrome) has no $LOOM_SCRATCH_DIR and no
auto-naming — use that tool's own capture flow instead. To persist a shot as a file under
Playwright, don't rely on claude-in-chrome save_to_disk — it renders the inline base64 but writes no
reachable file (Claude Code issue #40141). Use Playwright page.screenshot({ path }) against the
loopback page (launch with { channel: 'chrome' } to reuse system Chrome and skip a download), or
decode the base64 from the transcript for a shot you already captured.
- Compare what you see against the fundamentals and the don't-list.
- Fix what's visibly wrong and repeat. One or two iteration loops catch what reading the code never
will.
Do not declare a visual task done without having looked at the rendered result. If no browser is
available in this session, say so explicitly rather than implying you eyeballed it.
Pre-ship checklist
A short gate — not an exhaustive audit. Run it before calling any screen done:
- Squint test — the most important element is obvious within ~2 seconds.
- Contrast — body ≥4.5:1, large/UI ≥3:1; placeholders counted.
- Type — one scale, body ≥16px, measure ≤~75ch, ≤3 families.
- Color — one accent; OKLCH; tinted neutrals; no AI-purple, no reflexive beige, no gradient text.
- Spacing — consistent 4pt rhythm; grouping vs separation is varied, not uniform.
- States — focus ring visible; hover/active/disabled/loading/empty/error all real.
- Motion — motivated, <300ms, custom easing,
prefers-reduced-motion honored.
- Copy — verb+object buttons, useful errors, no em-dashes, no buzzwords.
- Layout tells — no nested cards, no eyebrow chips, no numbered section markers, no three-equal-cards.
- Responsive — single-column mobile works; nothing overflows or touches the viewport edge.
- A11y — headings not skipped, images have alt text, touch targets ≥44px.
- Image framing — for any embedded photo/portrait, the subject is framed correctly: faces/heads
not cropped, not cut off by
object-fit: cover or a fixed container aspect ratio. Verify against the
ACTUAL rendered crop in the browser, not the source image — a center-crop that looks fine full-bleed
can decapitate a portrait once it's constrained to the container. And don't reach for an optional,
IRREVERSIBLE transform (a square/center crop, a destructive resize) unless the layout truly needs it:
correctness of the shown subject comes before optimization, and a crop you can't undo isn't worth a
marginal layout gain.
- Eyeballed — you looked at the rendered result, not just the code.
- E2E — if the project has an end-to-end test suite, a new or changed screen ships with (or
updates) a test in it, run green — see the project's own docs for the harness.
Provenance
This skill distills three public design skills into Loom's own guidance. See the NOTICE file in this
directory for full attribution: Impeccable (Apache-2.0, Paul Bakaus — itself building on
Anthropic's frontend-design skill and ehmo's typecraft additions), taste-skill (MIT, Leonxlnx —
including its v2 "design-taste-frontend" rewrite), and Emil Kowalski's design-engineering skill (MIT).
1---2name: web-design3description: Apply when designing, building, or reviewing web UI / frontend — visual polish, typography, color, layout, spacing, motion, accessibility, and UX copy. Use to avoid the generic "AI default" look and to make deliberate, restrained design choices. Shipped and kept current by Loom.4---56# web-design78Guidance for producing web UI that looks **deliberately designed**, not auto-generated. The single9failure mode this skill exists to prevent: shipping something a viewer can immediately tell an AI10made. If it reads as the AI default — purple gradients, beige hero, three equal cards, em-dashes,11Inter on slate-900 — it failed, regardless of how clean the code is.1213Three ideas run through everything here:1415- **Taste is trained, not innate.** Good design is a stack of small correct decisions, each16 defensible. There is no single magic move.17- **Unseen details compound.** The 75ms exit animation, the 1px focus ring offset, the tinted18 neutral instead of pure gray — individually invisible, collectively the difference between19 "fine" and "considered."20- **Restraint beats expression by default.** One accent, one type scale, one corner-radius scale,21 motion only where it earns its place. Reach for more only when the brief asks for it.2223## When to use this2425Use it whenever you design or review web UI: a landing page, a dashboard, a component, a form, a26marketing section. Use it before you call a screen "done." **Don't** treat it as always-on — it's a27lens for visual/frontend work, not a tax on every code change.2829## Workflow30311. **Read the brief** (below) — infer intent, write a one-line Design Read, set the dials.322. **Apply the fundamentals** — type, color, spacing, the interactive states. Pull depth from33 `references/` as needed.343. **Avoid the anti-patterns** — `references/anti-patterns.md` is the load-bearing don't-list.354. **Look at it in a browser** — screenshot and eyeball. Do not ship on faith (see *Iterate by eye*).365. **Run the pre-ship checklist** (below) before declaring done.3738## The brief read3940Before writing any CSS, infer from what you were asked:4142- **Page kind** — product UI / dashboard / landing page / form / docs / editorial. This dominates43 every other choice (a dashboard and an Awwwards landing page want opposite densities).44- **Audience & register** — internal tool / consumer / enterprise / public-sector.45- **Vibe words & references** — anything the brief implies about feel ("calm", "punchy", "premium").46- **Hard constraints** — accessibility requirements, regulated/public-sector rules, brand assets.47 **These override aesthetic preference, always.**4849Then write a one-line **Design Read** ("Internal analytics dashboard, dense, calm, a11y-first —50restrained/standard/dense") so the intent is explicit and checkable. **Ask at most one clarifying51question, and only when the brief is genuinely ambiguous** — otherwise pick sensible defaults and52proceed.5354## The intent dials5556Three orthogonal knobs, each at **three named levels**. Set them from the brief; they bias every57later choice. They are set conversationally — say what they are, don't write them into a file.5859| Dial | Levels (low → high) | What it controls |60|---|---|---|61| **EXPRESSIVENESS** | restrained · balanced · expressive | asymmetry, decoration, type personality, layout variety |62| **MOTION** | minimal · standard · rich | how much animates and how present it is |63| **DENSITY** | airy · standard · dense | information per screen, whitespace, sizing |6465**Default = `restrained / standard / standard`.** This is restraint-biased on purpose: most real66work is product UI and dashboards, where high expressiveness actively harms usability. Move a dial up67only when the brief earns it — an expressive marketing hero is legitimate; an expressive settings68page is not. Whatever the dials, a **mobile single-column override** and the accessibility constraints69still apply.7071## The fundamentals (the spine)7273The corroborated core. Compact here; full treatment in `references/`.7475- **Type** — one modular scale, one ratio ≥1.25 (1.25 / 1.333 / 1.5); ~5 sizes cover most needs;76 body ≥16px; measure 45–75ch (cap 65–75ch); heading line-height 1.1–1.2, body 1.5–1.7; ≤3 font77 families. → `references/typography.md`78- **Color** — work in OKLCH, not HSL; 60-30-10 weight split; tinted neutrals, never pure gray;79 dark mode is a surface-lightness scale, not an inversion; commit to one accent. → `references/color.md`80- **Layout & space** — a 4pt spacing scale (4/8/12/16/24/32/48/64/96); tight grouping 8–12px,81 section separation 48–96px; deliberate hierarchy (combine size + weight + space); a semantic82 z-index scale (never `9999`); ≥44×44px touch targets; pass the squint test. → `references/layout-spacing.md`83- **Interaction** — design all **eight states** (default / hover / focus / active / disabled /84 loading / error / success); a visible `:focus-visible` ring always; placeholders aren't labels;85 undo beats confirm; skeletons beat spinners. → `references/interaction.md`86- **Motion** — short and motivated. UI animations <300ms, exits ~75% of the enter; custom87 decelerating easing, never bounce/elastic, never `ease-in` for UI; animate only `transform`/`opacity`;88 respect `prefers-reduced-motion`. Decide *whether* to animate before *how* — and motion you commit to89 must actually ship: a page that reads as animated but sits static is broken, so if you can't land it90 cleanly, dial motion down and ship clean static. → `references/motion.md`91- **Copy** — buttons are verb+object (never OK/Submit/Yes); errors say what happened, why, and how to92 fix; no em-dashes; no marketing buzzwords. → `references/ux-writing.md`93- **Accessibility** — WCAG AA minimum (4.5:1 body text, 3:1 large text and UI); this is a floor, not94 a goal.9596## Avoid the AI default9798The highest-value content in this skill is the **don't-list** in `references/anti-patterns.md` — a99cross-corroborated catalogue of the tells that mark generated UI (AI-purple gradients, reflexive100beige/cream, gradient text, eyebrow chips, numbered section markers, three equal feature cards,101em-dashes, overused fonts). Read it before building and check against it before shipping. The framing:102**if you can tell an AI made it, it failed.**103104## If a real design system fits, use it105106If the project already uses — or the brief clearly calls for — an established design system, adopt its107**official** package rather than hand-rolling CSS that approximates it. **One system per project;** don't108recreate its tokens by hand, and don't import a system and then override most of it. Match the brief to109the ecosystem it lives in:110111| Brief reads as… | Reach for its official system |112|---|---|113| Microsoft / enterprise SaaS / dashboards | Fluent UI |114| Google-flavored / Material product | Material 3 (Material Web) |115| IBM-style B2B / data-dense analytics | Carbon |116| Shopify app surface | Polaris |117| Atlassian / Jira-style product | Atlaskit |118| GitHub-style devtool or marketing | Primer (Brand variant for marketing) |119| UK / US public-sector service | GOV.UK Frontend / USWDS — often legally expected |120121(This skill is stack-agnostic and prescribes no framework or package beyond naming these systems.) If the122brief is an *aesthetic* (glassmorphism, bento, brutalist, editorial) rather than a system, there's no123official package — build it with native CSS and be honest about what's borrowed inspiration vs. official124material. If nothing fits, build restrained native CSS on the fundamentals above.125126## Output modes127128- **Build / enhance** — apply the rules directly to the work.129- **Review / fix** — emit a focused `| Before | After | Why |` table, one row per change. This is the130 default review format: concrete, scannable, each change justified.131- **Full critique** (on request) — a deeper pass using the compressed review lens in132 `references/anti-patterns.md` (heuristics + cognitive-load + persona lenses).133134## Redesigning an existing UI135136A redesign is not a greenfield build — misclassifying the mode is the biggest source of bad redesign137output. First decide: **preserve** (modernise without breaking the brand) or **overhaul** (a new visual138language over the same content and IA). If genuinely ambiguous, ask once.139140**Audit before you touch anything.** Record the current brand tokens (accent, type stack, radii), the141information architecture (page tree, nav labels, conversion paths), and the SEO/analytics baseline —142**SEO and analytics migration is the #1 redesign risk.** Extract the existing brand accent *before*143applying the color rules: a brand that is already purple stays purple (the anti-AI-purple rule governs144your defaults, not an established identity).145146**Apply the levers in priority order and stop when the brief is satisfied** — each is more risk than the147last: (1) typography, (2) spacing & rhythm, (3) color recalibration (unify neutrals, keep the brand148accent), (4) a motion layer, (5) hero / key-section recomposition, (6) full block replacement (only for a149block that's unsalvageable). Targeted evolution (1–4) is most of the value at a fraction of the risk.150151**Never change silently** — get explicit sign-off before touching URL slugs / anchor IDs, primary nav152labels, form field names or order (breaks analytics + autofill), the brand logo/wordmark, or legal /153consent copy.154155## Iterate by eye156157**Looking at the rules is not the same as looking at the result.** If this session has browser158testing available (a browser-testing-capable profile — a QA or web-design rig with a Playwright/browser159MCP — spawns with one), use it:1601611. Render the page or component in the browser — at the dev server's **actual bound URL**. Assert the162 bound port from the tracked server process itself, by whatever means your OS exposes (the framework's163 own startup banner, when captured — e.g. vite's `Local: http://…:PORT` — is one way but not the only164 one); never assume a default port. If that port is already held by another process, the dev server165 binds another port or fails, so eyeballing the default would render the wrong, *stale* server.166 For a **static on-disk HTML file** with no dev server (a CV, a rendered report), don't navigate167 `file://` — Playwright's `browser_navigate` blocks it outright — and don't hand-roll a web server168 per render cycle. Serve its directory over loopback with the bundled helper and open the printed169 URL instead: `node .claude/skills/web-design/scripts/serve-static.mjs <dir>`.1702. **Screenshot it and actually look** — squint test, hierarchy, spacing rhythm, contrast, the eight171 states, dark mode, a narrow (mobile) viewport. For a **throwaway** verification shot, take it with172 **no filename/path** — the screenshot tool auto-names it into your session's out-of-tree173 scratch/output dir, so the working tree stays clean. Only pass a path when you deliberately **persist**174 a shot (to attach or diff), and then make it an **absolute path under the per-session scratch175 directory the Playwright client itself allows writes to** — this is not necessarily the same as your176 generic session scratch/temp dir, and a path outside Playwright's own allowed roots is rejected ("…177 is outside allowed roots"); a bare or relative name also resolves against the browser tool's client178 workspace (the repo root), litters the tree, and risks an accidental commit (`git status` will flag179 it). **The auto-name-into-scratch and `$LOOM_SCRATCH_DIR` mechanics here are `@playwright/mcp`-specific:180 only a session that mounts Playwright gets that allowed root exposed as the `$LOOM_SCRATCH_DIR`181 environment variable** — persist a shot there rather than your generic session scratch/temp dir. A182 session on a DIFFERENT browser tool (e.g. claude-in-chrome) has no `$LOOM_SCRATCH_DIR` and no183 auto-naming — use that tool's own capture flow instead. To persist a shot **as a file** under184 Playwright, don't rely on claude-in-chrome `save_to_disk` — it renders the inline base64 but writes no185 reachable file (Claude Code issue #40141). Use Playwright `page.screenshot({ path })` against the186 loopback page (launch with `{ channel: 'chrome' }` to reuse system Chrome and skip a download), or187 decode the base64 from the transcript for a shot you already captured.1883. Compare what you see against the fundamentals and the don't-list.1894. Fix what's visibly wrong and repeat. One or two iteration loops catch what reading the code never190 will.191192Do not declare a visual task done without having looked at the rendered result. If no browser is193available in this session, say so explicitly rather than implying you eyeballed it.194195## Pre-ship checklist196197A short gate — not an exhaustive audit. Run it before calling any screen done:1981991. **Squint test** — the most important element is obvious within ~2 seconds.2002. **Contrast** — body ≥4.5:1, large/UI ≥3:1; placeholders counted.2013. **Type** — one scale, body ≥16px, measure ≤~75ch, ≤3 families.2024. **Color** — one accent; OKLCH; tinted neutrals; no AI-purple, no reflexive beige, no gradient text.2035. **Spacing** — consistent 4pt rhythm; grouping vs separation is varied, not uniform.2046. **States** — focus ring visible; hover/active/disabled/loading/empty/error all real.2057. **Motion** — motivated, <300ms, custom easing, `prefers-reduced-motion` honored.2068. **Copy** — verb+object buttons, useful errors, no em-dashes, no buzzwords.2079. **Layout tells** — no nested cards, no eyebrow chips, no numbered section markers, no three-equal-cards.20810. **Responsive** — single-column mobile works; nothing overflows or touches the viewport edge.20911. **A11y** — headings not skipped, images have alt text, touch targets ≥44px.21012. **Image framing** — for any embedded photo/portrait, the subject is framed correctly: faces/heads211 not cropped, not cut off by `object-fit: cover` or a fixed container aspect ratio. Verify against the212 ACTUAL rendered crop in the browser, not the source image — a center-crop that looks fine full-bleed213 can decapitate a portrait once it's constrained to the container. And don't reach for an optional,214 IRREVERSIBLE transform (a square/center crop, a destructive resize) unless the layout truly needs it:215 correctness of the shown subject comes before optimization, and a crop you can't undo isn't worth a216 marginal layout gain.21713. **Eyeballed** — you looked at the rendered result, not just the code.21814. **E2E** — if the project has an end-to-end test suite, a new or changed screen ships with (or219 updates) a test in it, run green — see the project's own docs for the harness.220221## Provenance222223This skill distills three public design skills into Loom's own guidance. See the `NOTICE` file in this224directory for full attribution: **Impeccable** (Apache-2.0, Paul Bakaus — itself building on225Anthropic's frontend-design skill and ehmo's typecraft additions), **taste-skill** (MIT, Leonxlnx —226including its v2 "design-taste-frontend" rewrite), and **Emil Kowalski's** design-engineering skill (MIT).