Skip to content

DESIGN.md

English governs the contract statements on this page. Contract names, addresses and numbers are shown as they are on the chain.

KAY9 Design System

KAY9 — The On-Chain Watchdog. This document is the design source of truth for kay9.io and every KAY9 interface. It was produced in the Fable design phase and is what the implementation reproduces. If the implementation and this document disagree, fix the implementation.

The brand promise is simple: KAY9 watches so you do not have to trust. Everything on the site should feel like standing in a quiet, well-lit control room with a very alert dog.


1. Brand foundation

1.1 Canonical asset

Kay9.png (1254×1254, black ground, brass ring, spaniel portrait) is the only mascot. It is never redrawn, recolored, flipped, cropped through the face, or replaced by a generic dog. Derived assets live in apps/web/public/brand/ and are produced from the original by script:

FileUse
kay9-original.pngUntouched source, served for press / OG fallback
kay9-mark.png, kay9-mark-1024.png, kay9-mark-64.pngCircular cut with transparent corners; navigation, hero, loading
favicon.ico, favicon-16.png, favicon-32.png, apple-touch-icon.pngBrowser
icon-192.png, icon-512.png, icon-maskable-*.pngPWA
token-icon-256.pngToken lists, wallet asset icon
social-avatar.pngX / Telegram / Discord
og-image.png, og-image.jpgOpenGraph 1200×630
kay9-watermark.pngDesaturated 18 % alpha for background treatments only

The ring in the logo is the brand's structural motif. Every ring on the site (score rings, scanning ring, progress rings, timelock rings) is the same ring: brass, 6 px stroke at 240 px, rounded caps, sweeping clockwise from 12 o'clock.

1.2 Personality

Watchdog. Serious. Protective. Premium. Intelligent. Crypto-native. Trustworthy. Powerful. Slightly edgy.

What this means in practice:

  • Quiet confidence over hype. No rocket emojis, no "100x", no countdown-to-moon. The site states facts a contract can prove and links to the proof.
  • The dog is a guard, not a pet. The mascot is shown large, still, and lit from the ring. It is never bouncing, winking, or wearing hats. Motion around it (scanning ring, status ticks) is what moves.
  • Meme identity without cartoon styling. The meme lives in copy that is dry and a little edgy ("Bark first. Rug never."), not in cute illustration.
  • Premium means restraint. One accent, two type families, generous space, hairline structure only where it carries meaning.

1.3 Voice

  • Sentence case everywhere, including buttons and navigation. No tracked-out uppercase labels.
  • Plain verbs. "Request deep audit", "Read the contract", "Claim tokens". A button's label is the past-tense of its toast: "Request deep audit" → "Deep audit requested".
  • Say what is proven and where: "Fixed supply — enforced by KAY9Token, no mint function exists."
  • Never say "verified", "safe", or "approved" about an audited token. Say "scored 25/100 — high risk" and show the signals.
  • Never imply a creator receives 1 % of every trade. Say "Official pool fee: 1 %" and separately explain the LP / creator split that is actually deployed.
  • Simulated or demo data is always labelled, in the component itself: a small "Demo data" chip in the corner, brass outline, never hidden in a tooltip.

Sample lines (usable as copy):

  • Hero: KAY9 — The On-Chain Watchdog. "A fixed-supply token, a fair launch, permanent liquidity, and an audit protocol that writes its verdicts to the chain. The website is a window. The contracts are the truth."
  • Audit page: "Paying for an audit buys analysis, not a score. A token can pay and still get 25/100."
  • Transparency page: "Every line below is read from Robinhood Chain when you load this page. Nothing here is a promise."
  • Empty registry: "No reports on this chain yet. The watchdog is awake — request the first deep audit."
  • Error: "Robinhood Chain RPC did not answer. Showing the last values we read (block 56,063,858, 40 s ago). Retry."

2. Design tokens

All tokens are CSS custom properties on :root; Tailwind v4 @theme maps them. The site commits to a single dark look (the logo is black-ground; a light theme would fight the asset). color-scheme: dark is set explicitly and all surfaces are painted, never transparent.

2.1 Color

Base palette (6 named values, all sampled from or derived from Kay9.png):

TokenHexRole
--ground#000000Page ground behind the hero and mascot. True black because the asset is true black.
--surface#15120EDefault section and card surface (warm charcoal, from the ring's shadow).
--surface-2#221D16Raised surfaces: modals, table headers, inputs.
--ink#F3EBDDPrimary text (warm ivory, from the ring highlight #FDF497 desaturated).
--brass#F2B62AThe accent. Ring, primary buttons, live indicators, focus. From ring sample #F7B928.
--copper#8A4A1EDeep accent from the fur shadow #88441A: gradients behind the mascot, pressed states, secondary rings.

Supporting text and line tokens:

TokenHexRole
--ink-2#BEB4A2Secondary text
--ink-3#958D7FMuted text, timestamps, disabled. 5.7:1 on --surface, 5.1:1 on --surface-2; the previous #7F776A was 4.2:1 and 3.8:1, under the 4.5:1 normal-text minimum for the micro copy that uses it.
--line#2C261EHairlines, table rules
--line-strong#4A4032Input borders, active rules
--brass-softrgba(242,182,42,0.14)Brass tint for chips and selected rows
--brass-deep#B98418Brass at chart-safe lightness (validated)

Status palette (reserved; never reused for series). Validated with the dataviz palette script against #15120E: all checks pass.

TokenHexMeaningAlways paired with
--ok#2FA377Low risk / healthy / confirmedshield-check icon + label
--warn#C77D1AElevated risk / stale / pendingtriangle icon + label
--danger#D8434AHigh risk / failed / impossibleoctagon icon + label

Trust score bands (0 = worst reading, 100 = most trustworthy; scores are trust, higher is better):

BandRangeColorLabel
Green76–100--okLow risk
Amber41–75--warnElevated risk
Red0–40--dangerHigh risk

Why the number runs this way now, and what it does not change. The scale used to run the opposite direction — 0 safest, 100 worst — for reasons that were sound at the time (the on-chain field the site was reading was itself named and computed as risk, and inverting only the display would have meant the site and the chain disagreed about what a number meant). That is no longer the constraint: overallTrust is now the field's real name on the on-chain AuditResult, computed by the engine as 100 - risk before it is ever signed, hashed or displayed (toTrustScores in services/watchdog/src/engine/scoring.ts), so the site and the chain say the same thing under the same name. What did not change is everything the old three reasons were actually protecting:

  1. This project still may never call a token safe. A 100 is "nothing risky was found by this engine at this block", never a safety certificate, and the permitted phrasings in §19 and the band labels above (Low risk, never Safe or Verified) still hold exactly as before. The highest published number is not an endorsement any more than the lowest one was an accusation.
  2. Missing evidence still never looks clean. It used to raise the published risk; it now caps the published trust instead, so a category or an overall score the engine could not fully measure can never read as fully trustworthy — see UNCERTAINTY_FLOOR/CRITICAL_FLOOR/ HIGH_FLOOR in scoring.ts, which still operate as risk-space floors internally and become trust-space ceilings only at the one conversion boundary.
  3. A ring or a bar still fills in proportion to its value — and now that fill direction is no longer backwards: a trustworthy asset draws a nearly full ring, which is what every progress meter a reader has ever seen already trained them to expect. The rule that no surface may render a score without naming the axis (charts.trustAxis, "Trust") stays in force regardless, because a reader should never have to infer which axis a number is on — it is just no longer the load-bearing defence against a misreading that it used to be. This sits alongside the rule in §19 that no surface may render a score without its date: a bare number is not a result.

Chart categorical palette (fixed order, validated on #15120E, all six checks pass):

SlotHexAssigned to
1#B98418 (brass-deep)Public fair auction / KAY9 / "protocol" series
2#5E8FD6Permanent liquidity reserve / ETH-side series
3#C0602ATeam
4#8B7BE8Burn / other

Sequential ramp (single hue brass, light→dark) for magnitude: #F6DFA3 → #E9C25C → #C99A2A → #A0761B → #6F5010. Diverging (rarely needed): #5E8FD6 (cool) → #6E675C (neutral) → #C0602A (warm).

2.2 Typography

Two families, clearly distinct, both from Google Fonts with local fallbacks:

  • Bricolage Grotesque (display). Variable font; use wght 700–800, wdth 75–87, opsz auto. Its tightened width at large sizes gives KAY9 a bark that Inter-alikes cannot. Used for the wordmark, page titles, big numbers, section headings.
  • IBM Plex Sans (text) and IBM Plex Mono (data). Plex is engineered and slightly severe, which is the watchdog register. Numbers use font-variant-numeric: tabular-nums everywhere a column of numbers appears. Mono is only for hashes, addresses, calldata, and Q96 values, never for labels.

Fallback stacks: "Bricolage Grotesque", "Arial Narrow", "Helvetica Neue", Arial, sans-serif and "IBM Plex Sans", system-ui, "Segoe UI", sans-serif, "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace.

Type scale (Elements of Typographic Style, 1.25 ratio at text sizes, larger jumps for display). Base 17 px on desktop, 16 px on mobile.

TokenSize / lineFamily / weightUse
display-188 / 0.95, wdth 75Bricolage 800Hero title only
display-256 / 1.0, wdth 80Bricolage 800Page titles
h140 / 1.1, wdth 87Bricolage 700Section titles
h228 / 1.2Bricolage 700Card and subsection titles
h320 / 1.3Plex Sans 600Row titles
lead21 / 1.5Plex Sans 400Intro paragraphs
body17 / 1.6Plex Sans 400Body
small14 / 1.5Plex Sans 400Meta, table cells
micro12 / 1.4Plex Sans 500Chips, axis ticks
num-xl64 / 1.0Bricolage 800 tabularScore rings, hero stats
mono14 / 1.5Plex Mono 400Addresses, hashes

Rules: line length ≤ 72 characters for body (max-width: 62ch). Headlines are never one-word-accented. No eyebrow labels above headings unless the eyebrow carries data (e.g. a live block number). Mobile display sizes: display-1 48, display-2 36, h1 30.

2.3 Spacing, radius, elevation

  • Spacing scale: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128. Sections are separated by 96 (desktop) / 64 (mobile), never by cards.
  • Radius has meaning: r-ring 9999 for rings, chips, avatars; r-2 8 px for inputs and buttons; r-3 14 px for cards that are interactive (link cards, audit job cards). Static content blocks have no radius and no border; they are separated by space or a single hairline.
  • Elevation: there are no drop shadows. Raised surfaces use --surface-2 and a 1 px --line top edge. Modals add a 40 % black scrim. The only glow on the site is the brass halo behind the mascot (radial-gradient of --copper at 35 % → transparent), and the score ring's 1 px outer glow when a score finalizes.

2.4 Layout

  • Container max-width: 1200px, 12-column grid, 24 px gutters; 16 px on mobile.
  • Left-aligned, never centered, including the hero. Centered layouts read as templates; a watchdog reads a ledger from the left.
  • Asymmetry is the default: text 7 columns, evidence 5 columns. On mobile everything stacks in reading order: title, fact, proof.
  • The right column is where evidence lives: contract addresses, live values, rings, charts. The left column is where claims live. This claim/evidence split repeats on every page so the eye learns it.
Desktop (12 col)
┌──────────────────────────────┬────────────────────┐
│ claim (7)                    │ evidence (5)       │
│ headline, prose, CTA         │ ring / live values │
└──────────────────────────────┴────────────────────┘
Mobile
┌────────────────┐
│ claim          │
│ evidence       │
└────────────────┘

3. Structural devices

Structure encodes information; it never decorates.

  • The Ledger. A table with hairline rules, no zebra striping, no card wrapper. Used whenever content is claims verified against chain: transparency, contract permissions, allocation totals. Columns are always Claim | Value | Enforced by | Proof. "Proof" is a link to the explorer or a function name, never the word "verified".
  • Numbered sequence. Used only for genuine sequences: How it works (request → detect → analyze → sign → commit), fair-launch phases (auction → clearing → migration → trading), team unlock timeline. Never on feature grids.
  • The Ring. Score, progress, and time. Always brass on --line track, always with the number inside in num-xl and the band label under it. Rings do not animate on scroll; they animate once when their value arrives from chain.
  • Live dot. A 8 px brass dot with a slow 2 s pulse (opacity 1 → 0.4) means "read from chain in the last 60 s". If the read is older than 60 s the dot goes --ink-3 and stops pulsing; if the RPC failed it becomes a --warn triangle with "stale" text. Nothing else pulses on the site.
  • Chips. Radius full, 1 px border, micro type. Used for chain names, status, tiers, and the "Demo data" flag. Never for decoration.
  • Address block. Mono, 0x1234…abcd middle-truncated with a copy affordance and an explorer link, plus a "verified source" check when Blockscout reports the source as verified.

4. Motion

Motion budget: one orchestrated moment per page, plus responses to user actions. prefers-reduced-motion disables all non-essential motion and replaces it with instant state changes; rings then render at final value.

  • Page load (home only): the brass ring around the mascot draws itself once (1.2 s, ease-out), then a thin radar sweep (a 30° conic gradient at 12 % brass) rotates once (2 s) and stops at 12 o'clock. Four status ticks light up around the ring in sequence as each chain read resolves (block, token, pool, audit hub). That is the whole page-load sequence. No section fade-ins, no card hover lifts.
  • Score reveal (audit result): ring fills from 0 to score (900 ms, cubic-bezier(.2,.8,.2,1)); number counts up; band label fades in at the end. Plays once per result, on arrival.
  • Auction state: the clearing price line on the auction chart moves with a 300 ms ease when a new checkpoint arrives; the "your bid" marker slides with it.
  • Transaction states: button → "Confirm in wallet" (spinner, brass outline) → "Pending" (ring rotates) → "Confirmed" (ring fills, check) → resolves back to a link "View transaction". Errors replace the button label with the reason and offer "Try again".
  • Unlock countdown: team vesting rings tick once per second; digits do not flip or roll.
  • Hover: links underline (1 px, --brass); interactive cards get a --line-strong border. That is all.

5. Information architecture

Primary navigation (7 items; mobile collapses to a sheet):

KAY9  |  Token   Fair launch   Audit   Registry   Transparency   Docs  |  [Connect wallet]

Routes:

RoutePurpose
/Home
/tokenLive token facts (address, supply, pool, fee, liquidity, team lock)
/tokenomicsAllocation and vesting explained and visualized
/launchFair launch: state machine, live auction, bid, claim
/auditRequest a basic scan or deep audit; job tracking
/audit/[jobId]Job detail and result
/registryBrowse all committed reports across chains
/registry/[chain]/[asset]Asset report history
/transparencyThe Ledger
/how-it-worksSequence explanation of the protocol
/contractsEvery deployed contract explained
/docsRendered protocol documentation (from /docs/*.md)

"Tokenomics", "How it works" and "Contracts" are reachable from the footer, the Token page, and in-page links; the primary nav stays at seven.

Wallet is only requested on /launch (bid, claim) and /audit (approve, request). Every other page never calls eth_requestAccounts.

Footer: contract addresses (mono, copy), chain (Robinhood Chain 4663), explorer, GitHub, docs, "Built for kay9.io — the contracts survive without it."


6. Pages

6.1 Home

The hero opens on the most characteristic thing in KAY9's world: the dog inside the ring, and the ring reporting live chain status.

┌─────────────────────────────────────────────┬───────────────────────────┐
│ KAY9                                        │       ╭──────────╮        │
│ The On-Chain Watchdog            (display-1)│     ╭─┤  mascot  ├─╮      │
│                                             │   block ● 56,063,858      │
│ Fixed supply. Fair launch. Permanent        │   token ● deployed        │
│ liquidity. 0 % token tax. An audit protocol │   pool  ● 1 % fee, live   │
│ whose verdicts live on Robinhood Chain.     │   audit ● 2-of-3 online   │
│                                             │       ╰──────────╯        │
│ [Request a deep audit]  Read the contracts  │                           │
└─────────────────────────────────────────────┴───────────────────────────┘

Below the hero, in order (each a full-width section separated by 96 px, left-aligned, claim/evidence split):

  1. What is proven — six short claims with the enforcing contract named under each: fixed supply, fair launch, permanent liquidity, 0 % token tax, official pool fee 1 %, team locked on-chain. Not cards: a two-column list with hairlines between rows.
  2. The audit protocol — the sequence (request → detect → analyze → sign → commit) as a horizontal numbered rail with the on-chain steps drawn in brass and the off-chain step drawn in --ink-3 outline, so the trust boundary is visible in the diagram itself.
  3. Live protocol activity — a compact ledger of the last 8 events (audit requested, result committed, team release, fee collected) read from chain with block links. Empty state: "No protocol events yet on this chain."
  4. Tokenomics preview — one stacked bar (45.5 / 45.5 / 9) with direct labels and a link to /tokenomics.
  5. Cross-chain vision — a small tree diagram (KAY9 core → EVM adapter → Robinhood, BNB, later chains; → Solana adapter). Chains not yet live are shown in outline with "planned" chips. No fake logos of partners.

6.2 Token

Right column: the mascot small (120 px) above a ledger of live values. Left column: explanation.

Ledger rows (all read from chain; each has an explorer proof link): Network, Token address, Symbol, Decimals, Total supply, Circulating (total − vesting-locked − launch-vault-held), Official pool (key, fee 1 %, tick spacing 200, hook), Pool liquidity (ETH and KAY9 sides), Liquidity lock (FeeSplitter address, "positions irrecoverable"), Team allocation, Vesting status (released / locked / next unlock), Owner/Timelock addresses.

Pre-deployment state: rows show "Not deployed yet" in --ink-3 with the predicted address when available and a chip "Testnet" or "Not launched".

6.3 Tokenomics

This page explains everything, with the numbers held constant across three visualizations that answer three questions.

  1. Where does supply go? A single horizontal stacked bar, 1,000,000,000 KAY9, three segments with 2 px gaps and direct labels: Public fair auction 455,000,000 (45.5 %), Permanent liquidity reserve 455,000,000 (45.5 %), Team 90,000,000 (9 %). Below, in prose: "The 91 % launch allocation is split evenly: half is sold in the auction, half is paired with the auction's ETH as permanent liquidity."
  2. When can the team sell? A step chart (time on x, cumulative unlocked on y, 0 → 9 %) with three steps annotated with exact UTC timestamps read from KAY9TeamVesting: TGE 1 %, +6 calendar months 5 %, +12 calendar months 9 %. Below it, three rings (one per tranche) showing time remaining. The step chart is a single brass line on a recessive grid; the current time is a vertical --ink-3 rule.
  3. Where does the auction ETH go? A Sankey-like flow drawn as three horizontal bands: auction ETH → v4 pool (100 %), 455 M reserve KAY9 → v4 pool, LP NFT → FeeSplitter (locked). Fee split box beside it showing the actual deployed arrangement (native ETH fees 40 % creator / 60 % compounding; token fees 100 % compounding).

Every number is followed by "enforced by Contract.function" in small --ink-2. Nothing on this page is a promise.

6.4 Fair launch

A state machine page. The header shows the current phase as a numbered rail:

1 Not launched → 2 Auction live → 3 Clearing / migration → 4 Trading on Uniswap v4

Phase panels:

  • Not launched: the parameters that will be used (floor FDV, graduation FDV, duration 4 h, tick spacing, LP allocation 100 %) read from the launch vault's pending configuration, with implied prices computed client-side and labelled "computed from on-chain parameters and the Chainlink ETH/USD feed". "Launch requires the owner's signature" is stated plainly.
  • Auction live: left, the clearing price chart (price vs time, brass line, checkpoints as 8 px dots, floor as a hairline, graduation threshold as a labelled --ink-3 rule). Right, the bid panel: budget input in ETH, max price (as FDV in USD with the raw Q96 shown in mono beneath), "Your bid clears if the final price ≤ your max". Below, "Your bids" ledger with exit/claim actions. A large ring shows time remaining. A second ring shows "raised / required to graduate". Link out: "Also visible on the Uniswap app auctions tab".
  • Clearing / migration: shows the final clearing price, tokens sold, ETH raised, and the migrate call anyone can send ("Anyone can trigger migration after block N").
  • Trading: pool key, position IDs held by the FeeSplitter, "Liquidity principal cannot be withdrawn by anyone", fee accounting, "Unsold KAY9: N — placed as permanent single-sided liquidity" (or the real outcome).

Fairness statement block (static, on every phase): no presale, no whitelist, no team pre-buy, no validation hook (validationHook = 0x0), team holds only the declared 9 %.

6.5 Audit

Two-step form on the left, the price and status evidence on the right.

┌───────────────────────────────┬──────────────────────────────┐
│ 1 Choose the target           │ Deep audit price             │
│   [Robinhood ▾] [0x… / mint]  │   $10.00 target              │
│ 2 Choose the depth            │   824.37 KAY9 now            │
│   ○ Basic scan   free         │   KAY9/ETH TWAP 30 m  ● ok   │
│   ● Deep audit   $10 in KAY9  │   ETH/USD Chainlink  ● fresh │
│                                │   Pool liquidity     ● ok    │
│ [Connect wallet to request]   │   Operators online   2 of 3  │
└───────────────────────────────┴──────────────────────────────┘
  • Basic scan runs immediately in the browser/against the public engine endpoint and renders a result card labelled "Basic scan — not committed on-chain".
  • Deep audit: Connect → Approve KAY9 (or sign permit) → Request. After the transaction confirms, the page routes to /audit/[jobId].
  • If pricing is unavailable (stale oracle, thin liquidity, no observations), the price panel shows the exact reason in --warn and the request button is disabled with "Deep audits are paused until pricing is reliable". Nothing falls back to spot.

6.6 Audit job

Top: the score ring (or a rotating ring while pending) with the band label. Left: job facts ledger (job ID, target, requester, payment in KAY9, transaction, requested at block/time, status, operators who signed, engine version, report hash, report pointer). Right: risk breakdown as seven horizontal bars (contract, liquidity, holder concentration, linked wallets, creator history, trading behaviour, bots/snipers), single-hue sequential fill mapped to the band, direct-labelled with the number, each with a one-line "why" beneath drawn from the flags bitmask. Flags render as chips with the flag name; hovering shows the definition.

Status timeline (numbered, vertical): Requested → Detected by operators → Analysis complete → Signed (n of m) → Committed on-chain. Each step shows its evidence (block, tx, signature count) or "waiting" in --ink-3.

Report integrity: "Report hash on-chain: 0x…; pointer: ipfs://…; hash of fetched report: matches ✓ / does not match ✗". If the pointer cannot be fetched, the on-chain fields still render.

6.7 Registry

A filterable ledger (chain, band, date). Columns: Asset (chain chip + address/mint), Latest score ring (48 px), Band, Reports (count), Last committed (block/time), Engine. Clicking a row opens /registry/[chain]/[asset] which lists the full report history newest first without overwriting older ones, plus a small sparkline of score over time when ≥ 2 reports exist. No wallet needed.

6.8 Transparency

The Ledger, exceptionally clear. Intro line: "Every row is read from Robinhood Chain when you open this page." Then rows:

ClaimValueEnforced byProof
Fixed supplyYes — 1,000,000,000KAY9Token has no mint functionsource
Additional mintingNono mint selector in bytecodesource
Buy / sell / transfer tax0 % / 0 % / 0 %standard OZ ERC-20 transfersource
Blacklist / freeze / pauseNoabsentsource
Proxy upgradesNonon-upgradeable, no admincode hash
Hidden transfer restrictionsNotransfer is unmodifiedsource
Team allocation9 %KAY9TeamVesting.TOTAL_ALLOCATIONcall
Team lockOn-chainthree immutable unlock timestampscall
LP withdrawal by teamImpossibleLP NFTs held by FeeSplitter, no withdrawal pathposition IDs
Official LP fee1 %pool key fee = 10000pool
Audit price target$10KAY9Pricing.deepAuditUsdTarget (48 h timelock to change)call
Fee split for audits70 / 20 / 10KAY9AuditHub constantscall

Under the ledger: every contract address with verification status and a "Compare deployed bytecode hash" line. A final block: "What the owner can and cannot do" as two plain lists.

6.9 How it works and Contracts

How it works: the five-step audit sequence and the four-phase launch sequence, each as a numbered vertical rail with a diagram (SVG, brass strokes on --surface) and a paragraph. Contracts: one section per contract with the same six fields every time: address, network, verification, purpose, permissions (what the owner can change, with the timelock delay), upgradeable (always "No" for the token). Cross-links to source in the repository.

6.10 Docs

Renders /docs/*.md with the site type system, a left index, and anchors. Nothing else.


7. Components

Minimal set; each has loading, empty, error, and stale states.

  • Ring (value 0–100 or fraction, size, label, band). Loading: track only with a slow rotating 60° brass arc. Error: track in --warn with a triangle in the middle.
  • LiveValue (label, value, source, updatedAt). Shows the live dot state. Skeleton: a --surface-2 block the width of the expected value, no shimmer animation (reduced motion friendly, and shimmer is a template tell).
  • Ledger (rows of claim / value / enforced by / proof). Responsive: below 720 px it becomes a definition list with the claim as heading.
  • AddressBlock (address, chain, verified). Copy on click, explorer link, verified check.
  • Chip (status | chain | tier | demo).
  • TxButton (idle → wallet → pending → confirmed → error). Contains the whole state machine so pages do not re-implement it.
  • PhaseRail (steps, current). Horizontal on desktop, vertical on mobile.
  • AllocationBar, VestingStepChart, RiskBars, PriceChart (auction), Sparkline. All charts: single y-axis, recessive --line grid, tabular numbers, direct labels, hover tooltip with crosshair, a table toggle for accessibility, texture fill option for forced-colors.
  • EmptyState (title, one line, one action). Never an illustration.
  • WalletMenu (address, chain check, disconnect). If the wallet is on another chain the primary button becomes "Switch to Robinhood Chain".

8. Responsive rules

  • Breakpoints: 480, 720, 1024, 1280. The claim/evidence split appears at ≥ 1024. Between 720 and 1024 evidence sits under the claim at full width. Below 720 tables become definition lists, rails go vertical, rings scale to 160 px, and the hero mascot sits above the title at 220 px.
  • Touch targets ≥ 44 px. Charts get a 12 px hit slop around marks.
  • Horizontal overflow is only allowed inside overflow-x: auto wrappers (addresses, calldata, wide tables). The body never scrolls sideways.

9. Accessibility and performance floor

  • Color is never the only channel: bands have icons and words; series have labels and a legend; live status has words.
  • Text contrast ≥ 4.5:1 for body (--ink on --surface is 15.6:1; --ink-2 on --surface is 8.9:1), ≥ 3:1 for --ink-3 micro text on --surface-2.
  • Visible focus: 2 px --brass outline with 2 px offset on every interactive element.
  • Reduced motion respected globally; rings and counters render final values.
  • Fonts: preloaded, font-display: swap, subset to Latin. The mascot is served as a 1024 px PNG with priority on the home hero only; elsewhere 256 px or smaller. LCP target < 2.0 s on a mid-range phone, CLS 0 (all live values reserve width).
  • Semantic landmarks, one h1 per page, tables with <th scope>, charts with an accessible table alternative.

10. Metadata and PWA

  • Canonical https://kay9.io. Titles: "KAY9 — The On-Chain Watchdog" (home), "Page · KAY9" elsewhere.
  • OpenGraph/Twitter: og-image.jpg (same 1200×630 card as og-image.png, 74% smaller), summary_large_image.
  • manifest.webmanifest: name KAY9, short name KAY9, theme #000000, background #000000, icons 192/512 plus maskable.
  • JSON-LD Organization and WebSite on the home page; SoftwareSourceCode on the contracts page; HowTo on how-it-works; WebApplication on audit.
  • llms.txt at the root: a plain-text summary for AI assistants, and robots.ts names the major ones (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, …) explicitly rather than leaving their access to the wildcard rule.

11. Self-review against the generic defaults

  • Warm cream + serif + terracotta: not used. The site is black/brass because the asset is black/brass.
  • Near-black + single acid accent: the accent is brass from the logo, the ground is true black under the hero and a warm charcoal elsewhere; no tinted #111. The accent is not acid and it is tied to the ring motif rather than sprinkled.
  • Broadsheet hairlines: hairlines exist only in the Ledger, where the content is a ledger. Sections are separated by space.
  • SaaS card kit: static content has no cards, no radius, no shadows. Only interactive things get a radius.
  • Template chrome: no uppercase eyebrows, no middle-dot meta strings, no arrows on links, no mono for labels, no scroll-triggered fades. One orchestrated load moment on the home page.

The one bold thing is the ring: the logo's ring becomes the scanning ring, the score ring, the timelock ring, and the transaction ring. Everything else stays quiet.


Part two: dynamic experience, theming and languages

This part supersedes the single-theme, low-motion decisions above where they conflict. The brand foundation, the ring motif, the type roles and the copy voice in Part one all still hold.

12. Two themes, system-led on the first visit

The site follows the visitor's operating system preference on first visit and offers a toggle. Neither theme is an inversion of the other: each is designed, and each has its own validated chart palette.

The toggle is binary: light or dark. There is no visible "system" option, and no way back to following the system once a visitor has chosen — a choice is meant to stick. The resolution order, implemented identically in the pre-paint inline script and in ThemeToggle, is:

  1. An explicit choice in localStorage['kay9-theme'] wins. The only values that mean anything are light and dark.
  2. With nothing stored, the operating system decides: prefers-color-scheme: dark gives dark, an unset or light system gives light. This is the absence of data-theme, which is what the CSS media query is written against.
  3. The toggle always writes an explicit value and sets data-theme on the root element.

Any other stored value — including the system this document used to specify — is removed from storage on read, and the theme then resolves from the operating system. The inline script runs before first paint, so the page never flashes the wrong theme.

12.1 Dark theme

The palette in section 2.1 is unchanged. It remains the identity theme: true black under the hero where the mascot sits, warm charcoal elsewhere, brass accent.

12.2 Light theme

Warm and papery rather than clinical white, so the brass accent still reads as brass and the mascot's black ground becomes a deliberate object on the page rather than a hole in it.

TokenLight valueRole
--ground#FFFFFFPage ground behind the hero
--surface#FBF7EFDefault section and card surface, warm paper
--surface-2#F2EADCRaised surfaces, table headers, inputs
--ink#1A1610Primary text
--ink-2#4A4234Secondary text
--ink-3#6E6656Muted text, timestamps
--line#E4DACAHairlines, table rules
--line-strong#C9BCA6Input borders, active rules
--brass#9A6B00Accent at text contrast on paper
--brass-ink#7A5400Brass text on --surface-2
--copper#8A4A1EDeep accent, unchanged
--brass-softrgba(154,107,0,0.10)Tint for chips and selected rows

The brass accent darkens in light mode because #F2B62A on paper fails text contrast. Large display type may still use #F2B62A over the dark hero panel, which is retained in light mode as an intentional dark island holding the mascot.

In light mode the mascot sits on a rounded dark panel (#0B0906) rather than floating, because the asset has a black ground. The panel is part of the composition, not a workaround.

12.3 Chart palettes, both validated

Dark, on surface #15120E, all six checks pass: #B98418, #5E8FD6, #C0602A, #8B7BE8. Status: #2FA377 good, #C77D1A warning, #D8434A critical.

Light, on surface #FBF7EF, all six checks pass: #8A5A00, #2C5FA8, #A03E12, #5B3FBF. Status: #1F7A55 good, #B07000 warning, #C2185B critical.

The light status palette uses crimson rather than red for critical, because red against the warning amber failed both the colour-blind separation check and the normal-vision floor. Every status still ships with an icon and a word, so colour is never the only channel.

13. Motion: a scrolled narrative

Part one budgeted one orchestrated moment per page. The home page now earns more: it is a scrolled argument, and motion is what paces it. The discipline is unchanged in substance; the budget is larger and spent deliberately.

Rules that do not bend:

  • prefers-reduced-motion: reduce disables every scroll-driven and decorative animation. Content appears at its final state, counters show final values, and sticky sequences become ordinary stacked sections. Nothing is only reachable through motion.
  • Motion never gates content. Every section is fully readable with JavaScript disabled and with animations off. Reveals start at full opacity in the no-JS case.
  • Nothing loops forever except the two live indicators: the brass status dot and the scanning ring, both slow and both meaning "this is live".
  • No parallax on text. No motion that moves the thing a person is trying to read.

Implementation is CSS-first: native scroll-driven animations (animation-timeline: view() and scroll()) where the browser supports them, with an IntersectionObserver fallback that sets a single data-revealed attribute. No animation library, because the whole site is a static export and every kilobyte is on the critical path.

13.1 The home sequence

  1. Hero. As Part one: the ring draws once, the radar sweeps once, four status ticks light as each chain read resolves. On scroll the ring scales down slightly and docks, becoming the scroll-progress indicator for the rest of the page.
  2. What is proven. Six claims. Each row's brass rule draws left to right as it enters, and the contract name fades in a beat later. The rule is the evidence appearing, not decoration.
  3. Supply. A sticky section: the allocation bar stays pinned while three captions advance, and the bar's three segments fill in sequence to their final widths. Numbers count up once, from zero to the real figure, and never re-run.
  4. Team unlock. The step chart draws its line as the section scrolls, the three rings fill to their positions, and the "today" marker slides along.
  5. Where the ETH goes. The three flow bands draw their arrows in order, then the fee split box counts up its percentages.
  6. The audit sequence. The five-step rail advances a brass token along the path as the section scrolls, with the off-chain step visibly outside the brass line, so the trust boundary is felt rather than annotated.
  7. Cross-chain. The tree draws its branches outward from KAY9 core; planned chains stay outlined and do not animate, which is itself the message.
  8. Close. A single call to action. No animation.

13.2 Timing

Entrances 320 ms, ease-out. Counters 900 ms. Sticky sequences advance over one viewport height each. Sequential reveals stagger by 60 ms, never more than four items, so a list never feels like it is being typed out.

14. Thirteen languages

CodeLanguageScriptDirection
enEnglishLatinltr
esSpanishLatinltr
deGermanLatinltr
frFrenchLatinltr
ptPortugueseLatinltr
trTurkishLatinltr
hiHindiDevanagariltr
thThaiThailtr
jaJapaneseJapaneseltr
zh-HansChinese, SimplifiedHanltr
zh-HantChinese, TraditionalHanltr
faFarsiArabicrtl
arArabicArabicrtl

14.1 What is translated, and what is not

Translated: navigation, headings, body copy, button labels, empty states, error messages, chart labels and axis titles, and every explanatory sentence.

Not translated, by decision: the transparency ledger's claim and enforcement columns, contract names, function signatures, addresses, hashes, numbers and units. These are the canonical record. A mistranslated sentence about a tax, a lock or a risk score is a false statement about money, and the protection is that these read identically in every language. Each localised page carries one line, itself translated, saying that English governs the contract statements and that the numbers come from the chain.

14.2 Routing

Every locale is prefixed: /en/token, /es/token, /fa/token. The site is a static export, so there is no server to negotiate a language; the host rewrites / to /en and the language switcher is an ordinary link. Each page emits hreflang alternates for all thirteen locales plus x-default pointing at English. The switcher preserves the current path.

14.3 Right to left

For fa and ar the root element sets dir="rtl". Layout uses logical properties throughout (margin-inline-start, padding-inline-end, border-inline-start) so mirroring is automatic. Specifically:

  • The claim and evidence columns swap sides. Evidence still sits on the reading-end side.
  • Directional icons and arrows mirror. The ring's sweep does not: a clock runs the same way in every language, and the ring is a scanner, not an arrow.
  • Numbers, addresses, hashes and code stay left to right inside an RTL paragraph, wrapped so the browser's bidirectional algorithm cannot reorder them. Mono text is always dir="ltr".
  • Charts mirror their axis placement but not their data direction: time still runs left to right, because a falling line must not read as rising.

14.4 Type per script

Bricolage Grotesque and IBM Plex cover Latin only. Each non-Latin locale loads one Noto family and nothing else, so a Spanish visitor never downloads a Han font:

ScriptDisplay and text
LatinBricolage Grotesque, IBM Plex Sans
DevanagariNoto Sans Devanagari
ThaiNoto Sans Thai
JapaneseNoto Sans JP
Han SimplifiedNoto Sans SC
Han TraditionalNoto Sans TC
ArabicNoto Kufi Arabic for display, Noto Naskh Arabic for body

Arabic uses Kufi for headings because Naskh at display sizes with heavy weight becomes hard to read, and Naskh for body because it is what long-form Arabic expects. IBM Plex Mono stays the mono face in every locale; hashes and addresses are Latin regardless.

The display type scale drops one step for Han, Japanese and Thai, whose glyphs carry more ink at the same point size, and line-height rises to 1.7 for Thai and Arabic body text to make room for marks and descenders.

15. What this does not become

The reference the owner supplied is a presale landing page. KAY9 borrows its energy, not its content or its claims. Specifically, the site never gains: a presale or countdown-to-buy, staking or governance promises the contracts do not implement, "guaranteed" or "rewards" language, radiating gradient cards as the primary layout device, or stock 3D renders standing in for real data. Every animated number on the KAY9 site is either a contract constant or a value read from the chain.


Part three: the access model, and being on the buyer's side

This part supersedes Part one and Part two wherever they conflict. The brand foundation, the tokens, the two themes, the thirteen languages and the motion budget are all unchanged. What changes is who the site is addressed to and what it is asking the visitor to do.

16. The audience changed, so the argument changed

Part one's home page argued this token is honestly constructed. That is still true and still worth saying, but it is the wrong opening for the actual visitor. The visitor is somebody who found a token an hour ago and wants to know what it can do to them. The token's own fairness is the second question, not the first.

So the order of the argument is now:

  1. The hero states what KAY9 does for the reader, not what KAY9 is.
  2. Who this is for, immediately, three cards, traders first and visually primary. The single most common reason a reader leaves is not knowing whether a product is aimed at them, and the answer here is specific enough to be worth one section.
  3. Then Part two's sequence continues unchanged: what is proven, supply, unlock, ETH flow, the audit sequence, cross-chain.
  4. What runs where, a table, immediately before the close. This is new and it earns its place by making "the website is a window" checkable instead of a slogan: every row names the contract or the machine that holds the thing. Its last block is the limitation, in --warn, admitting what a two-of-three quorum does not protect against.
  5. The close, unchanged.

Navigation reorders for the same reason. Audit is first, Access second, Registry third; the token's own pages follow. Seven items, as before.

17. Designing a lock so it does not look like a scam

This is the hardest design problem on the site. "Lock your tokens" is a sentence a fraud uses, usually next to a yield figure. KAY9 asks for exactly that action and offers no yield, so the design has to carry the difference rather than the copy alone.

Decisions, all of them deliberate:

  • The guarantee list sits beside the button, not further down the page. Six lines: three things that hold, shown in --ok with a tick, and three things that are absent, shown in --ink-2 with an em dash rather than a cross. An absent guarantee is not a warning, and a red cross beside "no APY" would read as a defect rather than a design.
  • The required amount is never a constant in the code. It is read from the vault on every render. When the oracle refuses to answer, the card says the amount is not quotable yet and explains that the price does not exist before the token trades. A placeholder number here would be the single most damaging thing the site could print.
  • The oracle's own checks are a panel, not an error. PricingChecks renders the four conditions the contract tests before it will quote, in the contract's own terms. A closed lock is a safety mechanism working, and it is presented that way.
  • No countdown, no scarcity, no total-value-locked hero number. The vault's totalLocked is available and is deliberately not the headline; it is a number that would only ever be used to create pressure.
  • The three tiers are one grid, and basic is one of the three. Basic has no amount, no period and no button, which is the point of it. Rendering it as an aside would imply the free tier is a teaser for the paid one.

18. Job states now include disagreement

Requested, Fulfilled, Disputed, Expired. The two new states are failures of the network, never of the requester, and the page says so and says the allowance was returned.

Disputed gets two sentences rather than one: what happened, and then why it is published rather than resolved. A reader's instinct on seeing "disputed" is that something is broken, and the second sentence has to arrive fast enough to correct that — averaging two contradictory findings would produce a number no auditor stands behind, and hiding the disagreement would be worse than either finding.

The attestation count replaces the signature count in the rail, because before a job settles the interesting number is how many auditors have taken a position, not how many agreed.

19. Risk over time

An audit is a snapshot and the registry keeps every snapshot, so the asset page now carries a real chart rather than a sparkline.

It is drawn as a step, not a line. A score did not drift between two audits; it was one number until a new report replaced it, and a sloping segment would claim knowledge of values nobody measured. Each measurement is a mark coloured by its risk band, so a token crossing from good to critical is visible before the axis is read. Time runs left to right in every locale, including the two right-to-left ones, because a falling line must never read as rising.

Beneath the latest score, one line, always: the latest report is a measurement of one past block and is not a statement about the token now. No surface may render a score without its date.

20. The badge

A badge is the one piece of KAY9 that appears on somebody else's page, so it is the one most likely to be used to mislead.

  • It reads KAY9Registry.latestSummary on render. It is never an image. A static "audited" picture keeps saying 88 after a later audit says 43, which is precisely the failure this product exists to catch.
  • It is delivered as an iframe, not a script. A script would run KAY9's code inside a publisher's page, which is more access than a badge needs and more trust than a publisher should have to extend.
  • Its wording is fixed by policy: a number, a band, and a date. KAY9 Audit Completed, KAY9 Technical Risk, KAY9 Monitored. There is no arrangement of the design system that produces the words "verified safe", and there is not going to be.
  • The badge page also documents the two contract reads, because the honest form of this feature is that a builder does not need the badge, or this site, at all.

21. What Part three does not change

The palettes, both validated. The type scale and the per-script families. The motion budget and the reduced-motion contract. The thirteen locales and the rule that contract statements, numbers and identifiers read identically in every one. The rule that every animated number is either a contract constant or a value read from the chain — which now includes every KAY9 amount on the access page, none of which is written into the code.