{
  "schemaVersion": 1,
  "ruleset": "anti-default",
  "title": "Anti-sameness rules — the visual twin of the Copy Gate",
  "spec": "shared/design-studio-plan.md §3.11b",
  "authoredBy": "command-center lane",
  "authoredAt": "2026-09-07",
  "status": "SPEC DATA — not yet implemented. Implementation lands in the ghl-command-mcp lane at Design Studio phase 0 as src/command-os/design/rules/anti-default.json.",
  "howToRead": {
    "check": {
      "measured": "The verifier reads a number or boolean straight off the rendered DOM or computed style. Reported with its value.",
      "proxy": "A heuristic that approximates the real rule. Reported with its value AND labelled a proxy in verification.json.",
      "judged": "Only a model can decide. Listed in the report, never hidden, never scored as a pass.",
      "undecidable": "A fourth state the verifier must be able to return for ANY check: the subject was not readable in this run, so the rule was neither passed nor failed. Styles in an external sheet, a utility framework, or injected by JS all produce it. Required because a checker without this state reports an unreadable page as a clean one — proved 2026-09-08 on fixtures/external-css.html, which carries two real failures and scored zero findings before the state existed."
    },
    "independence": "Per design-studio-plan §3.6 (F7): the verifier derives every subject itself and only cross-checks the model's data-* hooks. Headline = largest rendered text. Primary CTA = filled control whose background is within ΔE of brand accent. Section = top-level landmark or a direct child of main. A hook/DOM mismatch is a conformance warning, never a design finding.",
    "threshold": "The value the rule fails at or beyond. Where a rule is a plain ban the threshold is the string 'any'.",
    "rewrite": "Fed back verbatim to the generator by the retry protocol (design-studio-plan §3.9) as the next instruction.",
    "reporting": "Every report, including anything a member or an operator sees, states findings AND a decided count. A finding count on its own is invalid output: it cannot distinguish a clean page from an unreadable one. An undecidable check is information, not an absence, and is named in the report with its rule id. A bar of the form 'must trip 0' is only met when nothing was undecided. Requirement raised by the ghl-command-mcp lane 2026-09-08 and adopted into the contract."
  },
  "boundary": {
    "ownedHere": "Everything visible on the rendered page: layout, colour, type, decoration, motion, interaction states, and the literal strings as they render.",
    "ownedByCopyGate": "Tone, claims, persuasion structure and voice. Those never appear here.",
    "deliberateOverlap": "The renderedText group duplicates a handful of Copy Gate string bans on purpose. The Copy Gate grades copy before it becomes HTML and never sees the final page, so alt text, aria-labels, button labels and image captions reach the browser ungraded. This group is the last filter on the artifact itself.",
    "duplicatesOf": "Rules marked duplicateOf restate a web/slide rule that already exists in rules/web.json. They stay here so an anti-default-only run is self-contained; the verifier must report each finding once, keyed on the web.json id."
  },
  "provenance": {
    "note": "Command OS is a paid licensed product. These attribution lines must ship with the package that carries this file.",
    "sources": [
      {
        "id": "plan",
        "name": "shared/design-studio-plan.md §3.11b",
        "licence": "internal",
        "obligation": "none"
      },
      {
        "id": "impeccable",
        "name": "pbakaus/impeccable, reached via h3nryprod01/design-taste reference/anti-slop.md Part 1 and reference/interaction-states.md",
        "licence": "Apache-2.0",
        "derivedFrom": "Anthropic frontend-design skill (Apache-2.0)",
        "obligation": "Ship LICENSES/Apache-2.0.txt and a NOTICE naming Paul Bakaus and Anthropic, Inc. Same obligation Agency OS already discharges in skills/frontend-design/LICENSE-APACHE-2.0."
      },
      {
        "id": "taste-skill",
        "name": "leonxlnx/taste-skill, reached via design-taste reference/anti-slop.md Part 2 and reference/pre-flight.md",
        "licence": "MIT",
        "obligation": "Copyright and permission notice."
      },
      {
        "id": "emil",
        "name": "emilkowalski/skills review-animations/STANDARDS.md",
        "licence": "MIT",
        "obligation": "Copyright and permission notice."
      },
      {
        "id": "house",
        "name": "Standing house rule set by the owner",
        "licence": "internal",
        "obligation": "none"
      }
    ],
    "restated": "Rules below carry facts and thresholds, not prose. No block of upstream text is reproduced verbatim in this file."
  },
  "fixtures": {
    "mustTrip": {
      "file": "fixtures/claude-default.html",
      "bar": "≥5 tells light up",
      "why": "design-studio-plan §3.11b names this fixture as the ruleset's own proof."
    },
    "mustStayClean": {
      "file": "fixtures/house-clean.html",
      "bar": "0 tells light up",
      "why": "A tell catalogue with no false-positive fixture grades every honest page as slop. This is the other half of the proof."
    },
    "mustNotOverclaim": {
      "file": "fixtures/external-css.html",
      "bar": "0 findings and >= 20 undecidable",
      "why": "A page carrying real failures behind an external stylesheet. Proves the checker refuses to decide what it cannot see, rather than scoring it clean."
    }
  },
  "rules": [
    {
      "id": "AD01",
      "group": "type",
      "medium": "both",
      "name": "Default display face",
      "threshold": "any",
      "unit": "font-family",
      "check": "measured",
      "howMeasured": "computed font-family of the largest rendered text run on the page resolves to Inter, Space Grotesk, Roboto, Arial or Helvetica.",
      "rewrite": "Pick a display face that belongs to this brand. If the brand has no licensed face, use the brand-dna googleFallback, never the generator default.",
      "source": "plan"
    },
    {
      "id": "AD02",
      "group": "type",
      "medium": "web",
      "name": "Body copy size floor",
      "threshold": 18,
      "unit": "px",
      "check": "measured",
      "howMeasured": "computed font-size of the dominant body run (the longest text run outside nav, footer and caption roles) at 1440x900. Below 18px fails.",
      "rewrite": "Raise body copy to 18px or larger. 16px is a house failure, not a preference.",
      "source": "house"
    },
    {
      "id": "AD03",
      "group": "type",
      "medium": "web",
      "name": "Body copy is dark, not muted grey",
      "threshold": 7.0,
      "unit": "contrast ratio",
      "check": "measured",
      "howMeasured": "WCAG contrast of the dominant body run against its own resolved background. Below 7.0:1 fails. Verified separation: #4a5163 on #ffffff is 7.93:1 and on #f7f7f5 is 7.39:1; #666666 on #ffffff is 5.74:1; Tailwind gray-500 #6b7280 and zinc-500 #71717a are both 4.83:1.",
      "rewrite": "Darken body copy until it clears 7:1 against its own background. Stated as a ratio, not a hex, so it holds in dark mode and on brand-tinted neutrals.",
      "source": "house"
    },
    {
      "id": "AD04",
      "group": "type",
      "medium": "both",
      "name": "Flat type scale",
      "threshold": 1.25,
      "unit": "ratio between adjacent steps",
      "check": "measured",
      "howMeasured": "sorted distinct computed font-sizes of heading-role elements; any adjacent pair whose ratio is below 1.25 fails.",
      "rewrite": "Open the scale. Hierarchy comes from scale plus weight contrast, not from four sizes within 2px of each other.",
      "source": "taste-skill"
    },
    {
      "id": "AD05",
      "group": "type",
      "medium": "web",
      "name": "Line length",
      "threshold": 75,
      "unit": "ch",
      "check": "measured",
      "howMeasured": "rendered width of the dominant body run divided by the width of a '0' glyph at that computed style, at 1440x900.",
      "rewrite": "Cap the measure at 65-75ch with max-width on the text column.",
      "source": "taste-skill"
    },
    {
      "id": "AD06",
      "group": "type",
      "medium": "web",
      "name": "Body leading",
      "threshold": "outside 1.45-1.65",
      "unit": "unitless line-height",
      "check": "measured",
      "howMeasured": "computed line-height / font-size on the dominant body run.",
      "rewrite": "Set body line-height between 1.5 and 1.6. Headings go tight, 1.1 to 1.2.",
      "source": "taste-skill"
    },
    {
      "id": "AD07",
      "group": "type",
      "medium": "both",
      "name": "Font family count",
      "threshold": 3,
      "unit": "distinct families",
      "check": "measured",
      "howMeasured": "distinct first-choice families across every computed font-family on rendered text, generic fallbacks excluded. More than 3 fails.",
      "rewrite": "Display plus body plus optional mono. Drop the rest.",
      "source": "taste-skill"
    },
    {
      "id": "AD08",
      "group": "type",
      "medium": "web",
      "name": "Runaway display size",
      "threshold": 96,
      "unit": "px",
      "check": "measured",
      "howMeasured": "computed font-size of the h1 / largest heading at 1440x900. Above 96px fails.",
      "rewrite": "Cap the hero clamp max at 6rem. Control hierarchy with weight and colour, not raw scale.",
      "source": "taste-skill"
    },
    {
      "id": "AD09",
      "group": "type",
      "medium": "both",
      "name": "Letters touching",
      "threshold": -0.04,
      "unit": "em letter-spacing",
      "check": "measured",
      "howMeasured": "computed letter-spacing on display-role text, normalised to em. Tighter than -0.04em fails.",
      "rewrite": "Loosen display tracking to -0.04em or wider.",
      "source": "taste-skill"
    },
    {
      "id": "AD10",
      "group": "type",
      "medium": "both",
      "name": "All-caps body copy",
      "threshold": "any",
      "unit": "run",
      "check": "measured",
      "howMeasured": "any text run longer than 120 characters with computed text-transform uppercase, or whose rendered string is all uppercase.",
      "rewrite": "Sentence case. Reserve caps for a short label.",
      "source": "taste-skill"
    },
    {
      "id": "AD11",
      "group": "type",
      "medium": "both",
      "name": "Reflex serif",
      "threshold": "any",
      "unit": "font-family",
      "check": "measured",
      "howMeasured": "display face resolves to Fraunces or Instrument Serif.",
      "rewrite": "These two are the current AI-default serifs. Choose a different serif, or drop the serif; 'feels premium' is not a reason.",
      "source": "taste-skill"
    },
    {
      "id": "AD12",
      "group": "colour",
      "medium": "both",
      "name": "Pure black on pure white",
      "threshold": "any",
      "unit": "pair",
      "check": "measured",
      "howMeasured": "body or heading colour resolves to #000000 on a #ffffff ground, or the inverse.",
      "rewrite": "Off-black and off-white. Pure values kill depth.",
      "source": "plan"
    },
    {
      "id": "AD13",
      "group": "colour",
      "medium": "both",
      "name": "More than one accent",
      "threshold": 1,
      "unit": "accent hue",
      "check": "proxy",
      "howMeasured": "distinct hues among background-colors of filled interactive controls with OKLCH chroma above 0.08, clustered at 15 degrees. More than one cluster fails.",
      "rewrite": "Lock one accent across the whole page. Secondary emphasis comes from weight, size and neutrals.",
      "source": "taste-skill"
    },
    {
      "id": "AD14",
      "group": "colour",
      "medium": "both",
      "name": "Oversaturated accent",
      "threshold": 0.8,
      "unit": "normalised saturation",
      "check": "measured",
      "howMeasured": "HSL saturation of the accent cluster centroid. Above 0.80 fails unless brand-dna.colors.accent itself is above it.",
      "rewrite": "Desaturate the accent so it sits with the neutrals instead of vibrating against them.",
      "source": "taste-skill"
    },
    {
      "id": "AD15",
      "group": "colour",
      "medium": "both",
      "name": "Gradient hero",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "computed background-image of the first full-width section contains linear-gradient or radial-gradient.",
      "rewrite": "Solid ground, or a real photographic or textural asset. The gradient hero is the single most recognisable generated-page signature.",
      "source": "plan"
    },
    {
      "id": "AD16",
      "group": "colour",
      "medium": "both",
      "name": "Gradient text",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "any element with background-clip or -webkit-background-clip set to text and a gradient background-image.",
      "rewrite": "One solid colour. Emphasis via weight or size.",
      "source": "impeccable"
    },
    {
      "id": "AD17",
      "group": "colour",
      "medium": "both",
      "name": "AI purple-blue glow",
      "threshold": "any",
      "unit": "hue window",
      "check": "proxy",
      "howMeasured": "accent hue falls in 250-285 degrees AND the page carries a box-shadow or filter using that hue at above 0.5 alpha.",
      "rewrite": "Different hue family, or drop the glow. Inner borders and tinted shadows do the same job without the tell.",
      "source": "taste-skill"
    },
    {
      "id": "AD18",
      "group": "colour",
      "medium": "both",
      "name": "Premium-consumer default palette",
      "threshold": 12,
      "unit": "ΔE2000 to cluster centroid",
      "check": "proxy",
      "howMeasured": "the token triple (ground, display colour, accent) sits within ΔE 12 of the cream / brass / oxblood / espresso centroid AND the display face is a serif.",
      "rewrite": "This is the reflex palette for cookware, wellness, artisan and luxury briefs. Pick a different family and say which.",
      "source": "plan"
    },
    {
      "id": "AD19",
      "group": "colour",
      "medium": "both",
      "name": "Placeholder contrast",
      "threshold": 4.5,
      "unit": "contrast ratio",
      "check": "measured",
      "howMeasured": "computed ::placeholder colour against the input's resolved background.",
      "rewrite": "Placeholder text is text. Bring it to 4.5:1.",
      "source": "taste-skill",
      "duplicateOf": "W11"
    },
    {
      "id": "AD20",
      "group": "colour",
      "medium": "both",
      "name": "CTA label contrast",
      "threshold": 4.5,
      "unit": "contrast ratio",
      "check": "measured",
      "howMeasured": "label colour of every filled control against that control's own background.",
      "rewrite": "The button no one can read is the button no one clicks.",
      "source": "taste-skill",
      "duplicateOf": "W11"
    },
    {
      "id": "AD21",
      "group": "layout",
      "medium": "web",
      "name": "Three identical cards in a row",
      "threshold": "any",
      "unit": "row",
      "check": "measured",
      "howMeasured": "a grid or flex row of exactly 3 boxed siblings whose widths agree within 4px and whose child tag sequences are identical.",
      "rewrite": "Two-column zigzag, an asymmetric grid, a horizontal scroller, or a list. Not everything in the row is equally important.",
      "source": "plan"
    },
    {
      "id": "AD22",
      "group": "layout",
      "medium": "web",
      "name": "Identical card grid at any width",
      "threshold": 4,
      "unit": "matching siblings",
      "check": "measured",
      "howMeasured": "4 or more boxed siblings with identical child tag sequences, equal widths within 4px, and the same computed border-radius, padding and background.",
      "rewrite": "Vary size, emphasis or content shape across the set, or group with dividers and space instead of boxes.",
      "source": "impeccable"
    },
    {
      "id": "AD23",
      "group": "layout",
      "medium": "web",
      "name": "Nested cards",
      "threshold": "any",
      "unit": "nesting",
      "check": "measured",
      "howMeasured": "an element carrying both a border or box-shadow and a non-transparent background, contained by another element with the same combination.",
      "rewrite": "Nested cards are always wrong. Flatten the inner one to spacing.",
      "source": "taste-skill"
    },
    {
      "id": "AD24",
      "group": "layout",
      "medium": "web",
      "name": "Eyebrow on every section",
      "threshold": "ceil(sectionCount / 3)",
      "unit": "eyebrows",
      "check": "measured",
      "howMeasured": "count text runs under 40 characters whose computed text-transform is uppercase and letter-spacing is above 0.05em, positioned directly above a heading-role element. Hero counts as 1. Exceeding ceil(sectionCount / 3) fails.",
      "rewrite": "One deliberate kicker is voice. An eyebrow above every section is AI grammar. Delete the rest.",
      "source": "impeccable"
    },
    {
      "id": "AD25",
      "group": "layout",
      "medium": "both",
      "name": "Numbered section markers",
      "threshold": "any",
      "unit": "marker",
      "check": "measured",
      "howMeasured": "an eyebrow-shaped run matching ^\\s*(0?\\d{1,3})\\s*[·./|-]?\\s*\\S, or containing 'INDEX', where the page has no ordered-sequence semantics.",
      "rewrite": "Numbers earn their place only when the section really is a sequence the reader must follow in order. Name the topic in plain language instead.",
      "source": "taste-skill"
    },
    {
      "id": "AD26",
      "group": "layout",
      "medium": "web",
      "name": "One radius on everything",
      "threshold": 0.9,
      "unit": "share of boxed elements",
      "check": "proxy",
      "howMeasured": "share of elements with a non-zero border-radius that carry the single most common radius value. At or above 0.90 fails.",
      "rewrite": "One radius system does not mean one radius value. Differentiate cards, inputs, tags and buttons.",
      "source": "plan"
    },
    {
      "id": "AD27",
      "group": "layout",
      "medium": "web",
      "name": "Over-rounded cards",
      "threshold": 24,
      "unit": "px",
      "check": "measured",
      "howMeasured": "computed border-radius on a card-role element (bordered or shadowed box containing a heading and body text) at or above 24px. Pills on tags and buttons are exempt.",
      "rewrite": "Cards top out at 12-16px. 24, 28, 32 and 40px on a card is a generator tell.",
      "source": "impeccable"
    },
    {
      "id": "AD28",
      "group": "layout",
      "medium": "web",
      "name": "Ghost card",
      "threshold": 16,
      "unit": "px shadow blur alongside a 1px border",
      "check": "measured",
      "howMeasured": "an element with border-width 1px AND a box-shadow whose blur radius is 16px or greater.",
      "rewrite": "Pick one. A single solid border at the brand colour, or a defined shadow at 8px blur or less. Never both as decoration.",
      "source": "impeccable"
    },
    {
      "id": "AD29",
      "group": "layout",
      "medium": "web",
      "name": "Panic z-index",
      "threshold": 999,
      "unit": "z-index",
      "check": "measured",
      "howMeasured": "any computed z-index at or above 999.",
      "rewrite": "Semantic scale: dropdown, sticky, modal, toast, tooltip. 9999 means the stacking context was never designed.",
      "source": "taste-skill"
    },
    {
      "id": "AD30",
      "group": "layout",
      "medium": "both",
      "name": "Everything centred",
      "threshold": 0.6,
      "unit": "share of text blocks",
      "check": "proxy",
      "howMeasured": "share of text-bearing blocks with computed text-align center. Above 0.60 fails.",
      "rewrite": "Centre the hero if it earns it. Left-align the rest; centred body copy is hard to read and reads as a template.",
      "source": "plan"
    },
    {
      "id": "AD31",
      "group": "layout",
      "medium": "web",
      "name": "Nav height and wrap",
      "threshold": 80,
      "unit": "px",
      "check": "measured",
      "howMeasured": "bounding height of the nav landmark at 1440x900, and whether its direct children occupy more than one line box.",
      "rewrite": "One line, 80px or less at desktop.",
      "source": "taste-skill"
    },
    {
      "id": "AD32",
      "group": "layout",
      "medium": "web",
      "name": "Hero does not fit the fold",
      "threshold": "any",
      "unit": "overflow",
      "check": "measured",
      "howMeasured": "at 1440x900 and 390x844: h1 renders in more than 2 line boxes, OR the subline exceeds 20 words, OR no primary CTA intersects the first viewport rectangle.",
      "rewrite": "Headline 2 lines, subtext 20 words, CTA above the fold on both sizes.",
      "source": "taste-skill",
      "duplicateOf": "W6"
    },
    {
      "id": "AD33",
      "group": "layout",
      "medium": "web",
      "name": "Hero floating down the page",
      "threshold": 96,
      "unit": "px top padding",
      "check": "measured",
      "howMeasured": "computed padding-top of the hero section at 1440x900. Above 96px fails.",
      "rewrite": "The hero starts near the top. Pushing content halfway down the viewport is padding by reflex.",
      "source": "taste-skill"
    },
    {
      "id": "AD34",
      "group": "layout",
      "medium": "web",
      "name": "Hero stack overload",
      "threshold": 4,
      "unit": "text elements",
      "check": "measured",
      "howMeasured": "count of distinct text-bearing blocks that are descendants of the hero section, CTA labels excluded. More than 4 fails.",
      "rewrite": "Eyebrow or brand strip, headline, subtext, CTAs. No tagline under the buttons, no trust micro-strip in the hero.",
      "source": "taste-skill"
    },
    {
      "id": "AD35",
      "group": "layout",
      "medium": "web",
      "name": "Split header floater",
      "threshold": "any",
      "unit": "section header",
      "check": "proxy",
      "howMeasured": "a section header containing a heading in the left 60 percent and a text block under 200 characters in the right 40 percent, whose top edges differ by more than 8px and which shares no grid line with the heading.",
      "rewrite": "Put the sub-text under the headline, or build an aligned two-column header. The tiny corner paragraph is the tell.",
      "source": "taste-skill"
    },
    {
      "id": "AD36",
      "group": "layout",
      "medium": "web",
      "name": "Zigzag on repeat",
      "threshold": 3,
      "unit": "consecutive sections",
      "check": "proxy",
      "howMeasured": "3 or more consecutive sections classified into the same layout family, where family is derived from child count, column count and media position.",
      "rewrite": "Break the alternation. Three image-and-text splits in a row is one idea repeated, not a page.",
      "source": "taste-skill"
    },
    {
      "id": "AD37",
      "group": "layout",
      "medium": "web",
      "name": "Layout family variety",
      "threshold": 4,
      "unit": "distinct families across 8+ sections",
      "check": "proxy",
      "howMeasured": "on a page with 8 or more sections, fewer than 4 distinct layout families fails.",
      "rewrite": "A page needs at least four ways of arranging things before it stops reading as a template.",
      "source": "taste-skill"
    },
    {
      "id": "AD38",
      "group": "layout",
      "medium": "web",
      "name": "Duplicate CTA intent",
      "threshold": "any",
      "unit": "pair",
      "check": "judged",
      "howMeasured": "two CTA labels that mean the same thing in different words. Normalised string equality catches only the trivial case, so this is listed for the model, never scored.",
      "rewrite": "One intent, one label, repeated verbatim if it repeats.",
      "source": "taste-skill"
    },
    {
      "id": "AD39",
      "group": "layout",
      "medium": "both",
      "name": "Text overflows its container",
      "threshold": "any",
      "unit": "px overflow",
      "check": "measured",
      "howMeasured": "at 390, 768 and 1440 wide: any text-bearing element whose scrollWidth exceeds its clientWidth, or whose bounding box escapes its parent's padding box.",
      "rewrite": "Test the real copy at every breakpoint. Reduce the clamp max or rewrite the headline. The viewport is part of the design.",
      "source": "impeccable"
    },
    {
      "id": "AD40",
      "group": "layout",
      "medium": "web",
      "name": "Viewport instability",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "a full-height container sized with 100vh rather than 100dvh, or documentElement.scrollWidth exceeding the viewport width at 390.",
      "rewrite": "min-height: 100dvh, never height: 100vh. No horizontal scroll on any breakpoint.",
      "source": "taste-skill"
    },
    {
      "id": "AD41",
      "group": "layout",
      "medium": "web",
      "name": "Hairline under every row",
      "threshold": 5,
      "unit": "rows",
      "check": "measured",
      "howMeasured": "a list or table with more than 5 sibling rows where every row carries both a top and a bottom border.",
      "rewrite": "Pick one edge and use it sparsely, or move to a component that suits a long list.",
      "source": "taste-skill"
    },
    {
      "id": "AD42",
      "group": "decoration",
      "medium": "both",
      "name": "Side-stripe accent border",
      "threshold": 1,
      "unit": "px",
      "check": "measured",
      "howMeasured": "border-left-width or border-right-width above 1px with a colour distinct from the other three edges.",
      "rewrite": "Full border, background tint, a leading number or icon, or nothing.",
      "source": "impeccable",
      "alsoIn": "plan"
    },
    {
      "id": "AD43",
      "group": "decoration",
      "medium": "both",
      "name": "Decorative glassmorphism",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "computed backdrop-filter containing blur on an element that is not a modal, sheet or sticky nav.",
      "rewrite": "Rare and purposeful, or nothing.",
      "source": "impeccable"
    },
    {
      "id": "AD44",
      "group": "decoration",
      "medium": "both",
      "name": "Stripe background",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "computed background-image contains repeating-linear-gradient or repeating-radial-gradient.",
      "rewrite": "Remove it. Diagonal stripes behind a section are pure generator decoration.",
      "source": "impeccable"
    },
    {
      "id": "AD45",
      "group": "decoration",
      "medium": "both",
      "name": "Sketchy SVG illustration",
      "threshold": "any",
      "unit": "asset",
      "check": "measured",
      "howMeasured": "inline SVG containing feTurbulence or feDisplacementMap, or a class or id matching sketch, doodle, wavy, squiggle, hand-drawn.",
      "rewrite": "If the scene cannot be rendered with a real asset, ship no illustration. Sketchy SVG reads as amateurish, not whimsical.",
      "source": "impeccable"
    },
    {
      "id": "AD46",
      "group": "decoration",
      "medium": "both",
      "name": "Emoji as section marker",
      "threshold": "any",
      "unit": "codepoint",
      "check": "measured",
      "howMeasured": "an emoji codepoint inside a heading-role element or an eyebrow run.",
      "rewrite": "Use a real icon from the chosen set, or nothing.",
      "source": "plan"
    },
    {
      "id": "AD47",
      "group": "decoration",
      "medium": "web",
      "name": "Decorative status dots",
      "threshold": 1,
      "unit": "dot per section",
      "check": "proxy",
      "howMeasured": "elements under 16px square with border-radius at or above 50 percent and a saturated background, positioned before a text run. More than one per section fails; the default expectation is zero.",
      "rewrite": "A dot is allowed only when it carries real semantic state. A dot before every nav link or list row is decoration.",
      "source": "taste-skill"
    },
    {
      "id": "AD48",
      "group": "decoration",
      "medium": "both",
      "name": "Middle-dot separator run",
      "threshold": 1,
      "unit": "· per line",
      "check": "measured",
      "howMeasured": "more than one U+00B7 in a single rendered line box.",
      "rewrite": "One middle dot in a metadata strip is fine. A chain of them is a separator family that should have been line breaks, hairlines or columns.",
      "source": "taste-skill"
    },
    {
      "id": "AD49",
      "group": "decoration",
      "medium": "web",
      "name": "Div-based fake screenshot",
      "threshold": "any",
      "unit": "block",
      "check": "proxy",
      "howMeasured": "a hero-region subtree with 8 or more descendant elements carrying background, border-radius and border, containing no img, picture, video, canvas or svg with real content.",
      "rewrite": "Real screenshot, a generated image, a live component, or no preview. Fake product UI built out of divs is the most recognisable generated-page tell there is.",
      "source": "taste-skill"
    },
    {
      "id": "AD50",
      "group": "decoration",
      "medium": "web",
      "name": "Scroll cue",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "a rendered run in the first viewport matching /^(scroll|↓\\s*scroll|scroll to (explore|discover|begin|walk))/i, or an element with a scroll-hint class and a looping animation.",
      "rewrite": "The reader is looking at the hero. They know what scrolling is.",
      "source": "taste-skill"
    },
    {
      "id": "AD51",
      "group": "decoration",
      "medium": "web",
      "name": "Hero-bottom decoration strip",
      "threshold": "any",
      "unit": "strip",
      "check": "proxy",
      "howMeasured": "a run in the lower quarter of the hero, computed text-transform uppercase, under 60 characters, containing 2 or more separators, whose tokens are not links.",
      "rewrite": "Allowed only when the strip carries real navigable links or real status. Otherwise delete it.",
      "source": "taste-skill"
    },
    {
      "id": "AD52",
      "group": "decoration",
      "medium": "web",
      "name": "Locale, time or weather strip",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "a header or footer run matching a city name adjacent to a time pattern \\d{1,2}:\\d{2} or a temperature pattern -?\\d{1,3}\\s?°[CF].",
      "rewrite": "Allowed only for a genuinely distributed studio, a travel brand or a real physical venue. A contact address in the footer is fine; an atmospheric locale strip is not.",
      "source": "taste-skill"
    },
    {
      "id": "AD53",
      "group": "decoration",
      "medium": "web",
      "name": "Version or status label in the hero",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "a run inside the hero matching /\\bv?\\d+\\.\\d+(\\.\\d+)?\\b|\\b(beta|alpha|early access|invite[- ]only|preview|waitlist)\\b/i.",
      "rewrite": "Only when the brief is explicitly about launch status.",
      "source": "taste-skill"
    },
    {
      "id": "AD54",
      "group": "decoration",
      "medium": "web",
      "name": "Version footer on a marketing page",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "a footer run matching /\\bv\\d+\\.\\d+\\.\\d+|build \\d+|last sync|\\bmain\\b\\s*$/i on a page whose medium is marketing or landing.",
      "rewrite": "CLI and devtool fixtures, not landing-page content.",
      "source": "taste-skill"
    },
    {
      "id": "AD55",
      "group": "decoration",
      "medium": "web",
      "name": "Label overlaid on an image",
      "threshold": "any",
      "unit": "overlay",
      "check": "measured",
      "howMeasured": "an absolutely or fixed positioned text element whose bounding box intersects an img or picture by more than 50 percent of its own area.",
      "rewrite": "Let the image speak, or caption it directly below and outside the frame.",
      "source": "taste-skill"
    },
    {
      "id": "AD56",
      "group": "decoration",
      "medium": "both",
      "name": "Decorative photo credit",
      "threshold": "any",
      "unit": "string",
      "check": "proxy",
      "howMeasured": "a caption adjacent to an image matching /(plate|frame|field study|study|archive)\\s*(no\\.?\\s*)?[\\divxIVX]+/i where the image src resolves to a placeholder host.",
      "rewrite": "Credit a real photographer for a real photo, or write one functional caption, or none.",
      "source": "taste-skill"
    },
    {
      "id": "AD57",
      "group": "decoration",
      "medium": "web",
      "name": "Filled-track score bar",
      "threshold": "any",
      "unit": "bar",
      "check": "proxy",
      "howMeasured": "an element with a non-transparent neutral background containing a narrower child with a saturated background, in a non-form, non-dashboard context.",
      "rewrite": "A number with a small icon, or a thin inline bar with no background track. Big filled tracks are dashboard clutter on a marketing page.",
      "source": "taste-skill"
    },
    {
      "id": "AD58",
      "group": "decoration",
      "medium": "web",
      "name": "More than one marquee",
      "threshold": 1,
      "unit": "marquee",
      "check": "measured",
      "howMeasured": "count of horizontally translating infinite animations with a duration above 5s.",
      "rewrite": "One marquee at most, and only if it carries content worth reading twice.",
      "source": "taste-skill"
    },
    {
      "id": "AD59",
      "group": "decoration",
      "medium": "web",
      "name": "Custom cursor",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "computed cursor resolves to a url() on any element, or a fixed-position element that tracks pointermove.",
      "rewrite": "Outdated, accessibility-hostile and slow. Use the system cursor.",
      "source": "taste-skill"
    },
    {
      "id": "AD60",
      "group": "motion",
      "medium": "web",
      "name": "transition: all",
      "threshold": "any",
      "unit": "declaration",
      "check": "measured",
      "howMeasured": "computed transition-property contains 'all' on any element.",
      "rewrite": "Name the properties: transition: transform 200ms var(--ease-out).",
      "source": "emil"
    },
    {
      "id": "AD61",
      "group": "motion",
      "medium": "web",
      "name": "Entry from scale(0)",
      "threshold": "any",
      "unit": "keyframe",
      "check": "measured",
      "howMeasured": "a keyframe or starting-style declaring a transform scale at or below 0.5.",
      "rewrite": "Start at scale(0.95) with opacity 0. Nothing in the real world appears from nothing.",
      "source": "emil"
    },
    {
      "id": "AD62",
      "group": "motion",
      "medium": "web",
      "name": "ease-in on UI",
      "threshold": "any",
      "unit": "timing function",
      "check": "measured",
      "howMeasured": "computed transition-timing-function or animation-timing-function resolves to ease-in, or to a cubic-bezier whose first control point x is above its y, on an interactive element.",
      "rewrite": "ease-out, or a strong custom curve: cubic-bezier(0.23, 1, 0.32, 1). ease-in delays the exact moment the user is watching.",
      "source": "emil"
    },
    {
      "id": "AD63",
      "group": "motion",
      "medium": "web",
      "name": "UI animation over 300ms",
      "threshold": 300,
      "unit": "ms",
      "check": "measured",
      "howMeasured": "computed transition-duration or animation-duration on an interactive element, modals and drawers excluded (their budget is 500ms).",
      "rewrite": "Button press 100-160ms. Tooltip 125-200ms. Dropdown 150-250ms. A 180ms dropdown feels faster than a 400ms one.",
      "source": "emil"
    },
    {
      "id": "AD64",
      "group": "motion",
      "medium": "web",
      "name": "Animating layout properties",
      "threshold": "any",
      "unit": "property",
      "check": "measured",
      "howMeasured": "transition-property or an animated keyframe touches width, height, top, left, right, bottom, margin or padding.",
      "rewrite": "transform and opacity only. Everything else forces layout and paint on every frame.",
      "source": "emil"
    },
    {
      "id": "AD65",
      "group": "motion",
      "medium": "web",
      "name": "No reduced-motion fallback",
      "threshold": "any",
      "unit": "rule",
      "check": "measured",
      "howMeasured": "the stylesheet declares one or more animations or transitions with movement but contains no prefers-reduced-motion media block.",
      "rewrite": "Add the block. Reduced motion means gentler, not zero: keep opacity and colour, drop the movement.",
      "source": "emil"
    },
    {
      "id": "AD66",
      "group": "motion",
      "medium": "web",
      "name": "Ungated hover motion",
      "threshold": "any",
      "unit": "rule",
      "check": "measured",
      "howMeasured": "a :hover rule animating transform that is not nested inside @media (hover: hover) and (pointer: fine).",
      "rewrite": "Gate it. Touch devices fire a false hover on tap and the state sticks.",
      "source": "emil"
    },
    {
      "id": "AD67",
      "group": "motion",
      "medium": "web",
      "name": "Centre-origin popover",
      "threshold": "any",
      "unit": "declaration",
      "check": "proxy",
      "howMeasured": "an element with popover, role=menu, role=tooltip or role=listbox whose computed transform-origin is the box centre. Modals and dialogs are exempt and stay centred.",
      "rewrite": "Scale from the trigger. transform-origin: var(--radix-popover-content-transform-origin) or the Base UI equivalent.",
      "source": "emil"
    },
    {
      "id": "AD68",
      "group": "motion",
      "medium": "both",
      "name": "Everything enters at once",
      "threshold": "30-80",
      "unit": "ms stagger",
      "check": "proxy",
      "howMeasured": "4 or more siblings share an identical entrance animation with zero delay spread. Where a stagger exists, a per-item delay outside 30-80ms also fails.",
      "rewrite": "Stagger by 30-80ms. Longer feels slow, and stagger must never block interaction while it plays.",
      "source": "emil"
    },
    {
      "id": "AD69",
      "group": "states",
      "medium": "web",
      "name": "Focus removed without replacement",
      "threshold": "any",
      "unit": "rule",
      "check": "measured",
      "howMeasured": "a rule setting outline to none or 0 on a focusable element with no :focus-visible rule restoring a visible indicator on the same selector.",
      "rewrite": "Never remove focus without replacing it. Keyboard users never see hover.",
      "source": "impeccable",
      "duplicateOf": "W13"
    },
    {
      "id": "AD70",
      "group": "states",
      "medium": "web",
      "name": "Placeholder used as label",
      "threshold": "any",
      "unit": "field",
      "check": "measured",
      "howMeasured": "an input or textarea with a placeholder attribute and no associated label element, aria-label or aria-labelledby.",
      "rewrite": "Label above the input. The placeholder disappears the moment the user starts typing.",
      "source": "impeccable"
    },
    {
      "id": "AD71",
      "group": "states",
      "medium": "web",
      "name": "Touch target below 44px",
      "threshold": 44,
      "unit": "px",
      "check": "measured",
      "howMeasured": "bounding box of any interactive element at 390x844, including padding. Either dimension below 44px fails.",
      "rewrite": "44px minimum on both axes at mobile.",
      "source": "impeccable"
    },
    {
      "id": "AD72",
      "group": "states",
      "medium": "web",
      "name": "Missing interactive states",
      "threshold": 8,
      "unit": "states",
      "check": "proxy",
      "howMeasured": "for each interactive component, presence of rules or attributes covering default, hover, focus-visible, active, disabled, loading, error and success. Absence of a state that the component can actually reach fails.",
      "rewrite": "Design all eight. A control with only default and hover is a mockup, not a component.",
      "source": "impeccable"
    },
    {
      "id": "AD73",
      "group": "states",
      "medium": "web",
      "name": "No empty, loading or error state",
      "threshold": "any",
      "unit": "state",
      "check": "judged",
      "howMeasured": "a data-bearing surface with no markup for the empty, loading or error case. Only a model can tell which surfaces are data-bearing, so this is listed, never scored.",
      "rewrite": "Every surface that can be empty, slow or broken needs its own designed state.",
      "source": "taste-skill"
    },
    {
      "id": "AD74",
      "group": "renderedText",
      "medium": "both",
      "name": "Em-dash or en-dash",
      "threshold": 0,
      "unit": "characters",
      "check": "measured",
      "howMeasured": "count of U+2014 and U+2013 across all rendered text, alt attributes, aria-label attributes, title attributes and button labels. Any occurrence fails.",
      "rewrite": "Zero. Not 'sparingly'. Two sentences with a period, or a comma, or parentheses, or a colon. Ranges take a hyphen.",
      "source": "house"
    },
    {
      "id": "AD75",
      "group": "renderedText",
      "medium": "both",
      "name": "Placeholder person name",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b(john|jane|joe|jack)\\s+(doe|smith|su|chan)\\b|\\blorem ipsum\\b/i.",
      "rewrite": "Real-sounding, locale-appropriate names, or real people with permission.",
      "source": "taste-skill"
    },
    {
      "id": "AD76",
      "group": "renderedText",
      "medium": "both",
      "name": "Placeholder brand name",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b(acme|nexus|smartflow|cloudly|innovatech|globex|initech|vertex solutions)\\b/i.",
      "rewrite": "Invent a name that sounds like it belongs to a real company in this category, or use the client's own.",
      "source": "taste-skill"
    },
    {
      "id": "AD77",
      "group": "renderedText",
      "medium": "both",
      "name": "Fake-perfect number",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b(99\\.9{1,2}%|100%|10x|50%|1,?234,?567)\\b/ in a statistic or social-proof position.",
      "rewrite": "Real numbers are messy. If the real number is unknown, remove the claim rather than rounding it to a marketing shape.",
      "source": "taste-skill"
    },
    {
      "id": "AD78",
      "group": "renderedText",
      "medium": "both",
      "name": "Filler verb",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b(elevate|seamless(ly)?|unleash|supercharge|streamline|empower|world[- ]class|next[- ]gen|revolutioni[sz]e|cutting[- ]edge|game[- ]chang)/i.",
      "rewrite": "Concrete verbs. Say what it does, not how it feels to say it.",
      "source": "taste-skill"
    },
    {
      "id": "AD79",
      "group": "renderedText",
      "medium": "both",
      "name": "Performative-craftsman label",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b(quietly (in use|trusted)|from the field|field notes|on our desks|currently on the bench|loose plates)\\b/i.",
      "rewrite": "Plain functional labels: Testimonials, Latest writing, Customers include. Or no label at all.",
      "source": "taste-skill"
    },
    {
      "id": "AD80",
      "group": "renderedText",
      "medium": "both",
      "name": "Generic step label",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b(stage|step|phase|pass)\\s*(one|two|three|four|0?\\d)\\b/i.",
      "rewrite": "The step content is the label. Install, Configure, Ship. Not 'Stage 1: Install'.",
      "source": "taste-skill"
    },
    {
      "id": "AD81",
      "group": "renderedText",
      "medium": "both",
      "name": "Meta-criticism copy shape",
      "threshold": "any",
      "unit": "string",
      "check": "measured",
      "howMeasured": "rendered text matches /\\b\\w+ theat(er|re)\\b|\\bnot just (a |an )?\\w+,? it'?s\\b|\\bactually \\w+, not\\b/i.",
      "rewrite": "Name a specific noun. Meta-criticism phrasing is instant slop.",
      "source": "impeccable"
    },
    {
      "id": "AD82",
      "group": "category",
      "medium": "both",
      "name": "Category reflex, first order",
      "threshold": "any",
      "unit": "judgement",
      "check": "judged",
      "howMeasured": "could a reader guess this palette and type family from the client's category alone? If yes, the design is the first training-data reflex. Listed for the model on every run; never scored.",
      "rewrite": "Rework the scene sentence and the colour strategy until the answer is not obvious from the category.",
      "source": "impeccable"
    },
    {
      "id": "AD83",
      "group": "category",
      "medium": "both",
      "name": "Category reflex, second order",
      "threshold": "any",
      "unit": "judgement",
      "check": "judged",
      "howMeasured": "could a reader guess the aesthetic family from category-plus-anti-reference, for example 'clinic that is not clinical-blue therefore warm-editorial'? If yes, the first reflex was avoided and the second was not. Listed on every run; never scored.",
      "rewrite": "Rework until neither question has an obvious answer.",
      "source": "impeccable"
    }
  ],
  "originalNine": {
    "why": "design-studio-plan §3.11b listed 9 tells. Every one is preserved; this maps each to its rule id so nothing was dropped in the expansion.",
    "map": {
      "Default faces: Inter, Space Grotesk, Roboto/Arial as display": "AD01",
      "Everything centred": "AD30",
      "Three identical cards in a row": "AD21",
      "Gradient hero": "AD15",
      "One corner radius on every block": "AD26",
      "Emoji as section markers": "AD46",
      "Cream ground + serif display + terracotta accent": "AD18",
      "Accent bar/rail on rounded cards; rounded-lg everywhere": "AD42 + AD26",
      "Pure #000 on #FFF, or #FFF on #000": "AD12"
    }
  }
}