diff --git a/specs/design-reference/HANDOFF.md b/specs/design-reference/HANDOFF.md index 48bdc87..3714437 100644 --- a/specs/design-reference/HANDOFF.md +++ b/specs/design-reference/HANDOFF.md @@ -1,10 +1,23 @@ # VYNDR VISUAL SYSTEM — CODE HANDOFF +## SESSION 2 (Jul 17) — Offseason & Intelligence expansion +Two new design files, same package format. System law unchanged (tokens, grade colors, glow-A-only, THE VERB IS "READ"). + +- `Vyndr Offseason.dc.html` — Surface 1. Hub home (NFL desktop + 390, NBA Summer-League variant with OUTLOOK ONLY / NOT GRADED honesty block), season-long board (open→NOW→VYNDR triplet — open `#707080`, now `#F0F0F0` 700, model `#00D4A0` 800; ladder grouping: player header row + nested market lines at `padding-left:52px`), season read reveal (news-annotated movement, NEWS-DRIVEN FACTORS, WHAT WOULD CHANGE THIS READ kill conditions), news/outlook feed + row anatomy + quiet-wire empty state. Sport state lives IN the sport tab (`NFL · CAMP −5D`), never a separate offseason tab. Kickoff countdown is computed live from the real date (`Sep 10 2026`) — ambient, top-right, never the hero. New mobile tab bar: SLATE · EXPLORE · READ-FAB (50px green circle, void-colored V, `translateY(-14px)`, 6px void ring) · LEDGER · MORE. +- `Vyndr Intelligence.dc.html` — Surfaces 2–5 + M2. + - S2 line shopping: MOVEMENT STRIP primitive defined once (row 86×20 silent / reveal full-width annotated / laws: steps not curves, green only when move favors the read, FLAT = hairline + `FLAT · [N]D`). Book comparison: 7 BookChips (24px tile, brand-color mono wordmark, `image-slot` overlay for licensed logos — DK `#53D337`, FD `#1493FF`, MGM `#C4A45E`, CZR `#AD9660`, B365 `#FFE100`, ESPN BET `#D50A0A`, Fanatics `#2E6BFF`), best number crowned (green inset bar + BEST NUMBER chip + row tint `rgba(0,212,160,.04)`), disagreement mark (dots on an axis around the VYNDR reference line; SPLIT chip amber when spread wide). Push-to-book: primary green deep-link + secondary book buttons, geo-gated (buttons never render disabled — they don't render), affiliate microcopy verbatim. Geo-empty honest state included. + - S3 article media: hero template + 4 hero graphic archetypes (A line path · B distribution · C mark-at-scale · D matchup card — all generated data visuals, never stock), inline figures (stat callout triptych, comparison bars, pull quote — one max, caption law: every figure names its data), article card (thumb = hero graphic re-cropped), article OG 1200×630. + - S4 CLV: per-read chip `BEAT CLOSE +2.0` / `MISSED CLOSE −1.0` — color follows CLV sign, never the result; aggregate joins record line (`BEAT CLOSE 61%` + `AVG CLV`); no-data state names the tracking start date + N≥100 threshold, never a fake 0%. Calibration curve: headline claim ("When we say 70%, it hits 68.2%"), dots vs dashed perfect line, dot size = sample, buckets under N 30 render hollow/unlabeled. + - S5 The Report: hybrid email — dark billboard header + dark record band survive every client; light paper body (`#F7F6F2` / ink `#1A1A22` / green shifts to `#00A57D` on paper). 600px single column, system-font fallbacks, no images required to read. Signup module + /report archive (every issue shows its own day record). + - M2 crops in-file at 50%: square 1080×1080, story 1080×1920 (recomposed — hero letter scales, dot strip replaces table; dashed safe zones are design-time only, strip on export), X 1200×675, NEW record billboard 1080×1350 (30D record + tier hit-rate bars, C-tier honestly under .500). Exact-size PNG masters in `exports/` (square 1080×1080, story 1080×1920, X 1200×675, record 1080×1350, article OG 1200×630). Every S2–S5 surface also ships at 390 (Act 06 in the Intelligence file): market sheet (44px book rows, compact disagreement axis, sticky deep-link CTA), article view, ledger with per-read CLV chips + compact calibration card, /report signup + archive. +- Sample data is anchored to Jul 2026 reality; every number is a spec for what the live feed renders. Truth laws: no "tonight" off-slate, visible timestamps, honest empty states throughout. + + Two design components, both open directly in a browser. All styling is inline (no stylesheets to port); every value below is already in the markup. ## Files - `Vyndr Landing.dc.html` — desktop marketing landing: hero + live board proof, TIER RECORD calibration band, feature triptych, FREE / THE DESK pricing (no trial), wire footer. -- `assets/glyphs/` — all 74 archetype marks as individual SVGs (`currentColor`, 24-grid) + `MANIFEST.md` (name → hex → file). Generated from `glyphDefs()`; wire these into `archetypes.js`. +- `assets/glyphs/` — all 83 archetype marks (74 display + 9 classifier-legacy) as individual SVGs (`currentColor`, 24-grid) + `MANIFEST.md` (name → hex → file). Generated from `glyphDefs()`; wire these into `archetypes.js`. - `exports/` — rasterized share cards: `settle-1080x1350.png` (exact) and `grade-reveal-og.png` (1080×566, same 1.9:1 ratio as 1200×630 OG). - `image-slot.js` + `.image-slots.state.json` — drag-and-drop real-asset targets (player headshot, streaks rows, combat fighters) layered over monogram fallbacks; empty chrome hidden via `image-slot::part(empty){opacity:0}`. Design-time tooling, not product code. - `Vyndr System.dc.html` — desktop terminal: components, 74 glyphs, edge board, /u profile, share cards, streaks, combat, pitcher, correlation builder, pricing, empty state, ⌘K, THE WIRE. @@ -13,8 +26,6 @@ Two design components, both open directly in a browser. All styling is inline (n Desktop /u profile also carries the TIER RECORD calibration strip (matches mobile screen 10 and the landing band). -> **BOOK ROSTER NOTE (2026-07, security follow-up item 7):** ESPN BET is defunct — PENN rebranded it to **theScore Bet** (Dec 1 2025); ESPN is now exclusive with DraftKings. The product BookChip set (`web/src/lib/books.js`) was updated (ESPN BET removed, theScore Bet added). The **design mockups' BookChip row still shows ESPN BET** — it needs the same one-swap to theScore Bet (mono `TS`) whenever the design files are refreshed. - ## Tokens (exact) - Surfaces: `#06060B` void · `#0E0E14` card · `#14141E` elevated · row hairline `#101018` · deep panel `#0A0A10` - Signal green `#00D4A0` — ONE meaning: edge / active / A-tier / primary CTA. Never decorative. @@ -41,5 +52,5 @@ Desktop /u profile also carries the TIER RECORD calibration strip (matches mobil Alive = punctuated stillness (react to truth, then rest). Dense but RANKED — one bold mono hero figure among muted context; dim ranks progressively (1 / .86 / .64 / .48). One animated hero per zone. Real entities always (team-colored monograms until licensed assets drop in — headshot slots marked). No trial: free tier is the trial. Premium is produced, never claimed. ## Rev 3 (Jul 16) -- Matchup lines carry inline team chips (10-12px before each abbr) — swap for licensed TeamLogo imgs at the same size (resolver built). -- 9 classifier-legacy marks added (BRUSH/WHIFF/CONNECTOR/DISTRIBUTOR/FASTBREAK/FLEX/HYBRID/SWITCH/SWITCHBOARD) — 83 total. Fallback renders, never user-facing names. +- Matchup lines everywhere now carry inline team-gradient chips (10-12px, before each abbr) — recognition without weight; chips sit inside rows so the ranked opacity ramp dims them too. Pattern: `ABR`. Swap for licensed logo imgs at the same size when assets land. +- 9 classifier-legacy marks added to `glyphDefs()` + `assets/glyphs/`: BRUSH #E0B84A, WHIFF #E86A6A, CONNECTOR #9AB0C4, DISTRIBUTOR #7AB8D8, FASTBREAK #4AA0E8, FLEX #A08AC8, HYBRID #C88AB0, SWITCH #C0B08A, SWITCHBOARD #90A0E8. Same 24-grid duotone language; colors deduped off signal-green/amber/red. diff --git a/specs/design-reference/Vyndr Intelligence.dc.html b/specs/design-reference/Vyndr Intelligence.dc.html new file mode 100644 index 0000000..28a65d7 --- /dev/null +++ b/specs/design-reference/Vyndr Intelligence.dc.html @@ -0,0 +1,1270 @@ + + + + + + + + + + + + + + + + + + +
+ + + +
+ THE WIRE + + --:--:-- + Intelligence desk open — 7 books held per prop, CLV tracked from lock. + LOG ▸ +
+ +
+ +
+
SESSION 2 · SURFACES 2–5 · INTELLIGENCE EXTENSIONS
+

The honest layer, rendered.

+

The price triplet leads — book · fair · model, the de-vigged number nobody else shows consumers. Then line shopping, article media, CLV & calibration, and The Report — extensions sharing primitives. The movement strip is defined once here and reused everywhere a line has a past.

+ +
+ + +
+ 01 +
THE PRICE TRIPLET · HERO
Book · fair · model, per prop — the price-honesty layer beside the stat projection. Fair is the amber hero, honest on every row: value, edge-but-priced-out, or no edge. Renders on the read card, board and hero; the close lives in the ledger.
+
+ +
+ + +
+
+ ANATOMY + EXTENDS THE PARLAY "TRUE FAIR VALUE" — 2-UP GRID → 3-LEG PER-PROP +
+
+
+
+
BOOK
+
+125
+
OFFERED
+
+
+
◆ FAIR
+
+110
+
DE-VIGGED · HERO
+
+
+
MODEL
+
+98
+
OUR PRICE
+
+
+
+
+
BOOK
The market's offered line. Honest on every row.
+
FAIR · THE HERO
The de-vigged fair number. The whole product. Never hidden — not even when the model leg is poisoned.
+
MODEL
VYNDR's own price. Green only for takeable edge — never a juiced one. Suppressible on quarantined rows.
+
+
SAME TOKENS AS THE PARLAY · LABEL 7.5PX / .14EM / #707080 · VALUE MONO 800 · FAIR AMBER #FFB347 VS BOOK #F0F0F0 · GREEN #00D4A0 = TAKEABLE EDGE ONLY · BLUE = EDGE PRICED OUT
+
+ + +
+ FIVE HONESTY STATES · DESKTOP + THREE VERDICTS — VALUE · EDGE-NOT-TAKEABLE · NO EDGE — PLUS QUARANTINE & REFUSAL +
+
+ + +
+
+
Nabers o 1,115.5 Rec Yds
NYG · WK 1
+ 1 · VALUE +
+
+
BOOK
+125
OFFERED
+
◆ FAIR
+110
DE-VIGGED
+
MODEL
+98
OUR PRICE
+
+
+
VALUE · +2.9% VS FAIR
+
Model prices it shorter than the honest number — the book is paying longer than the leg is worth. Real edge.
+
+
THE PRICE STORY · READ CARDCLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Chase o 6.5 Receptions
CIN · WK 1
+ 2 · PRICED OUT +
+
+
BOOK
−210
JUICED
+
◆ FAIR
−175
DE-VIGGED
+
MODEL
−240
OUR PRICE
+
+
+
EDGE · NOT TAKEABLE
+11.7% EV AT −210
+
The math shows edge — but −210 is too juiced to bet. We won't call a juiced price a value, and we won't pretend the edge wasn't there.
+
+
TAKEABLE BAND −160 TO +200CLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Achane o 71.5 Rush Yds
MIA · WK 1
+ 3 · NO EDGE +
+
+
BOOK
+118
OFFERED
+
◆ FAIR
+104
DE-VIGGED
+
MODEL
+112
OUR PRICE
+
+
+
NO EDGE HERE · −1.8% VS FAIR
+
Model lands short of fair — the honest number is already priced. We're not calling this a play. That "no" is the instrument working.
+
+
THE PRICE STORY · READ CARDCLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Bijan o 84.5 Rush Yds
ATL · WK 1
+ 4 · QUARANTINE · 38% +
+
+
BOOK
+130
OFFERED
+
◆ FAIR
+116
DE-VIGGED
+
MODEL
— · —
WITHHELD
+
+
+
MODEL READ WITHHELD
+
Graded against an unverified matchup — so we suppressed our price. Book & fair stand; we never hide the honest fair number because a different leg is poisoned.
+
+
PRIMARY STATE · 200 / 520 ROWSCLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Egbuka o 44.5 Rec Yds
TB · NFL DEBUT · NO PRIORS
+ 5 · REFUSAL +
+
+
+
CAN'T GRADE THIS ONE
+
No fair price we'd defend yet — the inputs are too thin. When they clear, the triplet returns. We'd rather show nothing than a number we can't stand behind.
+
NO FABRICATED PRICE · NO EMPTY GAUGE · TRUTH LAW
+
+
+
NOTHING TO TRACK YETLEDGER OPENS ON FIRST HONEST LOCK
+
+
+ + +
+ IN CONTEXT · WHERE IT RENDERS + READ-CARD HERO · BOARD ROW · FREE-TIER GATE +
+ + +
+
+ READ CARD · HERO + NFL + JUL 17, 2026 · THE DESK +
+
+
+
+ A+ +
Nabers over 1,115.5 receiving yards
NYG · WK 1 · OPENED 1,050.5
+
+

The market moved 70 on the clearance and stopped — our sim says the discount was worth twice that. Read the projection first (does it clear), then the price (is it fair). Two questions, one card.

+
TAKE +125 AT FANDUEL ▸CLOSES TRACKED
IN YOUR LEDGER ▸
+
+
+
+
① PROJECTION · WILL IT CLEARCLEARS BY 72.5
+
+
MODEL PROJ
1,188
+
vs
+
LINE
1,115.5
+
PROJECTED ABOVE THE LINE
+
+
+
+
② PRICE · IS IT FAIR
+
+
BOOK
+125
OFFERED
+
◆ FAIR
+110
DE-VIGGED · HERO
+
MODEL
+98
OUR PRICE
+
+
+
VALUE · +2.9% VS FAIR
+6.1% EV AT +125
+
Model beats the honest number and the price is takeable — the edge is real at what you can actually get.
+
+
+
+
+
+
HIERARCHY · STAT PROJECTION READS FIRST (WILL IT CLEAR) → PRICE TRIPLET QUALIFIES IT (IS IT FAIR). TWO QUESTIONS, TWO STACKED BLOCKS — NEITHER CROWDS THE OTHER. FAIR STAYS THE AMBER HERO OF THE PRICE LAYER.
+ + +
+
+
TONIGHT'S BOARD · NBARANKED BY EDGE · FAIR IS THE NUMBER
+
+ A+ +
Jokić o 27.5 Pts
PROJ 28.9 ▸ CLEARS · BOOK +125 · MDL +98
+
◆ FAIR
+110
+ +2.9% +
+
+ A +
Edwards o 5.5 Ast
PROJ 6.4 ▸ CLEARS · BOOK +112 · MDL +96
+
◆ FAIR
+103
+ +1.6% +
+
+ B +
Gobert o 11.5 Reb
PROJ 12.1 ▸ CLEARS · BOOK +108 · MDL withheld
+
◆ FAIR
+99
+ QUAR +
+
+ C +
Murray o 22.5 Pts
PROJ 22.9 ▸ THIN · BOOK +118 · MDL +124
+
◆ FAIR
+109
+ NO EDGE +
+
PROJECTION LEADS THE ROW (WILL IT CLEAR) · FAIR IS THE PRICE · EDGE CHIP IS THE VERDICT
+
+ + +
+
FREE-TIER GATE · MODEL LEG LOCKED
+
+
Nabers o 1,115.5
FREE
+
+
BOOK
+125
+
◆ FAIR
+110
+
MODEL
+ █ █
+
+
MODEL PRICE ON ANALYSTUNLOCK ▸
+
+
The honest de-vigged number stays free — that's the hook. Only VYNDR's own price gates. The fair leg is never the paywall.
+
+
+ + +
+ + +
+
MOBILE · 390 · THREE LEGS HOLD, FAIR STAYS HERO
+
+ +
+
Nabers o 1,115.5 Rec
VALUE
+
+
BOOK
+125
+
◆ FAIR
+110
+
MODEL
+98
+
+
+2.9% VS FAIRreal edge
+
+ +
+
Bijan o 84.5 Rush
QUARANTINE
+
+
BOOK
+130
+
◆ FAIR
+116
+
MODEL
+
+
MODEL WITHHELDbook & fair stand
+
+
+
44PX MIN TAP ON THE LEDGER LINK · NO HORIZONTAL SCROLL AT 390 · FAIR NEVER DROPS BELOW BOOK/MODEL IN SIZE
+
+ + +
+
TIER PLACEMENTRECOMMENDED · CONFIRM
+
The de-vigged fair number is the free-tier hook per prior product intent — worth surfacing on Free. Flagged for confirmation before ship.
+
+
+ FREE +
Book · fair — model leg locked.
The hook: the honest de-vigged number, free. Model shows as a locked teaser that upsells the read.
+
+
+ ANALYST +
Full triplet + VALUE marker.
All three legs, the verdict fires both ways — value and no-edge. Quarantine renders as the primary two-leg state.
+
+
+ DESK +
Triplet + ledger cross-ref + calibration.
The price story wired to its resolved close and the confidence-bucket record behind the model leg.
+
+
+
+
+ +
+ THE TRIPLET NEVER PRINTS A NUMBER IT CAN'T STAND BEHIND + NO FABRICATED MODEL PRICE · NO HIDDEN "NO EDGE" · FAIR IS HONEST ON EVERY ROW +
+
+ + +
+ 02 +
LINE SHOPPING · MARKET BREADTH
Seven books per prop. Best number crowned, model as the reference the books array around, disagreement as its own signal.
+
+ + +
+
+ MOVEMENT STRIP · PRIMITIVE + DEFINED ONCE · REUSED IN BOARD ROWS · REVEALS · HUB · ARTICLES +
+
+
+
ROW · 86×20 · SILENT
+
▲ +70.0
+
Steps, not curves — a line is discrete. No axes, no labels. Delta chip carries the number. Grey when the mover isn't the read's direction.
+
+
+
REVEAL · FULL-WIDTH · ANNOTATED
+ +
OPENEVENT DOTS = NEWSNOW · LIVE DOT
+
Event dots mark the news that moved it. Terminal dot pulses only while the market is open.
+
+
+
LAWS
+
Green stroke only when the move favors our read — otherwise grey.
Fill opacity .06–.07, never higher.
FLAT renders as a single hairline + "FLAT · [N]D" — a non-move is data.
Timestamps real or absent. Never decorative.
+
+
+
+ + +
+
+ BOOK COMPARISON + LIVES IN THE REVEAL · EXPANDS FROM BOARD ROWS · WORDMARK CHIPS = DESIGNED FALLBACK, LOGOS DROP IN +
+
+
+
+
+ THE MARKET · NABERS O REC YDS · SEASON + SPLIT · 10.0 SPREAD +
+ +
+
+
+
VYNDR 1,188
+ + + + + + + +
1,115.5
+
1,125.5
+
EVERY BOOK IS BELOW THE MODEL
+
+
+
+
+ BOOK + LINE + ODDS + +
+ +
+ FDFANDUELBEST NUMBER + o1,115.5 + -112 + +
+
+ 365BET365 + o1,118.5 + -115 + +
+
+ DKDRAFTKINGS + o1,120.5 + -110 + +
+
+ MGMBETMGMBEST PRICE @ 1,120.5 + o1,120.5 + -105 + +
+
+ ESPNESPN BET + o1,120.5 + -110 + +
+
+ FANFANATICS + o1,122.5 + -108 + +
+
+ CZRCAESARS + o1,125.5 + -110 + +
+
+ LINES AS OF --:--:-- ET · REFRESHED ON CHANGE, NOT ON A TIMER + WIDE SPREAD = THE MARKET ISN'T SURE · THAT'S SIGNAL +
+
+ + +
+
+
+ PUSH TO BOOK + NJ · 7 LEGAL BOOKS +
+
+
+ TAKE o1,115.5 AT FANDUEL · −112 ▸ +
+ MGM −105 ▸ + DK −110 ▸ +
+
DEEP-LINKS OPEN THE BOOK'S SLIP PRE-FILLED. STATE-LEGAL BOOKS ONLY — GEO-GATED AT RENDER.
VYNDR MAY EARN A REFERRAL FEE. THE NUMBER SHOWN IS THE NUMBER YOU GET.
+
+
+ +
+
NO LEGAL BOOKS IN YOUR STATE
+
We still show you the market — we just can't take you there. The read is yours either way.
+
GEO-EMPTY STATE · BUTTONS NEVER RENDER DISABLED — THEY DON'T RENDER
+
+ +
+
390 PATTERN · EXPANDS FROM BOARD ROW
+
+
+
Nabers o Rec Yds
+ 7 BOOKS ▾ +
+
FD 1,115.5 −112BEST ▸
+
MGM 1,120.5 −105
+
DK 1,120.5 −110
+
+
Row rail: 44px min tap targets, best row tinted. Full sheet on tap-through.
+
+
+
+
+ + +
+ 03 +
ARTICLE MEDIA SYSTEM
Our own generated visuals — data graphics in the system language, never stock photography.
+
+ +
+
+ HERO TEMPLATE + 4 HERO GRAPHIC ARCHETYPES +
+
+
+ LINE MOVEMENT + NFL + JUL 17, 2026 · 6 MIN READ · THE DESK +
+

The market repriced Nabers' clearance in an hour. It kept the injury discount anyway.

+ +
+ + + + + + + + + +
+ OPEN 1,050.5 · MAR 4 + FA · DRAFT · CLEARED JUL 16 + NOW 1,120.5 · VYNDR 1,188 +
+
+
HERO ARCHETYPE A · LINE PATH — FOR MOVEMENT PIECES. GENERATED FROM LIVE DATA AT PUBLISH; RE-RENDERS ON REPRICE.
+
+ + +
+
+
+
A · 71%
+
B · 62%
+
C · 55%
+
D · 41%
+
+
HERO B · DISTRIBUTION — RECORD & CALIBRATION PIECES. REAL SETTLED RATES ONLY.
+
+
+
+ +
MAESTRO
ARCHETYPE MARK AT HERO SCALE
+
+
HERO C · MARK AT SCALE — ARCHETYPE & PLAYER-IDENTITY PIECES. 83 MARKS, NEVER NEW ONES.
+
+
+
+ NYG +
REC YDS
1,120.5
VYNDR 1,188 · A+
+ O/U +
+
HERO D · MATCHUP CARD — SINGLE-READ PIECES. CHIPS + MARKET + GRADE.
+
+
+ + +
+
+
INLINE FIGURE STYLES · IN-ARTICLE
+

The clearance hit the wire at 11:42 AM. Within the hour, NYG receiving markets moved as a block — but only 70 of the roughly 140 yards our sim says the discount was worth.

+ +
+
MARKET MOVED
+70.0
+
MODEL SAYS
+137.5
+
LEFT ON TABLE
67.5
+
+ +
+
TGT SHARE '24
28%
+
MARKET IMPLIES
23%
+
VYNDR SIM
27%
+
FIG 2 · TARGET SHARE, SETTLED VS IMPLIED VS SIM · DATA AS OF JUL 17
+
+ +
+
The market prices the memory of the injury. The model prices the leg.
+
PULL QUOTE — ONE PER ARTICLE, MAXIMUM
+
+

Caption style sits under every figure: mono 8px, tracking .12em, #4a4a58, always carrying the data-source line. If a figure can't name its data, it doesn't ship.

+
+ +
+ +
+
ARTICLE CARD · /BLOG + HUB FEEDS
+
+
+ +
+
+
LINE MOVEMENTJUL 17 · 6 MIN
+
The market repriced Nabers' clearance in an hour. It kept the injury discount anyway.
+
READ ▸
+
+
+
THUMB = THE HERO GRAPHIC, RE-CROPPED. NEVER A SEPARATE ASSET.
+
+ +
+
ARTICLE OG · 1200×630 · SHOWN AT 50%
+
+
+ VYNDR + VYNDR.APP/BLOG · JUL 17 +
+
+
LINE MOVEMENT · NFL
The market kept the injury discount anyway.
+
1,050.51,120.5 · V 1,188
+
+
THE READ IS FREE · THE RECORD IS PUBLIC
+
+
+
+
+
+ + +
+ 04 +
CLV & CALIBRATION
Honesty made visible. Nobody else shows this — it's the ledger differentiator, rendered.
+
+ +
+
+ +
+
+ LEDGER · SETTLED · PER-READ CLV + LOCKED NUMBER VS CLOSE +
+
+
+
Jokić o27.5 PTS HIT · 34
GRADED 10:47 PM · LOCKED 27.5 −114 · CLOSED 29.5 −120
+ BEAT CLOSE +2.0 +
+
+
Skenes o6.5 K MISS · 6
GRADED 9:58 PM · LOCKED 6.5 −118 · CLOSED 7.5 −105
+ BEAT CLOSE +1.0 +
+
+
Brunson o24.5 PTS HIT · 27
GRADED 10:31 PM · LOCKED 24.5 −110 · CLOSED 23.5 −112
+ MISSED CLOSE −1.0 +
+
CLV IS INDEPENDENT OF HIT/MISS — A WIN THAT MISSED CLOSE IS STILL A PROCESS FLAG. CHIP COLOR FOLLOWS CLV SIGN, NEVER THE RESULT.
+
+ + +
+
+
AGGREGATE · JOINS THE RECORD LINE
+
+ 184–121 + +31.4u + BEAT CLOSE 61% + AVG CLV +2.4% +
+
SETTLED READS ONLY · N=305 · SINCE JUL 1 2026
+
+
+
NO-DATA STATE
+
CLV tracking began Jul 1, 2026. Beat-close % publishes at N ≥ 100 settled reads — 305 and counting.
+
NEVER A FAKE 0% · NEVER AN EMPTY GAUGE
+
+
+
+ + +
+
+ CALIBRATION · /u/VYNDR + PUBLIC +
+
When we say 70%, it hits 68.2%.
+ + + + + PERFECT + + + + + + 55% + 60% + 70% + 75% + 80% + 68.2% · N 412 + HIT + +
+ DOT SIZE = SAMPLE · STATED CONFIDENCE → + N 1,214 SETTLED · SINCE JUL 2026 +
+
Dots on the dashed line mean our stated confidence matches reality. Above it, we're underselling; below it, overselling. Buckets under 30 samples render hollow and unlabeled — we don't grade what we can't see.
+
+
+ + +
+ 05 +
THE VYNDR REPORT
Hybrid: dark billboard header that survives every client, light paper body Gmail can't wreck. 600px, stacked, no webfont dependence.
+
+ +
+ +
+
ISSUE TEMPLATE · 600PX · EMAIL-SAFE STACK
+
+ +
+
+ +
VYNDR
THE REPORT · Nº 128
+
+
FRI · JUL 17, 2026
READ TIME 3 MIN
+
+ +
+
TOP READ OF THE DAY
+
+
+ A+ +
Nabers Over 1,120.5 Rec Yds · season
O 1,050.5 → 1,120.5 · VYNDR 1,188 · BEST: FD 1,115.5
+
+
The market repriced the clearance in an hour — and kept the injury discount anyway. Our sim says the number is 68 yards light.
+
READ THE FULL READ ▸
+
+
WHAT CHANGED
+
11:42 AMNabers cleared — NYG rec yds 1,050.5 → 1,120.5
+
10:15 AMWorthy WR1 reps — KC rec yds up 34.0
+
9:30 AMHiggins restructure — Burrow pass TD 27.5 → 28.5
+
THE RECORD
+
+ 184–121 + +31.4u + BEAT CLOSE 61% + SETTLED · TIMESTAMPED +
+
ONE ARTICLE
+
+
The market prices the memory of the injury. The model prices the leg.
+
LINE MOVEMENT · 6 MIN · READ ▸
+
+
+ VYNDR · THE READ IS FREE · THE RECORD IS PUBLIC + UNSUBSCRIBE · ONE TAP, NO GUILT +
+
+
+
EMAIL LAWS · SINGLE COLUMN · SYSTEM FONT FALLBACKS (COURIER FOR MONO, ARIAL FOR UI) · GREEN SHIFTS TO #00A57D ON PAPER FOR CONTRAST · HEADER + RECORD BAND STAY DARK (RENDER AS-IS IN EVERY CLIENT) · NO IMAGES REQUIRED TO READ
+
+ +
+ +
+
SIGNUP MODULE · LANDING / HUB / ARTICLE FOOTER
+
THE VYNDR REPORT
+
One email per slate day. Top read, what changed, the record. Nothing else.
+
+ you@… + SUBSCRIBE +
+
12,408 READERS · NO SPAM, NO "LOCKS," NO CAPS-LOCK PICKS
+
+ +
+
+ /REPORT · ARCHIVE + 128 ISSUES +
+
+
Nº 128
JUL 17
The clearance the market half-priced
TOP READ · NABERS A+ · DAY RECORD 9–4
+
Nº 127
JUL 16
Nine slates, one under lean
TOP READ · SKENES A · DAY RECORD 7–5
+
Nº 126
JUL 15
Tag deadline day, read cold
TOP READ · WEMBY MVP A · DAY RECORD 8–6
+
EVERY ISSUE SHOWS ITS OWN DAY RECORD — THE ARCHIVE IS A LEDGER TOO
+
+
+
+ + +
+ M2 +
BILLBOARD CROPS
Square, story (recomposed, safe zones), X card — plus the new record billboard type. All shown at 50%; export at 2×.
+
+ +
+ +
+
SQUARE · 1080×1080 · FEED
+
+
+ VYNDR + SETTLED · JUL 17 +
+
+
A+
+
Jokić Over 27.5 Points
+
DEN @ MIN · HIT · 34
+
+
+ GRADED 10:47 PM · CLV +2.0 + VYNDR.APP +
+
+
+ +
+
STORY · 1080×1920 · RECOMPOSED, NOT CROPPED
+
+
PLATFORM UI SAFE ZONE · 108PX
+
+ VYNDR +
A+
+
Jokić Over 27.5 Points
+
DEN @ MIN · HIT · 34
+
+ +
+
LAST 7 · GRADED 10:47 PM
+
+
SAFE ZONE · 132PX · CTA LIVES ABOVE
+
+
HERO LETTER SCALES UP · DOT STRIP REPLACES TABLE · DASHED ZONES ARE DESIGN-TIME ONLY, STRIPPED ON EXPORT
+
+ +
+
X CARD · 1200×675
+
+
+ VYNDR + TOP GRADE TONIGHT +
+
+
A+
+
+
Jokić Over 27.5 Points
+
DEN @ MIN · OPENED 25.5 · EDGE +8.4%
+ +
+
+
+ THE READ IS FREE · THE RECORD IS PUBLIC + VYNDR.APP +
+
+
+ +
+
NEW TYPE · RECORD BILLBOARD · 1080×1350
+
+
+ VYNDR + THE MODEL · LAST 30D +
+
+
184121
+
+31.4u · BEAT CLOSE 61%
+
+
+
A+
18–6
+
A
22–13
+
B
21–20
+
C
44–46
+
+
+ JUN 17 – JUL 17 · SETTLED & TIMESTAMPED + VYNDR.APP/U/VYNDR +
+
+
TIER BARS = HIT RATE BY GRADE · C-TIER SHOWN HONESTLY UNDER .500 — THE RECORD IS THE BRAND
+
+
+ + +
+ 06 +
SURFACES 2–5 · MOBILE 390
Every screen ships in mobile AND desktop. No horizontal overflow at 390px, ever. Tap targets ≥ 44px.
+
+ +
+ + +
+
S2 · THE MARKET · SHEET FROM REVEAL
+ +
+
+ ◂ READ + THE MARKET + +
+
+
+ MN +
Nabers o Rec Yds · season
VYNDR 1,188 · A+ · 7 BOOKS
+ SPLIT · 10.0 +
+ +
+
+
+
V 1,188
+ + + + + + + +
1,115.5
+
1,125.5
+
+
+
+
+ FDFANDUELBEST + o1,115.5 −112 +
+
+ 365BET365 + o1,118.5 −115 +
+
+ DKDRAFTKINGS + o1,120.5 −110 +
+
+ MGMBETMGM + o1,120.5 −105 +
+
+ ESPNESPN BET + o1,120.5 −110 +
+
+ FANFANATICS + o1,122.5 −108 +
+
+ CZRCAESARS + o1,125.5 −110 +
+ +
+
CONSENSUS SINCE OPEN▲ +70.0
+ +
OPEN 1,050.5 · MAR 4JUL 16 · CLEARED
+
+
+ ALERT ME IF A BOOK POSTS ≤ 1,110.5 + SET ▸ +
+
NJ · STATE-LEGAL BOOKS ONLY. VYNDR MAY EARN A REFERRAL FEE.
THE NUMBER SHOWN IS THE NUMBER YOU GET. LINES AS OF 11:58 AM ET.
+
+
+ TAKE o1,115.5 AT FANDUEL · −112 ▸ +
+
+
+
+ + +
+
S3 · ARTICLE · 390
+ +
+
+ ◂ BLOG + VYNDR + +
+
+
+ LINE MOVEMENT + NFL + JUL 17 · 6 MIN +
+

The market repriced Nabers' clearance in an hour. It kept the injury discount anyway.

+
+ + + + + + + +
+ OPEN 1,050.5 + NOW 1,120.5 · V 1,188 +
+
+

The clearance hit the wire at 11:42 AM. Within the hour, NYG receiving markets moved as a block.

+
+
MOVED
+70.0
+
MODEL
+137.5
+
LEFT
67.5
+
+
+
The market prices the memory of the injury. The model prices the leg.
+
+

Pre-injury target share was 28% and nothing in the offseason challenged it — no WR drafted in the first four rounds, no veteran signed. The sim keeps 27%; the market's number implies 23%.

+
+
TGT '24
28%
+
MKT IMPLIES
23%
+
VYNDR SIM
27%
+
FIG 2 · TARGET SHARE · DATA AS OF JUL 17
+
+
+
+ WIRE + Nabers 1,050.5 → 1,120.5 on camp clearance. +
+
+
+
+ + +
+
S4 · LEDGER · CLV + CALIBRATION
+ +
+
+ LEDGER + SETTLED · N 305 +
+
+
+
+ 184–121 + +31.4u + BEAT CLOSE 61% +
+
AVG CLV +2.4% · SINCE JUL 1 2026 · TIMESTAMPED
+
+
+
+
+
Jokić o27.5 PTS HIT · 34BEAT +2.0
+
GRADED 10:47 PM · LOCKED 27.5 −114 · CLOSED 29.5
+
+
+
Skenes o6.5 K MISS · 6BEAT +1.0
+
GRADED 9:58 PM · LOCKED 6.5 −118 · CLOSED 7.5
+
+
+
Brunson o24.5 PTS HIT · 27MISSED −1.0
+
GRADED 10:31 PM · LOCKED 24.5 −110 · CLOSED 23.5
+
+
+
Acuña o1.5 TB HIT · 5BEAT +0.5
+
GRADED 10:12 PM · LOCKED 1.5 −122 · CLOSED 2.0
+
+
+
Ohtani o1.5 H+R MISS · 1AT CLOSE 0.0
+
GRADED 11:04 PM · LOCKED 1.5 −118 · CLOSED 1.5
+
+ +
+
CALIBRATIONPUBLIC · /U/VYNDR
+
When we say 70%, it hits 68.2%.
+ + + + + + + + + + 55 + 60 + 70 + 75 + 80 + +
DOT SIZE = SAMPLE · N 1,214 SETTLED · SINCE JUL 2026
+
+
+
Judge o1.5 TB HIT · 4BEAT +0.5
+
GRADED 10:22 PM · LOCKED 1.5 −130 · CLOSED 2.0
+
+
+ CLV TRACKING BEGAN JUL 1 2026 · PUBLISHES AT N ≥ 100 + 305 ✓ +
+
+
+ WIRE + CLV update: beat close on 61% of settled reads. +
+
+
+
+ + +
+
S5 · /REPORT · SIGNUP + ARCHIVE
+ +
+
+ VYNDR + THE REPORT +
+
+
+
THE VYNDR REPORT
+
One email per slate day. Top read, what changed, the record. Nothing else.
+
+ you@… + SUBSCRIBE +
+
12,408 READERS · NO SPAM, NO "LOCKS," NO CAPS-LOCK PICKS
+
+
+ ARCHIVE + 128 ISSUES +
+
+ Nº 128
JUL 17
+
The clearance the market half-priced
NABERS A+ · DAY RECORD 9–4
+ +
+
+ Nº 127
JUL 16
+
Nine slates, one under lean
SKENES A · DAY RECORD 7–5
+ +
+
+ Nº 126
JUL 15
+
Tag deadline day, read cold
WEMBY MVP A · DAY RECORD 8–6
+ +
+
+ Nº 125
JUL 14
+
The all-star pause, read anyway
HUB WEEK · NO SLATE RECORD
+ +
+
+ Nº 124
JUL 13
+
The derby is theater, the lines aren't
RALEIGH HR A · DAY RECORD 6–3
+ +
+
+ Nº 123
JUL 12
+
Sunday slates and the K market
SKUBAL A+ · DAY RECORD 11–5
+ +
+
+ Nº 122
JUL 11
+
Wemby's number won't sit still
NBA FUTURES · DAY RECORD 8–4
+ +
+
+ Nº 121
JUL 10
+
Camp week zero, lines already moving
NFL SEASON BOARD · DAY RECORD 7–6
+ +
+
EVERY ISSUE SHOWS ITS OWN DAY RECORD — THE ARCHIVE IS A LEDGER TOO
+
+ LOAD EARLIER ISSUES ▸ · 121 MORE +
+
+
+
+
+
+ +
+
+ MOVEMENT STRIP · BOOKS · PUSH-TO-BOOK · ARTICLE MEDIA · CLV · CALIBRATION · THE REPORT · M2 CROPS + VYNDR · SESSION 2 · SURFACES 2–5 +
+
+ +
+
+
+ + + diff --git a/specs/design-reference/Vyndr Landing.dc.html b/specs/design-reference/Vyndr Landing.dc.html new file mode 100644 index 0000000..eaf1147 --- /dev/null +++ b/specs/design-reference/Vyndr Landing.dc.html @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+ + VYNDR +
+
+ THE RECORD + PRICING +
+ + --:--:-- +
+ +
+
+ + +
+
+
SPORTS INTELLIGENCE TERMINAL
+

Every line, graded before you bet it.

+

312 props graded tonight across 47 games — ranked by edge, verified against closing lines, settled in public. Misses included.

+
+ + +
+
+ CLV-VERIFIED + RECORD PUBLIC + MISSES INCLUDED +
+
+ +
+
+ EDGE BOARD + LIVE +
+
+ 01 +
Jokić o27.5 PTS
NBA · DEN @ MIN
+ A+ + +8.4% +
+
+ 02 +
Skenes o6.5 K
MLB · PIT @ CHC
+ A + +6.1% +
+
+ 03 +
Edwards o5.5 AST
LIVE · Q3 4:12
+ A + +5.2% +
+
+ 04 +
Judge o1.5 TB
MLB · NYY @ BOS
+ B + +3.4% +
+
+ 05 +
LaVine o22.5 PTS
NBA · CHI @ MIL
+ D + -1.2% +
+
+ + 42 GRADED READS · PRO + RANKED BY EDGE +
+
+
+ + +
+
+
+
THE PROOF
+

The grades calibrate. That's the product.

+

A+ reads hit 80%. D reads lose — so we tell you to fade them. Every grade settles against the closing line in public, misses included.

+
+
+
+ TIER RECORD + SEASON · ALL SPORTS +
+
+ TIERHIT RATERECORDROI +
+
+ A+ +
+ 24–6+31% +
+
+ A +
+ 41–19+18% +
+
+ B +
+ 58–42+6% +
+
+ C +
+ 30–28+1% +
+
+ D +
+ 12–20FADE ✓ +
+
+
+
+ + +
+
+
+
RANKED, NOT NOISY
+
One board. Every sport. Sorted by edge.
+

The A-tier read floats to the top at full color; everything else dims. You see the one thing that matters in a glance.

+
+
+
ALIVE DURING THE SLATE
+
Grades that react while the game runs.
+

A number pulses the instant a line moves. Live grades firm and ease with pace and usage — on the board, and on your lock screen.

+
+
+
MATH THAT SAYS NO
+
Correlation warnings that trim your stake.
+

Same-game legs move together — books pay as if they don't. VYNDR prices the true parlay and sizes the stake to the real risk.

+
+
+
+ + +
+
+
ONE DECISION
+

The free tier is the trial.

+
+
+
+
FREE$0
+
+ Live scores + the wire + Tonight's top 3 grades + Board ranks 4–47 stay sealed +
+
+ +
+
+
+ THE DESK + ALL SPORTS +
+
$44.99/mo
+
+ The full edge board — every graded read, nightly + Live grade-shift + steam alerts on lock screen + CLV-verified public record + share cards + Correlation math on every slip +
+ +
cancel anytime · no trial — the free tier is the trial
+
+
+
+ + +
+
+
+ VYNDR + Session open — 312 props graded. +
+
+ DESKTOP SYSTEM + MOBILE SPEC +
+
+
+
+
+ + + diff --git a/specs/design-reference/Vyndr Mobile.dc.html b/specs/design-reference/Vyndr Mobile.dc.html new file mode 100644 index 0000000..7ce91e0 --- /dev/null +++ b/specs/design-reference/Vyndr Mobile.dc.html @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + +
+ +
+
+
+
VYNDR MOBILE · COMPANION SPEC
+

The terminal, in a pocket.

+

Same tokens, same ranked density, same earned motion — recomposed for thumbs. Tab bar owns navigation; one hero per screen survives the shrink.

+
+ ◂ DESKTOP SYSTEM +
+ +
+ + +
+
01 · TERMINAL
+ +
+ +
+
+ + VYNDR +
+
+ + --:--:-- +
+
+ +
+ EDGES 14 + AVG CLV +2.8% + 47 GAMES +
+ +
+
+ EDGE BOARD + EDGE ▼ +
+ +
+ 01 +
Jokić o27.5 PTS
NBA · DEN @ MIN
+ A+ + +8.4% +
+
+ 02 +
Skenes o6.5 K
MLB · PIT @ CHC
+ A + +6.1% +
+
+ 03 +
Edwards o5.5 AST
LIVE · Q3 4:12
+ A + +5.2% +
+
+ 04 +
Judge o1.5 TB
MLB · NYY @ BOS
+ B + +3.4% +
+
+ 05 +
McDavid o1.5 PTS
NHL · EDM @ CGY
+ B + +3.0% +
+
+ 06 +
LaVine o22.5 PTS
NBA · CHI @ MIL
+ D + -1.2% +
+
+ +
+ WIRE + Jokić 25.5 → 27.5 since open. Grade holds A+. +
+ +
+
+ + BOARD +
+
+ + STREAKS +
+
+ + BUILD +
+
+ + LEDGER +
+
+ + /u +
+
+
+
+
+ + +
+
02 · GRADE REVEAL
+ +
+
+ ◂ BOARD + CLV-VERIFIED +
+
+
DECLASSIFIED · TOP GRADE
+
+
+
A+
+
TIER
+
+
+
+ NJ +
Nikola Jokić
C · DEN · CONFIRMED
+
+
Over 27.5 Points
+
DK -114opened 25.5
+
+
+
+ + + + + +
+ OPEN 25.5 + STEAM ▲ + NOW 27.5 +
+
+
+
EDGE
+8.4%
+
L10 HIT
8/10
+
PROJ
30.8
+
+
+
VYNDR INTELLIGENCE
+

MIN plays the 4th-fastest pace vs. centers and concedes +3.1 boards to CONDUCTOR-type bigs. Line lagged the matchup by 2.0.

+
+ + +
+
+
+
+ + +
+
03 · BUILD
+ +
+
+ PARLAY SLIP + 2 LEGS +
+
+
+ +
Jokić o27.5 PTS
NBA · DEN @ MIN
+ A+ + +105 +
+
+ +
Edwards o5.5 AST
NBA · DEN @ MIN
+ A + +130 +
+
+
SAME-GAME CORRELATION
+ DEN @ MIN — legs move together. Real edge shrinks; stake trimmed to size the true risk. +
+
+
COMBINED
+371
+
GRADE
A-
+
STAKE
0.4u
+
+
+
ADD FROM TONIGHT'S A/B READS
+
+ +
Skenes o6.5 K
+ -120 +
+
+ +
Judge o1.5 TB
+ +140 +
+
+
+
+ +
+
+
+
+ + +
+
04 · LOCK SCREEN · LIVE ACTIVITY
+ +
+ +
+
+ + Edwards A + +
+
+ +
+
Wednesday, July 16
+
9:41
+
+ +
+
+
+ + VYNDR +
+
LIVE · Q3 4:12
+
+
+
+ A + LIVE TIER +
+
+
Edwards o5.5 AST
+
4 AST · pacing · usage +6%
+
+ + + + + + +
+
4 OF 6NEEDS 2 · 16 MIN LEFT
+
+
+
+ +
+ +
STEAM ALERT
Skenes K line 6.5 → 7.5 — grade re-check at first pitch.
+ now +
+
+
THE TERMINAL WATCHES WHILE YOU DON'T
+
+
+
+ + +
+
05 · /u RECORD
+ +
+
+
+
EH
+
+
@edgehunter
+
+ CLV-VERIFIED + 412 graded +
+
+
+
+
HIT RATE
64.2%
+
UNITS
+38.6u
+
ROI
+11.4%
+
AVG CLV
+3.1%
+
+
+ GRADE HISTORY · L18 + HITMISS +
+
+
ARCHETYPE FINGERPRINT
+
+
TORCH32%
+
BOMBER24%
+
ALPHA18%
+
+
+
+ +
+
+
+
+ + +
+
06 · LANDING
+ +
+
+
+ + VYNDR +
+

Every line,
graded before
you bet it.

+

The sports intelligence terminal. 312 props graded tonight, every grade settled in public.

+
+ +
+
+ LIVE · TOP OF THE BOARD + --:--:-- +
+
+
Jokić o27.5 PTS
NBA · DEN @ MIN
+ A+ + +8.4% +
+
+
Skenes o6.5 K
MLB · PIT @ CHC
+ A + +6.1% +
+
+ +
+ CLV-VERIFIED + RECORD PUBLIC + MISSES INCLUDED +
+
+
+ + +
+
+
+
+ + +
+
07 · PRICING
+ +
+
+
ONE DECISION
+
+ FREE + PRO +
+
+ +
+
+ THE DESK + ALL SPORTS +
+
+ $44.99 + /mo +
+
+ The full edge board — every graded read, nightly + Live grade-shift + steam alerts on lock screen + CLV-verified public record + share cards + Correlation math on every slip +
+ +
cancel anytime · no trial — the free tier is the trial
+
+ +
+
+ FREE + $0 +
+
+ Live scores + the wire + Tonight's top 3 grades + Board ranks 4–47 stay sealed +
+
+
+
412 GRADED · RECORD PUBLIC · MISSES INCLUDED
+
+
+
+ + +
+
08 · STREAKS
+ +
+
+ STREAKS + RANKED · LENGTH ▼ +
+
+ +
+
+
LA
+
+
Luka Dončić
+
o29.5 PTS · built vs bottom-10 defenses
+
+
+
+ 11 + STRAIGHT + 100% · L11 +
+
+ +
+
TOR
+
Vladimir Guerrero Jr.
o1.5 TB · correlated w/ team total
+
7
STRAIGHT
+
+
+
NYY
+
Aaron Judge
o1.5 TB · vs LHP only
+
5
STRAIGHT
+
+
+
EDM
+
Connor McDavid
o1.5 PTS · home only
+
4
STRAIGHT
+
+
+ +
+
BOARD
+
STREAKS
+
BUILD
+
LEDGER
+
/u
+
+
+
+
+ + +
+
09 · LEDGER · PENDING
+ +
+
+ LEDGER +
+ PENDING + SETTLED +
+
+
+
+ +
Edwards o5.5 AST
LIVE · 4 OF 6 · Q3 4:12
+ A + 1.0u +
+
+ +
Jokić o27.5 PTS
TIP 10:05 PM · DK -114
+ A+ + 1.5u +
+
+ +
Parlay · Jokić + Edwards
CORRELATED · STAKE TRIMMED
+ A- + 0.4u +
+
+ +
Skenes o6.5 K
FIRST PITCH 7:10 PM
+ A + 1.0u +
+
+
AT RISK
3.9u
+
TO WIN
5.6u
+
AVG GRADE
A
+
+
+
+
BOARD
+
STREAKS
+
BUILD
+
LEDGER
+
/u
+
+
+
+
+ + +
+
10 · LEDGER · SETTLED + TIER RECORD
+ +
+
+ LEDGER +
+ PENDING + SETTLED +
+
+
+
+
Jokić o27.5 PTS
settled 30 · CLV +2.4%
+ A + HIT +
+
+
Edwards o5.5 REB
settled 4 · CLV −0.8%
+ C + MISS +
+
+
Skenes o6.5 K
settled 8 · CLV +4.0%
+ A+ + HIT +
+ +
+
+ TIER RECORD + SEASON · ALL SPORTS +
+
+ TIERHIT RATERECORDROI +
+
+ A+ +
+ 24–6+31% +
+
+ A +
+ 41–19+18% +
+
+ B +
+ 58–42+6% +
+
+ C +
+ 30–28+1% +
+
+ D +
+ 12–20FADE ✓ +
+
THE GRADES CALIBRATE — A+ HITS 80%, D LOSES. THAT'S THE PRODUCT.
+
+
+ +
+
BOARD
+
STREAKS
+
BUILD
+
LEDGER
+
/u
+
+
+
+
+ + +
+
11 · EMPTY STATE
+ +
+
+ EDGE BOARD + A-TIER FILTER ON +
+
+
TRANSMISSION QUIET
+
No reads cleared the A-tier threshold yet.
+

312 props graded — none hit the edge floor. That's the system protecting you, not a bug. Grades refresh as lines move.

+ + +
+
THE ENGINE IS STILL WATCHING
+
+
+
+ + +
+
12 · SCAN
+ +
+
+
+ + jok +
+
+
+
TOP MATCH
+
+ NJ +
Nikola Jokić
C · DEN · CONDUCTOR
+ A+ +
+
READS TONIGHT
+
+
Jokić o27.5 PTS
DK -114 · steam ▲
+ +8.4% +
+
+
Jokić o12.5 REB
FD -108 · flat
+ +1.2% +
+
RECENT
+
+ skenes + den @ min + ufc 312 +
+
+
+
+
+ + +
+
13 · PLAYER
+ +
+
+
+
NJ
+
+
Nikola JokićCONFIRMED
+
C · #15 · Denver Nuggets · vs MIN tonight
+
+
+
+ CONDUCTOR 62% + TORCH 38% +
+ +
+
+ PTS / GAME · L15 + ▲ +2.3 +
+
+ 28.6 +
#4 of 82 C
MIN allows 26.1
+
+
+ + L10 +
+
+ +
TONIGHT'S READS
+
+
o27.5 PTS
DK -114 · opened 25.5 · steam ▲
+ A+ + +8.4% +
+
+
o12.5 REB
FD -108 · flat
+ B + +1.2% +
+
+
o9.5 AST
MGM -115 · drifting ▼
+ C + +0.3% +
+
+
+
+
+ + +
+
14 · TEAM HUB
+ +
+
+
+
DEN
+
+
Denver Nuggets
+
53–24 · 2ND WEST · @ MIN TONIGHT
+
+
+2.1
NET RTG L10
+
+
+
PACE
98.4 #12
+
OFF RTG
121.2 #2
+
DEF RTG
114.8 #18
+
+
ROTATION · RANKED BY TONIGHT'S EDGE
+
+ 01 +
Jokić
C · 28.6 PPG · o27.5
+ A+ +
+
+ 02 +
Murray
PG · 21.3 PPG · o20.5
+ B +
+
+ 03 +
Gordon
PF · 14.8 PPG · o13.5
+ C +
+
+ 04 +
Porter Jr. GTD
SF · 16.4 PPG · awaiting status
+ +
+
+
TEAM READ
+

MIN concedes +3.1 boards to CONDUCTOR bigs and plays top-5 pace — DEN's edge concentrates in Jokić props, not the spread.

+
+
+
+
+
+ + +
+
15 · EXPLORE
+ +
+
+ EXPLORE + FUTURES · NEWS +
+
+ +
+
FUTURES · MVP · MISPRICED
+
+ NJ +
Jokić MVP
DK +320 · model fair +190
+
+9.3%
EDGE
+
+
+
MORE FUTURES
+
+
Thunder to win West
FD +240 · fair +215
+ B +
+
+
Skenes NL Cy Young
MGM +150 · fair +160
+ C +
+
THE WIRE · TONIGHT
+
+ 8:42 +
INJURY
Porter Jr. GTD (ankle) — Jokić usage read firms if he sits.
+
+
+ 7:15 +
STEAM
Sharp money on Skenes K over — line 6.5 → 7.5 at three books.
+
+
+
+
+
+ + +
+
16 · COMBAT MATCHUP
+ +
+
+ UFC 312 · LW · 5 RND + CLV-VERIFIED +
+
+ +
+
+
IM
+
Islam M.
26–1 · ORTHODOX · 70"
+ GRAPPLER 80% +
+
+
+ COMBAT SAMBO + DAGESTAN WRESTLING +
+
+ +
+ + GRAPPLER EDGE + +
+
+
+
AV
+
Alex V.
26–3 · ORTHODOX · 71.5"
+ PRESSURE 72% +
+
+
+ ELITE TDD 94% + ? WRESTLING BASE +
+
+
+ RECENT-FORM OVERRIDE +
Volk stuffed 11 of 12 TDs last 3 fights → pushes DECISION over submission.
+
+
+
ML · ISLAM -230
A-
+
DECISION · +120
A
+
SUB · +280
B+
+
O2.5 RND · -140
A-
+
+
+
+
+
+ + +
+
17 · PITCHER IDENTITY
+ +
+
+
+
PIT
+
+
Paul SkenesCONFIRMED
+
RHP · #30 · PIT · vs CHC 7:10
+
+
+
+ ALPHA + PUNCHOUT + TUNNELER +
+ +
+
+ PITCHVELOUSEWHIFF +
+
4-Seam99.132%26%
+
Splinker94.224%38%
+
Slider87.022%41%
+
+
+
APPROACH · TUNNELER
+

Splinker + slider tunnel off the 99mph 4-seam — whiffs cluster the 3rd time through.

+
+
ONE IDENTITY → MANY PROPS
+
+
o6.5 K
line moved 6.5 → 7.5 · steam ▲
+ A + +6.1% +
+
+
o16.5 OUTS
workload steady L5
+ A- + +4.2% +
+
+
+
+
+ + +
+
18 · ARCHETYPES
+ +
+
+ ARCHETYPES + 83 MARKS · 8 SPORTS +
+
+ NBA + MLB + NFL + MMA + +4 +
+
+
+
TORCH
+
CONDUCTOR
+
ARCHITECT
+
BOLT
+
IGNITER
+
FORTRESS
+
+ +
+
THE BLEND · EVERY PLAYER IS A FINGERPRINT
+
+ NJ +
+
CONDUCTOR62%
+
+
TORCH38%
+
+
+
+
+
+
+
+
+ + +
+
19 · CORRELATION DETAIL
+ +
+
+ ◂ SLIP + CORRELATION READ +
+
+
+
JOKIĆ o27.5 × EDWARDS o5.5
+
r=.62
+
STRONGLY CORRELATED
+
+
+
WHY THEY MOVE TOGETHER
+

Same game, same pace engine. A fast DEN @ MIN inflates both point and assist volume — when one hits, the other usually has. Books price the parlay as if independent; it isn't.

+
+
+
BOOK PAYS AS IF
+371
+
TRUE FAIR VALUE
+295
+
+
+
+ VYNDR STAKE GUIDANCE + 0.4u +
+
Sized to the true fair value, not the payout. Keep the legs — trim the stake.
+
+
+
+ +
+
+
+
+
+
+
+
+ + + diff --git a/specs/design-reference/Vyndr Offseason.dc.html b/specs/design-reference/Vyndr Offseason.dc.html new file mode 100644 index 0000000..35af5d5 --- /dev/null +++ b/specs/design-reference/Vyndr Offseason.dc.html @@ -0,0 +1,961 @@ + + + + + + + + + + + + + + + + + + +
+ + + + +
+ THE WIRE + + --:--:-- + Offseason desk open — 214 NFL season lines repriced on news. + LOG ▸ +
+ +
+ + +
+
SESSION 2 · SURFACE 1 · THE OFFSEASON HUB
+

Every sport is always in season.

+

A sport is in one of two states: LIVE SLATE or OFFSEASON HUB. The hub lives inside the sport — tapping NFL gets NFL, whatever NFL is today. Season-long is a different read: no "tonight," longer horizons, lines that move on news. The story is the movement — open, current, model.

+ +
+ + +
+ 01 +
HUB HOME
Per sport. NFL primary · NBA variant. Countdown is ambient context, never the hero — the hero is what changed today.
+
+ +
+
+ NFL HUB · DESKTOP + SPORT TAB STATE LIVES IN THE TAB · NEVER A SEPARATE "OFFSEASON" TAB +
+ +
+ +
+
+ + VYNDR +
+
+ MLB · 9 LIVE + NFL · CAMP −5D + NBA · SL DAY 8 + NHL · FA + UFC · SAT +
+
+ + --:--:-- +
+
+ + +
+
+
+ NFL + THE OFFSEASON DESK +
+
OUTLOOKS REPRICE ON NEWS · NOT GAME ODDS · AS OF JUL 17
+
+
{{ kickoffDays }} DAYS TO KICKOFF · THU SEP 10
+
+ + +
+
+
+
+
+
TAG DDL · JUL 15
+
CAMPS · JUL 22
+
HOF GAME · JUL 30
+
PRESEASON · AUG 6
+
FINAL CUTS · SEP 1
+
KICKOFF · SEP 10
+
+
+
+ +
+ +
+
+ WHAT CHANGED TODAY + 4 EVENTS · JUL 17 +
+
+
+
11:42 AMCLEARED
+
Nabers cleared for camp — no restrictions
+
NYG · REC YDS 1,050.51,120.5 · VYNDR 1,188 · READ ▸
+
+
+
10:15 AMCAMP REPORT
+
Worthy running clear WR1 reps in KC installs
+
KC · REC YDS 990.51,024.5 · VYNDR 1,061 · READ ▸
+
+
+
9:30 AMCONTRACT
+
Higgins restructure done — reports Tuesday
+
CIN · BURROW PASS TD 27.528.5 · VYNDR 29.8 · READ ▸
+
+
+
8:05 AMDEPTH
+
Bills name Coleman the starting X receiver
+
BUF · REC YDS 815.5815.5 · FLAT · MODEL HOLDS 862
+
+
+ SOURCE-LINKED · EVERY IMPACT SHOWS THE REPRICE + FULL FEED ▸ +
+
+ + +
+
+
+ SEASON BOARD + BIGGEST MODEL−MARKET GAPS +
+
+
+
Nabers o1,120.5 Rec Yds
O 1,050.5 · NOW 1,120.5 · M 1,188
+ A+ + +6.0% +
+
+
Allen o37.5 Pass+Rush TD
O 36.5 · NOW 37.5 · M 40.2
+ A + +4.8% +
+
+
Chiefs u11.5 Wins
O 11.5 · NOW 11.5 · M 10.8
+ B + +3.1% +
+
+ 214 SEASON LINES + FULL BOARD ▸ +
+
+ +
+
+
THE VYNDR REPORT
+
The offseason, read daily. One email, no noise.
+
+ SUBSCRIBE +
+
+
+
+
+ + +
+
+ HUB HOME · 390 + NEW TAB BAR — SLATE · EXPLORE · READ FAB · LEDGER · MORE +
+
+ +
+
S1-A · NFL HUB
+ +
+
+
+ + VYNDR +
+
+ + --:--:-- +
+
+
+ MLB 9 + NFL · CAMP −5D + NBA · SL + NHL +
+
+
NFLOFFSEASON DESK
+ {{ kickoffDays }}D TO KICKOFF +
+
+
+ WHAT CHANGED TODAY + JUL 17 +
+
+
Nabers cleared for camp — no restrictions
+
NYG · REC YDS 1,050.51,120.5 · M 1,188
+
+
+
Worthy running clear WR1 reps in KC installs
+
KC · REC YDS 990.51,024.5 · M 1,061
+
+
+
Higgins restructure done — reports Tuesday
+
CIN · PASS TD 27.528.5 · M 29.8
+
+
+ SEASON BOARD + FULL BOARD ▸ +
+
+
Nabers o1,120.5 Rec Yds
O 1,050.5 → 1,120.5 · M 1,188
+ A+ +
+
+
Allen o37.5 P+R TD
O 36.5 → 37.5 · M 40.2
+ A +
+ +
+
CAMPS
JUL 22
+
PRESEASON
AUG 6
+
CUTS
SEP 1
+
KICKOFF
SEP 10
+
+
+ +
+ WIRE + Nabers 1,050.5 → 1,120.5 on camp clearance. +
+ +
+
+ + SLATE +
+
+ + EXPLORE +
+
+ + READ +
+
+ + LEDGER +
+
+ + MORE +
+
+
+
+
+ +
+
S1-B · NBA VARIANT · SUMMER LEAGUE HONESTY
+ +
+
+
+ + VYNDR +
+
+ + --:--:-- +
+
+
+ MLB 9 + NFL · CAMP + NBA · SL DAY 8 + NHL +
+
+
NBASUMMER DESK
+ 95D TO OPENING NIGHT +
+
+ +
+
+ SUMMER LEAGUE · LAS VEGAS + OUTLOOK ONLY +
+
We don't grade Summer League props. Ten-day samples against fringe rosters aren't signal — they're noise wearing a jersey. We read minutes, roles, and rotation intent instead.
+
+
AD
Dybantsa F · R
31 MIN/G · USAGE-HEAVY · STARTER-TRACK
NOT GRADED
+
+
+
CB
Boozer F · R
28 MIN/G · CLOSING LINEUPS · ROTATION LOCK
NOT GRADED
+
+
+
DP
Peterson G · R
24 MIN/G · SECOND UNIT · WATCHLIST
NOT GRADED
+
+
+ +
+ SEASON BOARD + FULL BOARD ▸ +
+
+
Wembanyama MVP
O +420 → +330 · FAIR +270
+ A +
+
+
Thunder o61.5 Wins
O 60.5 → 61.5 · M 60.9 · THIN
+ C +
+ +
+
SL ENDS
JUL 19
+
MEDIA DAY
SEP 28
+
CAMPS
SEP 29
+
OPENER
OCT 20
+
+
+
+ WIRE + Wemby MVP steams +420 → +330 since June. +
+
+
+ + SLATE +
+
+ + EXPLORE +
+
+ + READ +
+
+ + LEDGER +
+
+ + MORE +
+
+
+
+
+
+
+ + +
+ 02 +
SEASON-LONG BOARD
The open → current → model triplet is the signature. Ladder pattern: one row per player, lines nested.
+
+ +
+
+ NFL SEASON BOARD · DESKTOP + OPEN DIM · NOW WHITE · MODEL GREEN — ONE READING ORDER, EVERY ROW +
+ +
+ +
+ ALL MARKETS + PASSING + RUSH / REC + TDS + WIN TOTALS + DIVISIONS + AWARDS + SORTED · MODEL GAP ▼ +
+ +
+ PLAYER / MARKET + OPEN + NOW + VYNDR + EDGE + GRD + MOVE · SINCE OPEN +
+
+ + +
+ MN + Malik Nabers + WR · NYG + BURNER + 2 LINES +
+
+ Receiving Yards O/U + 1,050.5 + 1,120.5 + 1,188 + +6.0% + A+ + ▲ +70.0 +
+
+ Receptions O/U + 88.5 + 92.5 + 97.0 + +3.2% + B + ▲ +4.0 +
+ + +
+ JA + Josh Allen + QB · BUF + MAESTRO + 3 LINES +
+
+ Pass + Rush TD O/U + 36.5 + 37.5 + 40.2 + +4.8% + A + ▲ +1.0 +
+
+ Passing Yards O/U + 3,895.5 + 3,940.5 + 4,080 + +3.6% + B + ▲ +45.0 +
+
+ MVP AWARDS + +650 + +600 + +490 + +2.6% + B + ▲ 650→600 +
+ + +
+ BR + Bijan Robinson + RB · ATL + BELL COW + 2 LINES +
+
+ Rushing Yards O/U + 1,275.5 + 1,290.5 + 1,342 + +2.2% + C + ▲ +15.0 +
+
+ Rushing TD O/U + 11.5 + 11.5 + 12.4 + +1.4% + C + FLAT · 41D +
+ + +
+ TEAM WIN TOTALS + 32 TEAMS +
+
+ DET Lions WINS O/U + 10.5 + 11.5 + 11.9 + +1.9% + C + ▲ +1.0 +
+
+ KC Chiefs WINS O/U · MODEL LEANS UNDER + 11.5 + 11.5 + 10.8 + +3.1% + B + FLAT · 62D +
+ +
+ + 207 SEASON LINES · REPRICED ON NEWS · LAST REPRICE JUL 17 · 11:42 AM ET + EDGE = MODEL VS CURRENT · ODDS MARKETS GRADED ON FAIR PRICE +
+
+
+ + +
+
+
+
S1-C · SEASON BOARD · 390
+ +
+
+
+ + NFL · SEASON BOARD +
+ 214 LINES +
+
+ ALL + PASS + RUSH/REC + WINS + AWARDS +
+
+ +
+ MN + Nabers + WR · NYG + BURNER +
+
+
+
Rec Yds
+
1,050.51,120.5 · V 1,188
+
+ ▲ +70 + A+ +
+
+
+
Receptions
+
88.592.5 · V 97.0
+
+ ▲ +4 + B +
+
+ JA + Allen + QB · BUF + MAESTRO +
+
+
+
Pass + Rush TD
+
36.537.5 · V 40.2
+
+ ▲ +1 + A +
+
+
+
MVP FAIR PRICE
+
+650+600 · V +490
+
+ + B +
+
+ TEAM WIN TOTALS +
+
+
+
DET Wins
+
10.511.5 · V 11.9
+
+ ▲ +1 + C +
+
+
+ WIRE + Last reprice 11:42 AM — Nabers rec yds. +
+
+
+
+ + +
+
S1-D · SEASON READ REVEAL · 390
+ +
+
+ ◂ BOARD + SEASON READ + +
+
+
+ MN +
+
Malik Nabers
+
WR · NYG · BURNER
+
+ A+ +
+
Over 1,120.5 Receiving Yards · 2026 season
+ +
+
OPEN · MAR 4
1,050.5
+
NOW
1,120.5
+
VYNDR
1,188
+
+ +
+ + + + + + + +
+ MAR 12 · FA + APR 25 · DRAFT + JUL 16 · CLEARED +
+
+ +
+
+ + VYNDR INTELLIGENCE +
+

Pre-injury target share 28% with no added competition. Market repriced the clearance +70 but still carries the injury discount — on current form the number is 68 yards light.

+
+ +
+
WHAT WOULD CHANGE THIS READ
+
Soft-tissue setback in campRE-GRADE ≤ B
+
QB room change before Week 1RE-RUN MODEL
+
Slot usage in preseason installsMODEL ↑
+
+
+
+
MODEL RUN JUL 17 · 6:00 AM ET · SETTLES WK 18 · CLV TRACKED FROM LOCK
+
+ LOCK THE READ + SHOP ▸ +
+
+
+
+
+
+
+ + +
+ 03 +
SEASON READ REVEAL
The grade reveal at a season horizon — no "tonight." News-driven factors, movement annotated with events, and what would change the read.
+
+ +
+
+
+
+
+ SEASON READ · 2026 · NFL + CLV TRACKED FROM LOCK +
+
+
+
A+
+
TIER
+
+
+
+ MN +
+
Malik Nabers
+
WR · NYG · BURNER
+
+
+
Over 1,120.5 Receiving Yards · season
+
+ + + + + + + +
+ OPEN 1,050.5 · MAR 4 + MAR 12 FA · APR 25 DRAFT + JUL 16 CLEARED · NOW 1,120.5 +
+
+
+
+
+
OPEN
1,050.5
+
NOW
1,120.5
+
VYNDR
1,188
+
+
+
+ + VYNDR INTELLIGENCE +
+

On current form: pre-injury target share 28%, no added target competition, QB continuity intact. The market repriced the clearance +70 yards but still carries last season's injury discount. Our season sim puts the number 68 yards light — the news, not the noise, moved this line.

+
+
MODEL RUN JUL 17 · 6:00 AM ET · SETTLES WK 18 · JAN 3 2027 · NO "TONIGHT" ON A SEASON READ
+
+
+ + +
+
+
NEWS-DRIVEN FACTORS
+
+
JUL 16Cleared for camp — no restrictionsMODEL ▲
+
APR 25No WR drafted in first four roundsMODEL ▲
+
MAR 12O-line additions in free agencyNEUTRAL
+
+
+
WHAT WOULD CHANGE THIS READ
+
+
Soft-tissue setback in campRE-GRADE ≤ B
+
QB room change before Week 1RE-RUN MODEL
+
Heavy slot usage in preseason installsMODEL ↑
+
EVERY SEASON READ NAMES ITS KILL CONDITIONS — HONESTY IS THE PRODUCT
+
+
+
+
+ + +
+ 04 +
NEWS / OUTLOOK FEED
Event → affected entities → line impact → article. One anatomy, reused in the hub, the reveal, and /blog.
+
+ +
+
+
+ THE FEED · NFL + EVENTS THAT MOVED OUTLOOKS ONLY +
+
+
TODAY · JUL 17
+
+
11:42 AMCLEAREDSOURCE · TEAM REPORT
+
Nabers cleared for camp with no restrictions — the injury discount is now the market's mistake
+
+ NYG + M. NABERS + REC YDS 1,050.51,120.5 · V 1,188 + FULL ARTICLE ▸ +
+
+
+
10:15 AMCAMP REPORTSOURCE · BEAT POOL
+
Worthy running clear WR1 reps in KC installs
+
+ KC + REC YDS 990.51,024.5 · V 1,061 + FULL ARTICLE ▸ +
+
+
+
9:30 AMCONTRACTSOURCE · LEAGUE FILING
+
Higgins restructure keeps the WR2 in stripes — Burrow's ceiling holds
+
+ CIN + PASS TD 27.528.5 · V 29.8 + FULL ARTICLE ▸ +
+
+
WED · JUL 16
+
+
4:40 PMINJURY
+
Kincaid tweaks hamstring in conditioning — monitoring, not moving
+
+ BUF + REC YDS 625.5 · HELD · WATCHING +
+
+ +
+
QUIET WIRE
+
No outlook-moving news since 11:42 AM. We don't manufacture movement.
+
EMPTY STATE · RENDERS WHEN A DAY HAS NO EVENTS
+
+
+ + +
+
ROW ANATOMY
+
+
1
TIME + EVENT TAG + SOURCE
Tag color = meaning: neutral grey, CONTRACT amber, INJURY red. Never green — green is edge only.
+
2
HEADLINE
Editorial voice — states what happened and why the desk cares. Weight 700 only when the event repriced a line.
+
3
AFFECTED CHIPS
Existing TeamChip + player chips. Tap target ≥ 44px on mobile.
+
4
LINE IMPACT
old → new · VYNDR model. FLAT + HELD are honest values — a non-move is information.
+
5
FULL ARTICLE ▸
Present only when an article exists. No dead links, no "coming soon."
+
+
+
REUSED IN · HUB "WHAT CHANGED TODAY" · REVEAL FACTORS · /BLOG FEED · THE REPORT EMAIL
+
+
+
+ +
+
+ HUB HOME · SEASON BOARD · SEASON REVEAL · THE FEED · NFL + NBA · DESKTOP + 390 + VYNDR · SESSION 2 · SURFACE 1 +
+
+ +
+
+
+ + + diff --git a/specs/design-reference/Vyndr Price Triplet.dc.html b/specs/design-reference/Vyndr Price Triplet.dc.html new file mode 100644 index 0000000..44fad24 --- /dev/null +++ b/specs/design-reference/Vyndr Price Triplet.dc.html @@ -0,0 +1,426 @@ + + + + + + + + + + + + + + + + + + +
+ + + +
+ THE WIRE + + --:--:-- + Intelligence desk open — 7 books held per prop, CLV tracked from lock. + LOG ▸ +
+ +
+ +
+
VYNDR · ACT 01 · THE PRICE TRIPLET
+

The honest layer, rendered.

+

The price triplet leads — book · fair · model, the de-vigged number nobody else shows consumers. Then line shopping, article media, CLV & calibration, and The Report — extensions sharing primitives. The movement strip is defined once here and reused everywhere a line has a past.

+
BOOK · FAIR · MODEL · FIVE HONESTY STATES · STAT PROJECTION + PRICE LAYER
+
+ + +
+ 01 +
THE PRICE TRIPLET · HERO
Book · fair · model, per prop — the price-honesty layer beside the stat projection. Fair is the amber hero, honest on every row: value, edge-but-priced-out, or no edge. Renders on the read card, board and hero; the close lives in the ledger.
+
+ +
+ + +
+
+ ANATOMY + EXTENDS THE PARLAY "TRUE FAIR VALUE" — 2-UP GRID → 3-LEG PER-PROP +
+
+
+
+
BOOK
+
+125
+
OFFERED
+
+
+
◆ FAIR
+
+110
+
DE-VIGGED · HERO
+
+
+
MODEL
+
+98
+
OUR PRICE
+
+
+
+
+
BOOK
The market's offered line. Honest on every row.
+
FAIR · THE HERO
The de-vigged fair number. The whole product. Never hidden — not even when the model leg is poisoned.
+
MODEL
VYNDR's own price. Green only for takeable edge — never a juiced one. Suppressible on quarantined rows.
+
+
SAME TOKENS AS THE PARLAY · LABEL 7.5PX / .14EM / #707080 · VALUE MONO 800 · FAIR AMBER #FFB347 VS BOOK #F0F0F0 · GREEN #00D4A0 = TAKEABLE EDGE ONLY · BLUE = EDGE PRICED OUT
+
+ + +
+ FIVE HONESTY STATES · DESKTOP + THREE VERDICTS — VALUE · EDGE-NOT-TAKEABLE · NO EDGE — PLUS QUARANTINE & REFUSAL +
+
+ + +
+
+
Nabers o 1,115.5 Rec Yds
NYG · WK 1
+ 1 · VALUE +
+
+
BOOK
+125
OFFERED
+
◆ FAIR
+110
DE-VIGGED
+
MODEL
+98
OUR PRICE
+
+
+
VALUE · +2.9% VS FAIR
+
Model prices it shorter than the honest number — the book is paying longer than the leg is worth. Real edge.
+
+
THE PRICE STORY · READ CARDCLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Chase o 6.5 Receptions
CIN · WK 1
+ 2 · PRICED OUT +
+
+
BOOK
−210
JUICED
+
◆ FAIR
−175
DE-VIGGED
+
MODEL
−240
OUR PRICE
+
+
+
EDGE · NOT TAKEABLE
+11.7% EV AT −210
+
The math shows edge — but −210 is too juiced to bet. We won't call a juiced price a value, and we won't pretend the edge wasn't there.
+
+
TAKEABLE BAND −160 TO +200CLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Achane o 71.5 Rush Yds
MIA · WK 1
+ 3 · NO EDGE +
+
+
BOOK
+118
OFFERED
+
◆ FAIR
+104
DE-VIGGED
+
MODEL
+112
OUR PRICE
+
+
+
NO EDGE HERE · −1.8% VS FAIR
+
Model lands short of fair — the honest number is already priced. We're not calling this a play. That "no" is the instrument working.
+
+
THE PRICE STORY · READ CARDCLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Bijan o 84.5 Rush Yds
ATL · WK 1
+ 4 · QUARANTINE · 38% +
+
+
BOOK
+130
OFFERED
+
◆ FAIR
+116
DE-VIGGED
+
MODEL
— · —
WITHHELD
+
+
+
MODEL READ WITHHELD
+
Graded against an unverified matchup — so we suppressed our price. Book & fair stand; we never hide the honest fair number because a different leg is poisoned.
+
+
PRIMARY STATE · 200 / 520 ROWSCLOSES TRACKED IN YOUR LEDGER ▸
+
+ + +
+
+
Egbuka o 44.5 Rec Yds
TB · NFL DEBUT · NO PRIORS
+ 5 · REFUSAL +
+
+
+
CAN'T GRADE THIS ONE
+
No fair price we'd defend yet — the inputs are too thin. When they clear, the triplet returns. We'd rather show nothing than a number we can't stand behind.
+
NO FABRICATED PRICE · NO EMPTY GAUGE · TRUTH LAW
+
+
+
NOTHING TO TRACK YETLEDGER OPENS ON FIRST HONEST LOCK
+
+
+ + +
+ IN CONTEXT · WHERE IT RENDERS + READ-CARD HERO · BOARD ROW · FREE-TIER GATE +
+ + +
+
+ READ CARD · HERO + NFL + JUL 17, 2026 · THE DESK +
+
+
+
+ A+ +
Nabers over 1,115.5 receiving yards
NYG · WK 1 · OPENED 1,050.5
+
+

The market moved 70 on the clearance and stopped — our sim says the discount was worth twice that. Read the projection first (does it clear), then the price (is it fair). Two questions, one card.

+
TAKE +125 AT FANDUEL ▸CLOSES TRACKED
IN YOUR LEDGER ▸
+
+
+
+
① PROJECTION · WILL IT CLEARCLEARS BY 72.5
+
+
MODEL PROJ
1,188
+
vs
+
LINE
1,115.5
+
PROJECTED ABOVE THE LINE
+
+
+
+
② PRICE · IS IT FAIR
+
+
BOOK
+125
OFFERED
+
◆ FAIR
+110
DE-VIGGED · HERO
+
MODEL
+98
OUR PRICE
+
+
+
VALUE · +2.9% VS FAIR
+6.1% EV AT +125
+
Model beats the honest number and the price is takeable — the edge is real at what you can actually get.
+
+
+
+
+
+
HIERARCHY · STAT PROJECTION READS FIRST (WILL IT CLEAR) → PRICE TRIPLET QUALIFIES IT (IS IT FAIR). TWO QUESTIONS, TWO STACKED BLOCKS — NEITHER CROWDS THE OTHER. FAIR STAYS THE AMBER HERO OF THE PRICE LAYER.
+ + +
+
+
TONIGHT'S BOARD · NBARANKED BY EDGE · FAIR IS THE NUMBER
+
+ A+ +
Jokić o 27.5 Pts
PROJ 28.9 ▸ CLEARS · BOOK +125 · MDL +98
+
◆ FAIR
+110
+ +2.9% +
+
+ A +
Edwards o 5.5 Ast
PROJ 6.4 ▸ CLEARS · BOOK +112 · MDL +96
+
◆ FAIR
+103
+ +1.6% +
+
+ B +
Gobert o 11.5 Reb
PROJ 12.1 ▸ CLEARS · BOOK +108 · MDL withheld
+
◆ FAIR
+99
+ QUAR +
+
+ C +
Murray o 22.5 Pts
PROJ 22.9 ▸ THIN · BOOK +118 · MDL +124
+
◆ FAIR
+109
+ NO EDGE +
+
PROJECTION LEADS THE ROW (WILL IT CLEAR) · FAIR IS THE PRICE · EDGE CHIP IS THE VERDICT
+
+ + +
+
FREE-TIER GATE · MODEL LEG LOCKED
+
+
Nabers o 1,115.5
FREE
+
+
BOOK
+125
+
◆ FAIR
+110
+
MODEL
+ █ █
+
+
MODEL PRICE ON ANALYSTUNLOCK ▸
+
+
The honest de-vigged number stays free — that's the hook. Only VYNDR's own price gates. The fair leg is never the paywall.
+
+
+ + +
+ + +
+
MOBILE · 390 · THREE LEGS HOLD, FAIR STAYS HERO
+
+ +
+
Nabers o 1,115.5 Rec
VALUE
+
+
BOOK
+125
+
◆ FAIR
+110
+
MODEL
+98
+
+
+2.9% VS FAIRreal edge
+
+ +
+
Bijan o 84.5 Rush
QUARANTINE
+
+
BOOK
+130
+
◆ FAIR
+116
+
MODEL
+
+
MODEL WITHHELDbook & fair stand
+
+
+
44PX MIN TAP ON THE LEDGER LINK · NO HORIZONTAL SCROLL AT 390 · FAIR NEVER DROPS BELOW BOOK/MODEL IN SIZE
+
+ + +
+
TIER PLACEMENTRECOMMENDED · CONFIRM
+
The de-vigged fair number is the free-tier hook per prior product intent — worth surfacing on Free. Flagged for confirmation before ship.
+
+
+ FREE +
Book · fair — model leg locked.
The hook: the honest de-vigged number, free. Model shows as a locked teaser that upsells the read.
+
+
+ ANALYST +
Full triplet + VALUE marker.
All three legs, the verdict fires both ways — value and no-edge. Quarantine renders as the primary two-leg state.
+
+
+ DESK +
Triplet + ledger cross-ref + calibration.
The price story wired to its resolved close and the confidence-bucket record behind the model leg.
+
+
+
+
+ +
+ THE TRIPLET NEVER PRINTS A NUMBER IT CAN'T STAND BEHIND + NO FABRICATED MODEL PRICE · NO HIDDEN "NO EDGE" · FAIR IS HONEST ON EVERY ROW +
+
+ +
+
+
+ + + diff --git a/specs/design-reference/Vyndr System.dc.html b/specs/design-reference/Vyndr System.dc.html new file mode 100644 index 0000000..47b8a04 --- /dev/null +++ b/specs/design-reference/Vyndr System.dc.html @@ -0,0 +1,1743 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ THE WIRE + + --:--:-- + Session open — 312 props graded across 47 games. + LOG ▸ +
+ + + +
+
+
+ + + ESC +
+
+
JUMP TO
+
+ + Tonight's Slate · Edge Board + +
+
+ + Ledger · settled history + +
+
+ + Streaks + +
+
PLAYERS
+
+ NJ + Nikola Jokić · C · DEN + A+ +
+
+ PIT + Paul Skenes · RHP · PIT + A +
+
+ MIN + Anthony Edwards · SG · MIN + A +
+
ACTIONS
+
+ + Generate share card + +
+
+ + Export /u public record + +
+ +
+
+ ↑ ↓ NAVIGATE · ↵ OPEN · ESC CLOSE + VYNDR TERMINAL +
+
+
+
+ + +
+
+ +
+ + + + + + +
+
+
+ VYNDR + +
+ SPORTS INTELLIGENCE TERMINAL +
+
+
+ +
+
+ 14 + EDGES +
+
+ +2.8% + AVG CLV +
+
+
+ + SYNC + --:--:-- +
+ +
$44.99/mo
+
+
+ +
+ + +
+
01 · THE TERMINAL · VISUAL SYSTEM v2
+

The instrument, not the sportsbook.

+

Dense but ranked. Mostly still, then it reacts — a number pulses the instant a line moves, a card breathes when its game goes live. One hero per screen.

+
+ + +
+
+
+ TONIGHT'S SLATE + MULTI-LEAGUE · 47 GAMES · 312 PROPS GRADED +
+ RANKED BY EDGE +
+ +
+ +
+
+
+ EDGE BOARD + 14 LIVE +
+ SORTED · EDGE ▼ +
+
+
+ # + READ + GRD + EDGE +
+ +
+ 01 +
Jokić o27.5 PTS
NBA · DEN @ MIN
+ A+ + +8.4% +
+
+ 02 +
Skenes o6.5 K
MLB · PIT @ CHC
+ A + +6.1% +
+
+ 03 +
Edwards o5.5 AST
LIVE · Q3 4:12 · DEN @ MIN
+ A + +5.2% +
+
+ 04 +
Judge o1.5 TB
MLB · NYY @ BOS
+ B + +3.4% +
+
+ 05 +
McDavid o1.5 PTS
NHL · EDM @ CGY
+ B + +3.0% +
+
+ 06 +
Mahomes o274.5 PYD
NFL · KC @ BUF
+ B + +2.6% +
+
+ 07 +
Betts o1.5 TB
MLB · LAD @ SD
+ C + +2.1% +
+
+ 08 +
Brunson o24.5 PTS
NBA · NYK @ BOS
+ C + +1.4% +
+
+ 09 +
Curry o4.5 3PM
NBA · GSW @ SAC
+ C + +0.4% +
+
+ 10 +
LaVine o22.5 PTS
NBA · CHI @ MIL
+ D + -1.2% +
+
+ + 37 GRADED READS BELOW + VIEW ALL ▸ +
+
+ + +
+
+
+
+ DECLASSIFIED · TOP GRADE TONIGHT + CLV-VERIFIED +
+ +
+
+
A+
+
TIER
+
+
+
+ NJ +
+
Nikola Jokić
+
C · DEN · CONFIRMED
+
+
+
Over 27.5 Points
+
+ DK -114 + + opened 25.5 +
+ +
+ + + + + +
+ OPEN 25.5 · 9:02 AM + STEAM 4:40 PM ▲ + NOW 27.5 +
+
+
+
+ + +
+
+
EDGE
+
+8.4%
+
+
+
L10 HIT
+
8/10
+
+
+
PROJ
+
30.8
+
+
+ + +
+
+ + + + + + + + + + +
+
+ HIT + MISS +
+
+ + +
+
+ + VYNDR INTELLIGENCE +
+

MIN plays the 4th-fastest pace vs. centers and concedes +3.1 rebounds to CONDUCTOR-type bigs. Line lagged the pace matchup by 2.0 — the steam confirms it.

+
+
+
+
+
+ + +
+ 02 +
THE SYSTEM
Five components + 74 marks. Every other page is assembled from these.
+
+ +
+
+ COMPONENT SYSTEM + THE PARTS EVERY SURFACE INHERITS +
+
+ +
+ + +
+
02 · STAT BLOCK
+
+
+ PTS / GAME · L15 + ▲ +2.3 +
+
+ 28.6 +
+
#4 of 82 C
+
vs MIN allows 26.1
+
+
+
+ + + + + + + + + + + + L10 +
+
+
+ + +
+
03 · PLAYER IDENTITY
+
+
+
NJ
+ +
+
+
+ Nikola Jokić + CONFIRMED +
+
C · #15 · Denver Nuggets
+
+ + + CONDUCTOR + + + + TORCH + +
+
+
+
+ + +
+
04 · LINE DISPLAY
+
+
+ Jokić o27.5 PTS + STEAM ▲ +
+
+
+ DraftKings +
+ -114 + BEST +
+
+
+ FanDuel +
+ -120 + +
+
+
+ BetMGM +
+ -118 + +
+
+
+
+ CLV vs CLOSE + +2.4% +
+
+
+ + +
+
05 · GRADE REVEAL · tiers
+
+
+
+
A
+
EDGE
+
+
+
B
+
LEAN
+
+
+
C
+
PASS
+
+
+
D
+
FADE
+
+
+
F
+
AVOID
+
+
+
+
+ GLOW RESERVED FOR A-TIER ONLY + EDGE COLORED BY SIGN +
+
+
+
+
+ + +
+
+ ARCHETYPE GLYPHS + FULL SYSTEM · 83 UNIQUE MARKS · 8 SPORTS + CLASSIFIER LEGACY +
+
+ +

One grid, one weight, one construction logic — filled base plus weighted stroke, so each mark reads as an emblem before the word. Colors deduped off signal-green so #00D4A0 stays uniquely "edge."

+

marks names reused across sports — the vocabulary is a language: a WALL is the immovable last line in every sport.

+ + +
+ + +
+
+
THE BLEND · MULTI-ARCHETYPE FINGERPRINT
+

Players aren't one archetype — they carry a weighted blend. The mark system combines: primary dominant + supporting muted. This is the moat nobody copies.

+
+
+
+ TB +
+
Brandon Lowe
+
2B · Tampa Bay Rays
+
+
+
+
+
+ +
+
BOMBER
+
+
+
+ PRIMARY · BOMBER + 54% +
+
+
+ SUPPORTING · DRIVER + 46% +
+
+
+
+
+ +
+
DRIVER
+
+
+
+
+
+ + + +
+ 03 +
THE SURFACES
What gets posted. The timeline is customer #1 — these stop a scroll.
+
+ +
+
+ STREAKS + A BLOOMBERG ALERT, NOT A LOG LINE +
+
+ +
+ +
+
+
LA
+ +
+
+
+ Luka Dončić + + + TORCH + +
+
o29.5 PTS · built vs bottom-10 defenses
+
+
+
+ 11 + STRAIGHT +
+ HIT RATE 100% · L11 +
+
+ +
+
+
TOR
+ +
+
+
+ Vladimir Guerrero Jr. + + + BOMBER + +
+
o1.5 TB · correlated w/ team total
+
+
+
+ 7 + STRAIGHT +
+ CAUTION · CORRELATION FLAG +
+
+
+ +
+ + +
+
+ PUBLIC RECORD + vyndr.io/u/edgehunter · THE VERIFIED WEAPON +
+
+ +
+ +
+
EH
+
+
+ @edgehunter + CLV-VERIFIED +
+
+ + + TORCH-HEAVY BETTOR + + 412 graded · since Mar '24 +
+
+ +
+ + +
+
+
HIT RATE
+
64.2%
+
+
+
UNITS
+
+38.6u
+
+
+
ROI
+
+11.4%
+
+
+
AVG CLV
+
+3.1%
+
+
+ + +
+
+ GRADE HISTORY · LAST 24 +
+ HIT + MISS +
+
+
+
+ + +
+
+
ARCHETYPE FINGERPRINT
+
+
+
+ TORCH + 32% +
+
+
+
+
+ BOMBER + 24% +
+
+
+
+
+ ALPHA + 18% +
+
+
+
+
+ CONDUCTOR + 14% +
+
+
+
+
+ +
+
RECENT SETTLED
+
+
+
Jokić o27.5 PTS
DEN · settled 30 · CLV +2.4%
+
AHIT
+
+
+
Judge o1.5 TB
NYY · settled 2 · CLV +1.1%
+
BHIT
+
+
+
Edwards o5.5 REB
MIN · settled 4 · CLV −0.8%
+
CMISS
+
+
+
Skenes o6.5 K
PIT · settled 8 · CLV +4.0%
+
A+HIT
+
+
+
+
+ + +
+
+ TIER RECORD · CALIBRATION + SEASON · ALL SPORTS +
+
+
A+24–6
+31% ROI
+
A41–19
+18% ROI
+
B58–42
+6% ROI
+
C30–28
+1% ROI
+
D12–20
FADE ✓
+
+
+
+
+ + +
+
+ SHARE CARDS + STOP-THE-SCROLL · TIMELINE IS CUSTOMER #1 +
+
+ +
+ + +
+
THE SETTLE · 1080 × 1350
+
+
+
+ + + VYNDR + + JUL 11 +
+
+
DAILY SETTLE
+
+ +5.4u +
+
7–2 · 78% HIT · 9 GRADED
+
+
+
+ Jokić o27.5 PTS + A+HIT +
+
+ Skenes o6.5 K + AHIT +
+
+ Edwards o5.5 REB + CMISS +
+
+ Judge o1.5 TB + BHIT +
+
+
+ @edgehunter + CLV-VERIFIED +
+
+
+
+ + +
+
GRADE REVEAL · 1200 × 630 OG
+
+
+
+ + + VYNDR + + CLV-VERIFIED +
+
+
+ A+ + TIER +
+
+
Nikola Jokić
+
Over 27.5 Points
+
+
EDGE
+8.4%
+
L10
8/10
+
DK
−114
+
+
+
+
vyndr.io/u/edgehunter
+
+
+
+
+
+ + + +
+ 04 +
THE INTELLIGENCE
One analysis, many grades — style matchups, arsenal reads, correlation math.
+
+ +
+
+ STYLE MATCHUP + COMBAT · STYLES MAKE FIGHTS · FIGHT-WEEK WEAPON +
+
+ +
+
+ UFC 312 · LIGHTWEIGHT · 5 RND MAIN + CLV-VERIFIED READ +
+ +
+ +
+
+
+
IM
+ +
+
Islam M.
26–1 · ORTHODOX · 70" REACH
+
+
GRAPPLER80%
+
STRIKER30%
+
+ COMBAT SAMBO + DAGESTAN WRESTLING + BJJ BROWN +
+
+ + +
+
VERDICT
+
VS
+
GRAPPLER EDGE
+
+ + +
+
+
+
AV
+ +
+
Alex V.
26–3 · ORTHODOX · 71.5" REACH
+
+
PRESSURE72%
+
TECHNICIAN44%
+
+ KICKBOXING + ELITE TDD 94% + ?WRESTLING BASE +
+
+
+ + +
+

Takedown defense is the whole fight. Islam's chain-wrestling beats a pure striker — but Volk's 94% TDD is the one variable that flips it.

+
+ RECENT-FORM OVERRIDE + Volk stuffed 11 of 12 TDs last 3 fights → pushes this toward DECISION, not submission. +
+
+
+
MONEYLINE
A-Islam -230
+
DECISION
A+120
+
SUBMISSION
B++280
+
KO / TKO
C+650
+
O 2.5 RND
A--140
+
+
+
+ + +
+
+ SYSTEM SURFACES + LIVE GRADE-SHIFT · THE $44.99 TIER · EMPTY STATE +
+
+ +
+ + +
+
+
LIVE GRADE
+ Q3 4:12 · DEN @ MIN +
+
+
+
+
A+
+
LIVE TIER
+
+
+
Edwards o5.5 AST
+
GRADE FIRMED ▲ 04:12
+
+ PACE + USAGE + MATCHUP +
+
+
+ +
+
GRADE · BY GAME CLOCK+6.8%
+
+
+
+
+
+
+
+
+
+
+
TIPNOW
+
+
+
+ + +
+
+
+
+ VYNDR PRO + FREEPRO +
+
+ $44.99 + /mo +
+

The whole engine — grades, CLV, the board. Free gives you live scores + tonight's top 3 grades — Pro unlocks the whole board.

+
+ Every graded read, every league, every night + CLV-verified public record + share cards + Live grade-shift + steam alerts + Archetype fingerprints + combat style engine +
+ +
cancel anytime · no trial — the free tier is the trial
+
+
+
+ + +
+
+
+
TRANSMISSION QUIET
+
No reads cleared the A-tier threshold yet.
+

The engine graded 312 props tonight — none hit the edge floor. That's the system protecting you, not a bug. Grades refresh as lines move.

+
+ + +
+
+
+
+ + +
+
+ PITCHER IDENTITY + THE ARSENAL READ · ONE IDENTITY → MANY PROPS +
+
+ +
+
+ +
+
+
PIT
+
+
Paul SkenesCONFIRMED
+
RHP · #30 · Pittsburgh · vs CHC
+
+
+
IDENTITY BLEND
+
+ ALPHA + PUNCHOUT +
+
+
APPROACH · TUNNELER
+

Splinker + slider tunnel off the 99mph 4-seam — hitters commit early and miss. Whiffs cluster the 3rd time through.

+
+
+ + +
+
+ PITCH + VELO + USE + WHIFF +
+
4-Seam99.132%26%
+
Splinker94.224%38%
+
Slider87.022%41%
+
Curve82.514%33%
+
+
+ +
+
STRIKEOUTS O6.5
A+6.1%
+
OUTS O16.5
A-+4.2%
+
EARNED RUNS U2.5
B++2.4%
+
1ST-INNING K
B+1.6%
+
+
+
+ + +
+
+ CORRELATION BUILDER + PARLAY MATH · THE STAKE-DOWN SIGNAL · TAP TO BUILD +
+
+ +
+ +
+
TONIGHT'S A/B READSTAP TO ADD
+
+
+ +
Jokić o27.5 PTS
NBA · DEN @ MIN
+ A+ + +105 +
+
+ +
Edwards o5.5 AST
NBA · DEN @ MIN
+ A + +130 +
+
+ +
Skenes o6.5 K
MLB · PIT @ CHC
+ A + -120 +
+
+ +
Judge o1.5 TB
MLB · NYY @ BOS
+ B + +140 +
+
+
+ + +
+
+ PARLAY SLIP + 2 LEGS +
+
+
+
SAME-GAME CORRELATION
+ +
+
+
COMBINED
+320
+
GRADE
B
+
VYNDR STAKE
0.4u
+
+
+
+
+ +
+
+ COMPONENTS · 74 GLYPHS · EDGE BOARD · /u · SHARE · COMBAT · LIVE GRADE · PRO · STATES + VYNDR VISUAL SYSTEM · v2 +
+
+ +
+
+
+ + + diff --git a/specs/design-reference/assets/glyphs/MANIFEST.md b/specs/design-reference/assets/glyphs/MANIFEST.md index 4c790ae..936e5ce 100644 --- a/specs/design-reference/assets/glyphs/MANIFEST.md +++ b/specs/design-reference/assets/glyphs/MANIFEST.md @@ -86,4 +86,8 @@ | SWITCHBOARD | `#90A0E8` | `assets/glyphs/switchboard.svg` | | RETURNER | `#6C9CB0` | `assets/glyphs/returner.svg` | -Marks use `currentColor`. 74 display names + 9 classifier-legacy aliases (never user-facing). +Marks use `currentColor`. 74 display names + 9 classifier-legacy aliases (BRUSH, WHIFF, CONNECTOR, DISTRIBUTOR, FASTBREAK, FLEX, HYBRID, SWITCH, SWITCHBOARD) — legacy names are classifier-side only, never displayed; map them to renamed display archetypes (BRUSH→SLASH etc.) or render their own mark where the backend emits the legacy key. + + +## SESSION 2 NOTE (Jul 17 2026) +No new marks. Surfaces 2-5 reuse the 83 existing marks only (hero-scale usage in article media: render the SVG at up to 72px, duotone unchanged). New Session 2 assets are chart/viz templates and book wordmark chips, defined in `Vyndr Intelligence.dc.html` — not glyph assets. diff --git a/specs/design-reference/assets/glyphs/aggressor.svg b/specs/design-reference/assets/glyphs/aggressor.svg index 103c694..703215a 100644 --- a/specs/design-reference/assets/glyphs/aggressor.svg +++ b/specs/design-reference/assets/glyphs/aggressor.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/alpha.svg b/specs/design-reference/assets/glyphs/alpha.svg index 20854cc..c325330 100644 --- a/specs/design-reference/assets/glyphs/alpha.svg +++ b/specs/design-reference/assets/glyphs/alpha.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/anchor.svg b/specs/design-reference/assets/glyphs/anchor.svg index ae30bed..c0e8109 100644 --- a/specs/design-reference/assets/glyphs/anchor.svg +++ b/specs/design-reference/assets/glyphs/anchor.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/architect.svg b/specs/design-reference/assets/glyphs/architect.svg index 59f6bcc..767a6ad 100644 --- a/specs/design-reference/assets/glyphs/architect.svg +++ b/specs/design-reference/assets/glyphs/architect.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/artillery.svg b/specs/design-reference/assets/glyphs/artillery.svg index ad378fb..cbc1f81 100644 --- a/specs/design-reference/assets/glyphs/artillery.svg +++ b/specs/design-reference/assets/glyphs/artillery.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/bell-cow.svg b/specs/design-reference/assets/glyphs/bell-cow.svg index c791698..15c1f49 100644 --- a/specs/design-reference/assets/glyphs/bell-cow.svg +++ b/specs/design-reference/assets/glyphs/bell-cow.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/blade.svg b/specs/design-reference/assets/glyphs/blade.svg index 0bb437e..eaa953a 100644 --- a/specs/design-reference/assets/glyphs/blade.svg +++ b/specs/design-reference/assets/glyphs/blade.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/bolt.svg b/specs/design-reference/assets/glyphs/bolt.svg index 73eb1df..9d1b113 100644 --- a/specs/design-reference/assets/glyphs/bolt.svg +++ b/specs/design-reference/assets/glyphs/bolt.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/bomber.svg b/specs/design-reference/assets/glyphs/bomber.svg index 234d2d3..b665f83 100644 --- a/specs/design-reference/assets/glyphs/bomber.svg +++ b/specs/design-reference/assets/glyphs/bomber.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/bridge.svg b/specs/design-reference/assets/glyphs/bridge.svg index 823da2b..c6d3d9c 100644 --- a/specs/design-reference/assets/glyphs/bridge.svg +++ b/specs/design-reference/assets/glyphs/bridge.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/bruiser.svg b/specs/design-reference/assets/glyphs/bruiser.svg index d5e2e42..d5663e8 100644 --- a/specs/design-reference/assets/glyphs/bruiser.svg +++ b/specs/design-reference/assets/glyphs/bruiser.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/brush.svg b/specs/design-reference/assets/glyphs/brush.svg index 683b5a6..82c8b6c 100644 --- a/specs/design-reference/assets/glyphs/brush.svg +++ b/specs/design-reference/assets/glyphs/brush.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/burner.svg b/specs/design-reference/assets/glyphs/burner.svg index d2d959e..2c57b78 100644 --- a/specs/design-reference/assets/glyphs/burner.svg +++ b/specs/design-reference/assets/glyphs/burner.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/cannon.svg b/specs/design-reference/assets/glyphs/cannon.svg index 53c2cf2..671485b 100644 --- a/specs/design-reference/assets/glyphs/cannon.svg +++ b/specs/design-reference/assets/glyphs/cannon.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/catalyst.svg b/specs/design-reference/assets/glyphs/catalyst.svg index 926e27c..756452f 100644 --- a/specs/design-reference/assets/glyphs/catalyst.svg +++ b/specs/design-reference/assets/glyphs/catalyst.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/centerfield.svg b/specs/design-reference/assets/glyphs/centerfield.svg index 7f06278..0f15da3 100644 --- a/specs/design-reference/assets/glyphs/centerfield.svg +++ b/specs/design-reference/assets/glyphs/centerfield.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/chainmover.svg b/specs/design-reference/assets/glyphs/chainmover.svg index 979f44b..29c1720 100644 --- a/specs/design-reference/assets/glyphs/chainmover.svg +++ b/specs/design-reference/assets/glyphs/chainmover.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/conductor.svg b/specs/design-reference/assets/glyphs/conductor.svg index 4e63540..6d8da3d 100644 --- a/specs/design-reference/assets/glyphs/conductor.svg +++ b/specs/design-reference/assets/glyphs/conductor.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/connector.svg b/specs/design-reference/assets/glyphs/connector.svg index 92b927b..b176226 100644 --- a/specs/design-reference/assets/glyphs/connector.svg +++ b/specs/design-reference/assets/glyphs/connector.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/counter.svg b/specs/design-reference/assets/glyphs/counter.svg index de1d2a9..86e3bd0 100644 --- a/specs/design-reference/assets/glyphs/counter.svg +++ b/specs/design-reference/assets/glyphs/counter.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/distributor.svg b/specs/design-reference/assets/glyphs/distributor.svg index bef6c00..59f46e1 100644 --- a/specs/design-reference/assets/glyphs/distributor.svg +++ b/specs/design-reference/assets/glyphs/distributor.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/driver.svg b/specs/design-reference/assets/glyphs/driver.svg index a58ec6f..294d961 100644 --- a/specs/design-reference/assets/glyphs/driver.svg +++ b/specs/design-reference/assets/glyphs/driver.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/enforcer.svg b/specs/design-reference/assets/glyphs/enforcer.svg index c9a9abe..6407d56 100644 --- a/specs/design-reference/assets/glyphs/enforcer.svg +++ b/specs/design-reference/assets/glyphs/enforcer.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/engine.svg b/specs/design-reference/assets/glyphs/engine.svg index c407c11..4f6eb3b 100644 --- a/specs/design-reference/assets/glyphs/engine.svg +++ b/specs/design-reference/assets/glyphs/engine.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/fastbreak.svg b/specs/design-reference/assets/glyphs/fastbreak.svg index f4b1fb1..def6df6 100644 --- a/specs/design-reference/assets/glyphs/fastbreak.svg +++ b/specs/design-reference/assets/glyphs/fastbreak.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/finisher.svg b/specs/design-reference/assets/glyphs/finisher.svg index 07d8e90..f082efe 100644 --- a/specs/design-reference/assets/glyphs/finisher.svg +++ b/specs/design-reference/assets/glyphs/finisher.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/flex.svg b/specs/design-reference/assets/glyphs/flex.svg index bae259f..ae983b5 100644 --- a/specs/design-reference/assets/glyphs/flex.svg +++ b/specs/design-reference/assets/glyphs/flex.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/fortress.svg b/specs/design-reference/assets/glyphs/fortress.svg index b05edef..9282ddc 100644 --- a/specs/design-reference/assets/glyphs/fortress.svg +++ b/specs/design-reference/assets/glyphs/fortress.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/ghost.svg b/specs/design-reference/assets/glyphs/ghost.svg index 3bcdaf4..820e067 100644 --- a/specs/design-reference/assets/glyphs/ghost.svg +++ b/specs/design-reference/assets/glyphs/ghost.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/glue.svg b/specs/design-reference/assets/glyphs/glue.svg index cf20c9a..1bfa9be 100644 --- a/specs/design-reference/assets/glyphs/glue.svg +++ b/specs/design-reference/assets/glyphs/glue.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/grappler.svg b/specs/design-reference/assets/glyphs/grappler.svg index fbbef08..5dc2e19 100644 --- a/specs/design-reference/assets/glyphs/grappler.svg +++ b/specs/design-reference/assets/glyphs/grappler.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/grinder.svg b/specs/design-reference/assets/glyphs/grinder.svg index 8568c22..7f1045d 100644 --- a/specs/design-reference/assets/glyphs/grinder.svg +++ b/specs/design-reference/assets/glyphs/grinder.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/hammer.svg b/specs/design-reference/assets/glyphs/hammer.svg index da3ac36..bcc32f5 100644 --- a/specs/design-reference/assets/glyphs/hammer.svg +++ b/specs/design-reference/assets/glyphs/hammer.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/howitzer.svg b/specs/design-reference/assets/glyphs/howitzer.svg index 7d6f2f7..abcff65 100644 --- a/specs/design-reference/assets/glyphs/howitzer.svg +++ b/specs/design-reference/assets/glyphs/howitzer.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/hub.svg b/specs/design-reference/assets/glyphs/hub.svg index f44c212..8c5da26 100644 --- a/specs/design-reference/assets/glyphs/hub.svg +++ b/specs/design-reference/assets/glyphs/hub.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/hunter.svg b/specs/design-reference/assets/glyphs/hunter.svg index c38f6ef..745a0a8 100644 --- a/specs/design-reference/assets/glyphs/hunter.svg +++ b/specs/design-reference/assets/glyphs/hunter.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/hybrid.svg b/specs/design-reference/assets/glyphs/hybrid.svg index 7e76c33..50b0f60 100644 --- a/specs/design-reference/assets/glyphs/hybrid.svg +++ b/specs/design-reference/assets/glyphs/hybrid.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/igniter.svg b/specs/design-reference/assets/glyphs/igniter.svg index 1eb7fc6..3c6da0f 100644 --- a/specs/design-reference/assets/glyphs/igniter.svg +++ b/specs/design-reference/assets/glyphs/igniter.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/iron.svg b/specs/design-reference/assets/glyphs/iron.svg index 889b328..2c87898 100644 --- a/specs/design-reference/assets/glyphs/iron.svg +++ b/specs/design-reference/assets/glyphs/iron.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/island.svg b/specs/design-reference/assets/glyphs/island.svg index 92b6f0e..b601b90 100644 --- a/specs/design-reference/assets/glyphs/island.svg +++ b/specs/design-reference/assets/glyphs/island.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/leg.svg b/specs/design-reference/assets/glyphs/leg.svg index 66fafda..f8bdea3 100644 --- a/specs/design-reference/assets/glyphs/leg.svg +++ b/specs/design-reference/assets/glyphs/leg.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/lightning.svg b/specs/design-reference/assets/glyphs/lightning.svg index dd061a5..45c78bf 100644 --- a/specs/design-reference/assets/glyphs/lightning.svg +++ b/specs/design-reference/assets/glyphs/lightning.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/link.svg b/specs/design-reference/assets/glyphs/link.svg index dba6361..c9a5113 100644 --- a/specs/design-reference/assets/glyphs/link.svg +++ b/specs/design-reference/assets/glyphs/link.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/lockdown.svg b/specs/design-reference/assets/glyphs/lockdown.svg index a81f3ca..6d2a6e9 100644 --- a/specs/design-reference/assets/glyphs/lockdown.svg +++ b/specs/design-reference/assets/glyphs/lockdown.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/maestro.svg b/specs/design-reference/assets/glyphs/maestro.svg index 3a82b68..16f4bec 100644 --- a/specs/design-reference/assets/glyphs/maestro.svg +++ b/specs/design-reference/assets/glyphs/maestro.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/mirror.svg b/specs/design-reference/assets/glyphs/mirror.svg index 6e11de5..501c356 100644 --- a/specs/design-reference/assets/glyphs/mirror.svg +++ b/specs/design-reference/assets/glyphs/mirror.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/motor.svg b/specs/design-reference/assets/glyphs/motor.svg index 81306de..26ba5fa 100644 --- a/specs/design-reference/assets/glyphs/motor.svg +++ b/specs/design-reference/assets/glyphs/motor.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/pest.svg b/specs/design-reference/assets/glyphs/pest.svg index 0f7cbc2..00ffb84 100644 --- a/specs/design-reference/assets/glyphs/pest.svg +++ b/specs/design-reference/assets/glyphs/pest.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/phantom.svg b/specs/design-reference/assets/glyphs/phantom.svg index 9f6673f..453ae24 100644 --- a/specs/design-reference/assets/glyphs/phantom.svg +++ b/specs/design-reference/assets/glyphs/phantom.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/piston.svg b/specs/design-reference/assets/glyphs/piston.svg index 609cb94..38c0e39 100644 --- a/specs/design-reference/assets/glyphs/piston.svg +++ b/specs/design-reference/assets/glyphs/piston.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/plug.svg b/specs/design-reference/assets/glyphs/plug.svg index ef0c535..bab20a0 100644 --- a/specs/design-reference/assets/glyphs/plug.svg +++ b/specs/design-reference/assets/glyphs/plug.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/pressure.svg b/specs/design-reference/assets/glyphs/pressure.svg index 7d031b2..23a8789 100644 --- a/specs/design-reference/assets/glyphs/pressure.svg +++ b/specs/design-reference/assets/glyphs/pressure.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/prism.svg b/specs/design-reference/assets/glyphs/prism.svg index 9161099..4b08e64 100644 --- a/specs/design-reference/assets/glyphs/prism.svg +++ b/specs/design-reference/assets/glyphs/prism.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/punchout.svg b/specs/design-reference/assets/glyphs/punchout.svg index d8e185e..ffcb221 100644 --- a/specs/design-reference/assets/glyphs/punchout.svg +++ b/specs/design-reference/assets/glyphs/punchout.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/range.svg b/specs/design-reference/assets/glyphs/range.svg index 0e46b32..686e628 100644 --- a/specs/design-reference/assets/glyphs/range.svg +++ b/specs/design-reference/assets/glyphs/range.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/rearguard.svg b/specs/design-reference/assets/glyphs/rearguard.svg index e2c3c1f..6547396 100644 --- a/specs/design-reference/assets/glyphs/rearguard.svg +++ b/specs/design-reference/assets/glyphs/rearguard.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/returner.svg b/specs/design-reference/assets/glyphs/returner.svg index e26b79a..28228b1 100644 --- a/specs/design-reference/assets/glyphs/returner.svg +++ b/specs/design-reference/assets/glyphs/returner.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/seam.svg b/specs/design-reference/assets/glyphs/seam.svg index df2096d..39a9692 100644 --- a/specs/design-reference/assets/glyphs/seam.svg +++ b/specs/design-reference/assets/glyphs/seam.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/sentinel.svg b/specs/design-reference/assets/glyphs/sentinel.svg index 929918d..1897f85 100644 --- a/specs/design-reference/assets/glyphs/sentinel.svg +++ b/specs/design-reference/assets/glyphs/sentinel.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/setup.svg b/specs/design-reference/assets/glyphs/setup.svg index 0cf2449..47e9606 100644 --- a/specs/design-reference/assets/glyphs/setup.svg +++ b/specs/design-reference/assets/glyphs/setup.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/shield.svg b/specs/design-reference/assets/glyphs/shield.svg index 0b4dded..13ce1ec 100644 --- a/specs/design-reference/assets/glyphs/shield.svg +++ b/specs/design-reference/assets/glyphs/shield.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/sinker.svg b/specs/design-reference/assets/glyphs/sinker.svg index f8be2c2..d4cebf1 100644 --- a/specs/design-reference/assets/glyphs/sinker.svg +++ b/specs/design-reference/assets/glyphs/sinker.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/slash.svg b/specs/design-reference/assets/glyphs/slash.svg index 406fa3d..c9319a2 100644 --- a/specs/design-reference/assets/glyphs/slash.svg +++ b/specs/design-reference/assets/glyphs/slash.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/slugger.svg b/specs/design-reference/assets/glyphs/slugger.svg index 6ff31b4..15c5fea 100644 --- a/specs/design-reference/assets/glyphs/slugger.svg +++ b/specs/design-reference/assets/glyphs/slugger.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/sniper.svg b/specs/design-reference/assets/glyphs/sniper.svg index 1997069..2f45253 100644 --- a/specs/design-reference/assets/glyphs/sniper.svg +++ b/specs/design-reference/assets/glyphs/sniper.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/southpaw.svg b/specs/design-reference/assets/glyphs/southpaw.svg index e5a73d0..1d9e32f 100644 --- a/specs/design-reference/assets/glyphs/southpaw.svg +++ b/specs/design-reference/assets/glyphs/southpaw.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/spark.svg b/specs/design-reference/assets/glyphs/spark.svg index da0a8bc..becfc59 100644 --- a/specs/design-reference/assets/glyphs/spark.svg +++ b/specs/design-reference/assets/glyphs/spark.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/striker.svg b/specs/design-reference/assets/glyphs/striker.svg index 543e436..dac37d5 100644 --- a/specs/design-reference/assets/glyphs/striker.svg +++ b/specs/design-reference/assets/glyphs/striker.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/surge.svg b/specs/design-reference/assets/glyphs/surge.svg index 2fd5744..4e7ef51 100644 --- a/specs/design-reference/assets/glyphs/surge.svg +++ b/specs/design-reference/assets/glyphs/surge.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/surgeon.svg b/specs/design-reference/assets/glyphs/surgeon.svg index 0f09063..a533cee 100644 --- a/specs/design-reference/assets/glyphs/surgeon.svg +++ b/specs/design-reference/assets/glyphs/surgeon.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/swiss.svg b/specs/design-reference/assets/glyphs/swiss.svg index 38ee1fc..91c860a 100644 --- a/specs/design-reference/assets/glyphs/swiss.svg +++ b/specs/design-reference/assets/glyphs/swiss.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/switch.svg b/specs/design-reference/assets/glyphs/switch.svg index 52ff040..6a19a75 100644 --- a/specs/design-reference/assets/glyphs/switch.svg +++ b/specs/design-reference/assets/glyphs/switch.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/switchboard.svg b/specs/design-reference/assets/glyphs/switchboard.svg index 14e1159..b098b08 100644 --- a/specs/design-reference/assets/glyphs/switchboard.svg +++ b/specs/design-reference/assets/glyphs/switchboard.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/technician.svg b/specs/design-reference/assets/glyphs/technician.svg index ca8e6c3..9725b71 100644 --- a/specs/design-reference/assets/glyphs/technician.svg +++ b/specs/design-reference/assets/glyphs/technician.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/topspin.svg b/specs/design-reference/assets/glyphs/topspin.svg index 52b7747..84a45c8 100644 --- a/specs/design-reference/assets/glyphs/topspin.svg +++ b/specs/design-reference/assets/glyphs/topspin.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/torch.svg b/specs/design-reference/assets/glyphs/torch.svg index c5e7f41..8037f7b 100644 --- a/specs/design-reference/assets/glyphs/torch.svg +++ b/specs/design-reference/assets/glyphs/torch.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/tower.svg b/specs/design-reference/assets/glyphs/tower.svg index 53e0352..8ed6083 100644 --- a/specs/design-reference/assets/glyphs/tower.svg +++ b/specs/design-reference/assets/glyphs/tower.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/unicorn.svg b/specs/design-reference/assets/glyphs/unicorn.svg index 285ade2..3bd72be 100644 --- a/specs/design-reference/assets/glyphs/unicorn.svg +++ b/specs/design-reference/assets/glyphs/unicorn.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/vault.svg b/specs/design-reference/assets/glyphs/vault.svg index df160db..646beea 100644 --- a/specs/design-reference/assets/glyphs/vault.svg +++ b/specs/design-reference/assets/glyphs/vault.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/wall.svg b/specs/design-reference/assets/glyphs/wall.svg index 37b27e8..d3dcbf3 100644 --- a/specs/design-reference/assets/glyphs/wall.svg +++ b/specs/design-reference/assets/glyphs/wall.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/wedge.svg b/specs/design-reference/assets/glyphs/wedge.svg index 80eb933..174a640 100644 --- a/specs/design-reference/assets/glyphs/wedge.svg +++ b/specs/design-reference/assets/glyphs/wedge.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/whiff.svg b/specs/design-reference/assets/glyphs/whiff.svg index ed61c1b..576497f 100644 --- a/specs/design-reference/assets/glyphs/whiff.svg +++ b/specs/design-reference/assets/glyphs/whiff.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/assets/glyphs/workhorse.svg b/specs/design-reference/assets/glyphs/workhorse.svg index 753f9fb..853b19d 100644 --- a/specs/design-reference/assets/glyphs/workhorse.svg +++ b/specs/design-reference/assets/glyphs/workhorse.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/specs/design-reference/exports/article-og-1200x630.png b/specs/design-reference/exports/article-og-1200x630.png new file mode 100644 index 0000000..f2265ae Binary files /dev/null and b/specs/design-reference/exports/article-og-1200x630.png differ diff --git a/specs/design-reference/exports/grade-reveal-og.png b/specs/design-reference/exports/grade-reveal-og.png new file mode 100644 index 0000000..2b26e79 Binary files /dev/null and b/specs/design-reference/exports/grade-reveal-og.png differ diff --git a/specs/design-reference/exports/record-30d-1080x1350.png b/specs/design-reference/exports/record-30d-1080x1350.png new file mode 100644 index 0000000..0fd10da Binary files /dev/null and b/specs/design-reference/exports/record-30d-1080x1350.png differ diff --git a/specs/design-reference/exports/reveal-xcard-1200x675.png b/specs/design-reference/exports/reveal-xcard-1200x675.png new file mode 100644 index 0000000..7418b39 Binary files /dev/null and b/specs/design-reference/exports/reveal-xcard-1200x675.png differ diff --git a/specs/design-reference/exports/settle-1080x1350.png b/specs/design-reference/exports/settle-1080x1350.png new file mode 100644 index 0000000..0d5036e Binary files /dev/null and b/specs/design-reference/exports/settle-1080x1350.png differ diff --git a/specs/design-reference/exports/settle-square-1080.png b/specs/design-reference/exports/settle-square-1080.png new file mode 100644 index 0000000..8a4bef1 Binary files /dev/null and b/specs/design-reference/exports/settle-square-1080.png differ diff --git a/specs/design-reference/exports/settle-story-1080x1920.png b/specs/design-reference/exports/settle-story-1080x1920.png new file mode 100644 index 0000000..91a36de Binary files /dev/null and b/specs/design-reference/exports/settle-story-1080x1920.png differ diff --git a/specs/design-reference/image-slot.js b/specs/design-reference/image-slot.js new file mode 100644 index 0000000..f27a9ba --- /dev/null +++ b/specs/design-reference/image-slot.js @@ -0,0 +1,1197 @@ +// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design) +// Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected). +/* BEGIN USAGE */ +/** + * — user-fillable image placeholder. + * + * Drop this into a deck, mockup, or page wherever a design needs an image. + * You control the slot's shape; it sizes to its container by default. When the search_stock_photos tool + * is available, prefill the slot by default — write the photo's URL into + * src (with credit/credit-href); the user can still fill or replace it + * by dragging an image file onto it (or clicking to browse). The dropped + * image persists across reloads via a .image-slots.state.json sidecar — + * same read-via-fetch / write-via-window.omelette pattern as + * design_canvas.jsx, so the filled slot shows on share links, downloaded + * zips, and PPTX export. Outside the omelette runtime the slot is read-only. + * + * The sidecar is a SIBLING of the HTML file that uses this component: the + * read is a document-relative fetch, and the host resolves the bridge's + * sidecar writes into the previewed file's directory to match (same + * contract as design_canvas.jsx). Pages in the same directory share one + * sidecar; keep slot ids distinct across them. + * + * Attributes: + * id Persistence key. REQUIRED for the drop to survive reload — + * every slot on the page needs a distinct id. + * shape 'rect' | 'rounded' | 'circle' | 'pill' (default 'rounded') + * 'circle' applies 50% border-radius; on a non-square slot + * that's an ellipse — set equal width and height for a true + * circle. + * radius Corner radius in px for 'rounded'. (default 12) + * mask Any CSS clip-path value. Overrides `shape` — use this for + * hexagons, blobs, arbitrary polygons. + * fit Initial framing baseline: cover | contain. (default 'cover') + * cover starts the image filling the frame (overflow cropped); + * contain starts it fully visible (letterboxed). Either way the + * user can always pan/scale from there — double-click, or the + * Edit control, enters reframe mode (drag to move, scroll or + * corner-handles to scale; Escape / click-out commits). The + * crop persists alongside the image in the sidecar. + * placeholder Empty-state caption. (default 'Drop an image') + * src Optional initial/fallback image URL. Prefill it with a real + * photo via search_stock_photos when that tool is available + * (set credit/credit-href from the result). A user drop + * overrides it; clearing the drop reveals src again. + * credit Attribution text shown as a small overlay at the + * bottom-left of the filled slot. REQUIRED whenever src + * points at any Unsplash host (images.unsplash.com, + * plus.unsplash.com, …): an Unsplash src with no credit + * renders an error tile INSTEAD of the photo (Unsplash + * terms forbid showing their photos unattributed). Use the + * exact form 'Photo by {photographer name} on Unsplash' — + * the overlay then links the name to credit-href and + * 'Unsplash' to the Unsplash homepage, and links back to + * unsplash.com automatically get the required utm referral + * params appended at render time. The credit belongs to + * the src image, so it only shows while src is what's + * displayed — a user-dropped image hides it. + * credit-href Link for the photographer's name in the credit overlay + * (their Unsplash profile URL from the stock-photo search + * results). http(s) URLs only — anything else renders the + * name as plain text. + * + * Sizing: the slot fills its container by default (width/height 100%). + * Put it in a sized wrapper — absolutely positioned, a grid cell, a fixed + * frame — and it takes exactly that box. When the parent's height is + * indefinite (ordinary flow), it falls back to full width at a 3:2 aspect + * ratio instead of collapsing. In a shrink-to-fit parent (a float, + * width:max-content, an unsized absolute wrapper), percentages have + * nothing to resolve against — size the slot or its wrapper explicitly + * there. For a fixed-size slot, set + * width/height on the element itself (inline style), which overrides the + * default. When + * layering content above a slot (full-bleed layouts), make the overlay + * click-through — pointer-events: none on scrims/text plates, re-enabled + * on interactive children — so the slot's hover controls stay reachable. + * Keep the slot's bottom-left corner visually clear as well: the credit + * overlay renders there, and a dark fade or text plate covering it hides + * the attribution Unsplash's terms require — end the fade above that + * corner, or keep it nearly transparent where the credit sits. + * + * Usage: + *
+ * + *
+ * + * + * + */ +/* END USAGE */ + +(() => { + const STATE_FILE = '.image-slots.state.json'; + + // Unsplash terms require visible attribution wherever their photos + // display, and every link back to unsplash.com must carry utm referral + // params. Two render-time rules enforce that here: + // - an Unsplash-src slot with NO credit attribute renders an error + // tile INSTEAD of the photo (an uncredited Unsplash photo on screen + // is itself the terms violation, so it never renders bare); + // - rendered credit links pointing at unsplash.com get the referral + // params appended when absent (credit-href values live in page + // content that can't be edited after the fact). + // Keep the utm_source value in sync with UTM_SOURCE in + // platform/web-agent/unsplash.ts — this file is a project-local + // artifact and cannot import it (equality is pinned by tests). + const UNSPLASH_HOMEPAGE_HREF = + 'https://unsplash.com/?utm_source=claude_design&utm_medium=referral'; + // Host rule mirrors the hotlink validator that admits Unsplash srcs into + // pages in the first place (cdn$ in unsplash.ts: apex or any subdomain) + // — Unsplash+ results serve from plus.unsplash.com, not just images.*, + // and an admitted-but-uncredited photo must error whatever unsplash + // host it rides on. + // Trailing-dot FQDNs (images.unsplash.com.) are the same host to the + // browser but would miss the regex — strip one dot so the check fails + // CLOSED (unrecognized-but-real Unsplash srcs must error, not render). + const isUnsplashHost = (u) => { + try { + return /(^|\.)unsplash\.com$/.test( + new URL(u, document.baseURI).hostname.replace(/\.$/, '') + ); + } catch { + return false; + } + }; + // Render-time referral normalization for links back to Unsplash: + // appends utm_source/utm_medium when absent, preserves every existing + // query param, never overwrites an existing utm_source, and passes + // non-Unsplash URLs through untouched. Input is an ABSOLUTE validated + // http(s) URL (the credit render funnel resolves + validates first). + const withReferral = (href) => { + try { + const u = new URL(href); + if (!/(^|\.)unsplash\.com$/.test(u.hostname.replace(/\.$/, ''))) { + return href; + } + if (!u.searchParams.has('utm_source')) { + u.searchParams.set('utm_source', 'claude_design'); + } + if (!u.searchParams.has('utm_medium')) { + u.searchParams.set('utm_medium', 'referral'); + } + return u.toString(); + } catch (e) { + return href; + } + }; + // 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the + // sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB. + const MAX_DIM = 1200; + // Raster formats only. SVG is excluded (can carry script; createImageBitmap + // on SVG blobs is inconsistent). GIF is excluded because the canvas + // re-encode keeps only the first frame, so an animated GIF would silently + // go still — better to reject than surprise. + const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif']; + + // ── Shared sidecar store ──────────────────────────────────────────────── + // One fetch + immediate write-on-change for every on the + // page. Reads via fetch() so viewing works anywhere the HTML and sidecar + // are served together; writes go through window.omelette.writeFile, which + // the host allowlists to *.state.json basenames only. + const subs = new Set(); + let slots = {}; + // ids explicitly cleared before the sidecar fetch resolved — otherwise + // the merge below can't tell "never set" from "just deleted" and would + // resurrect the sidecar's stale value. + const tombstones = new Set(); + let loaded = false; + let loadP = null; + + function load() { + if (loadP) return loadP; + loadP = fetch(STATE_FILE) + .then((r) => (r.ok ? r.json() : null)) + .then((j) => { + // Merge: sidecar loses to any in-memory change that raced ahead of + // the fetch (drop or clear) so neither is clobbered by hydration. + if (j && typeof j === 'object') { + const merged = Object.assign({}, j, slots); + // A framing-only write that raced ahead of hydration must not + // drop a user image that's only on disk — inherit u from the + // sidecar for any in-memory entry that lacks one. + for (const k in slots) { + if (merged[k] && !merged[k].u && j[k]) { + merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u; + } + } + for (const id of tombstones) delete merged[id]; + slots = merged; + } + tombstones.clear(); + }) + .catch(() => {}) + .then(() => { loaded = true; subs.forEach((fn) => fn()); }); + return loadP; + } + + // Serialize writes so two near-simultaneous drops on different slots + // can't reorder at the backend and leave the sidecar with only the + // first. A save requested mid-flight just marks dirty and re-fires on + // completion with the then-current slots. + let saving = false; + let saveDirty = false; + // Unload-time flush: save()'s serialization defers a mid-RTT re-fire to a + // .then that never runs in an unloading document, silently dropping a + // pagehide commit. Post the current slots immediately instead — content + // is a superset snapshot of any in-flight save's, the write is a + // whole-file last-writer-wins replace, and postMessage FIFO delivers it + // to the host after the in-flight one, so a backend-side reorder at + // worst reproduces the dropped-commit outcome this flush improves on. + // Guarded on the initial sidecar read: pre-hydration slots can miss + // other slots' persisted entries, and flushing it would clobber them — + // that narrow case stays best-effort (the in-memory merge in load() + // cannot happen in an unloading document anyway). + function flushNow() { + if (!loaded) return; + const w = window.omelette && window.omelette.writeFile; + if (!w) return; + try { Promise.resolve(w(STATE_FILE, JSON.stringify(slots))).catch(() => {}); } catch (e) {} + } + function save() { + if (saving) { saveDirty = true; return; } + const w = window.omelette && window.omelette.writeFile; + if (!w) return; + saving = true; + Promise.resolve(w(STATE_FILE, JSON.stringify(slots))) + .catch(() => {}) + .then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } }); + } + + const S_MAX = 5; + const clampS = (s) => Math.max(1, Math.min(S_MAX, s)); + + // Normalize a stored slot value. Pre-reframe sidecars stored a bare + // data-URL string; newer ones store {u, s, x, y}. Either shape is valid. + function getSlot(id) { + const v = slots[id]; + if (!v) return null; + return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v; + } + + function setSlot(id, val) { + if (!id) return; + if (val) { slots[id] = val; tombstones.delete(id); } + else { delete slots[id]; if (!loaded) tombstones.add(id); } + subs.forEach((fn) => fn()); + // A drop is rare + high-value — write immediately so nav-away can't lose + // it. Gate on the initial read so we don't overwrite a sidecar we haven't + // merged yet; the merge in load() keeps this change once the read lands. + if (loaded) save(); else load().then(save); + } + + // ── Image downscale ───────────────────────────────────────────────────── + // Encode through a canvas so the sidecar carries resized bytes, not the + // raw upload. Longest side is capped at 2× the slot's rendered width + // (retina) and at MAX_DIM. WebP keeps alpha and is ~10× smaller than PNG + // for photos, so there's no need for per-image format picking. + async function toDataUrl(file, targetW) { + const bitmap = await createImageBitmap(file); + try { + const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM); + const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height)); + const w = Math.max(1, Math.round(bitmap.width * scale)); + const h = Math.max(1, Math.round(bitmap.height * scale)); + const canvas = document.createElement('canvas'); + canvas.width = w; canvas.height = h; + canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h); + return canvas.toDataURL('image/webp', 0.85); + } finally { + bitmap.close && bitmap.close(); + } + } + + // ── Custom element ────────────────────────────────────────────────────── + const stylesheet = + // Fill the container by default: slots are usually placed inside a + // sized wrapper (a hero frame, a grid cell, an inset:0 layer) and are + // expected to take that box — a fixed intrinsic size would render as + // a small tile in the corner of a full-bleed wrapper instead. + // aspect-ratio is the companion fallback that keeps a bare slot + // visible when the parent's height is indefinite: height:100% + // resolves to auto there, and the ratio then derives height from + // width instead of letting the slot collapse to zero height. + // Explicit width/height on the element override all of this. + ':host{display:block;position:relative;' + + ' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);' + + ' width:100%;height:100%;aspect-ratio:3/2}' + + '.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' + + // .frame img (clipped) and .spill (unclipped ghost + handles) share the + // same left/top/width/height in frame-%, computed by _applyView(), so the + // inside-mask crop and the outside-mask spill stay pixel-aligned. + '.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' + + ' -webkit-user-drag:none;user-select:none;touch-action:none}' + + // Reframe mode (double-click): the full image spills past the mask. The + // spill layer is sized to the IMAGE bounds so its corners are where the + // resize handles belong. The ghost inside is translucent; the real + // clipped underneath shows the opaque in-mask crop. + // popover=manual promotes the spill to the top layer on reframe, so it is + // not clipped by any overflow:hidden / clip-path / scroll-container + // ancestor (a plain z-index can't escape overflow clipping). UA popover + // defaults (inset:0;margin:auto) are reset; _applyView sets viewport px. + '.spill{position:fixed;margin:0;inset:auto;border:0;padding:0;background:transparent;' + + ' overflow:visible;transform:translate(-50%,-50%);z-index:1;cursor:grab;touch-action:none}' + + ':host([data-panning]) .spill{cursor:grabbing}' + + '.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' + + ' pointer-events:none;-webkit-user-drag:none;user-select:none;' + + ' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' + + '.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' + + ' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' + + ' transform:translate(-50%,-50%)}' + + '.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' + + '.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' + + '.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' + + '.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' + + ':host([data-reframe]){z-index:10}' + + ':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' + + '.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' + + ' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' + + ' cursor:pointer;user-select:none}' + + '.empty svg{opacity:.45}' + + '.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' + + '.empty .sub{font-size:11px}' + + '.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' + + '.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' + + ':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' + + ' background:rgba(201,100,66,.10)}' + + '.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' + + ' transition:border-color .12s}' + + ':host([data-over]) .ring{border-color:#c96442}' + + ':host([data-filled]) .ring{display:none}' + + // Controls overlay INSIDE the frame, pinned to the top-right corner, so + // a full-bleed slot in an overflow:hidden container still shows them + // (the old below-mask placement got clipped). Credit sits bottom-left, + // so top-right avoids collision. The blurred pill background keeps them + // legible over the image. + // The UA [popover] base rule styles the element in EVERY state (only + // display:none is gated on :not(:popover-open), and the display:flex + // below overrides that) — so the UA resets live HERE, like .spill's, + // or the ordinary hover-state strip renders as a bordered Canvas box + // centered by margin:auto. inset:auto precedes top/right (shorthand). + '.ctl{position:absolute;inset:auto;top:8px;right:8px;margin:0;border:0;padding:0;' + + ' background:transparent;overflow:visible;' + + ' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' + + ' white-space:nowrap}' + + // While reframing, the spill owns the top layer and would swallow every + // click on the in-frame controls. Promoting .ctl into the top layer + // ABOVE the spill (shown after it — later popovers stack higher) keeps + // Edit-as-toggle and Replace clickable mid-reframe. _applyView pins it + // to the frame's top-right in viewport px (translateX(-100%) + // right-aligns against the computed left edge); inset:auto clears the + // base rule's top/right so the inline left/top position it alone. + '.ctl:popover-open{position:fixed;inset:auto;transform:translateX(-100%)}' + + ':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' + + ' {opacity:1;pointer-events:auto}' + + '.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' + + ' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' + + ' backdrop-filter:blur(6px)}' + + '.ctl button:hover{background:rgba(0,0,0,.8)}' + + '.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' + + ' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}' + + // Replacement in flight: after a src swap the browser keeps painting + // the PREVIOUS image until the new one decodes, so a Replace would + // flash the old photo and then pop. Hide the stale frame (visibility, + // not display — _applyView geometry still applies) and spin until the + // new image reports in (load/error clears data-swapping). + ':host([data-swapping]) .frame img{visibility:hidden}' + + '.loading{position:absolute;inset:0;display:none;align-items:center;' + + ' justify-content:center;pointer-events:none}' + + ':host([data-swapping]) .loading{display:flex}' + + '.loading::after{content:"";width:22px;height:22px;border-radius:50%;' + + ' border:2px solid rgba(0,0,0,.12);border-top-color:rgba(0,0,0,.45);' + + ' animation:om-slot-spin .7s linear infinite}' + + '@keyframes om-slot-spin{to{transform:rotate(360deg)}}' + + // Reduced motion: the static two-tone ring still reads as "working". + '@media (prefers-reduced-motion:reduce){.loading::after{animation:none}}' + + '.credit{position:absolute;left:6px;bottom:6px;max-width:calc(100% - 12px);display:none;' + + ' padding:3px 7px;border-radius:5px;background:rgba(0,0,0,.55);color:#fff;' + + ' font:10px/1.2 system-ui,-apple-system,sans-serif;text-decoration:none;' + + ' white-space:nowrap;overflow:hidden;text-overflow:ellipsis;backdrop-filter:blur(6px)}' + + // The credit is a SPAN holding one or two s (Unsplash's prescribed + // form links the photographer AND Unsplash) — anchors style inline so + // the overlay reads as one line of text. + '.credit a{color:inherit;text-decoration:none}' + + '.credit a:hover,.credit a:focus-visible{text-decoration:underline}' + + ':host([data-filled][data-credit]) .credit{display:block}' + + // Exports must ship JUST the image — no hover controls, no credit chip + // (the host marks for the capture window; the + // page-level hide script can't reach shadow DOM, this rule can). + ':host-context([data-om-exporting]) .ctl,' + + ':host-context([data-om-exporting]) .credit{display:none !important}' + + // No export-window mask rules here on purpose: the export capture + // releases the replacement mask by REMOVING data-swapping (the + // shadow-root pass in pages/export/shared.ts HIDE_EXPORT_CHROME_SCRIPT) + // — attribute removal works in every engine (:host-context is + // Chromium-only), is scoped by construction to slots actually + // mid-swap, and hides the spinner through the same gate. A masked img + // would otherwise be silently dropped from PPTX decks (the capture + // walk skips visibility:hidden imgs). + // Attribution error tile: REPLACES the photo when an Unsplash src has + // no credit attribute — rendering the photo uncredited is the terms + // violation, so the photo must not appear at all. + // Calm and neutral on purpose (review feedback): the tile informs the + // user; the fix instructions are machine-facing (usage docblock, tool + // description, and the turn-end scan's bounce copy name the attributes + // for the agent). + '.attr-error{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;' + + ' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' + + ' background:#f2f1ef;color:#6e6c66;user-select:none;' + + ' font:13px/1.45 system-ui,-apple-system,sans-serif}' + + '.attr-error svg{opacity:.55}' + + '.attr-error .cap{max-width:92%;font-weight:500;letter-spacing:.01em}' + + ':host([data-attribution-error]) .attr-error{display:flex}' + + ':host([data-attribution-error]) .ring{display:none}'; + + const icon = + '' + + '' + + ''; + + const warnIcon = + '' + + '' + + ''; + + class ImageSlot extends HTMLElement { + static get observedAttributes() { + return ['shape', 'radius', 'mask', 'fit', 'placeholder', 'src', 'id', 'credit', 'credit-href']; + } + + /** Duplicate-slide hook (called by deck-stage, see its + * _remintDuplicateIds): copy this id's stored image, if any, under a + * freshly minted key and return that key — so a duplicated slide's + * slot keeps its dropped photo instead of reverting to the + * placeholder. 'isFree' is the caller's uniqueness check (document + * ids); candidates must ALSO be unused in the sidecar, which can + * hold keys from other pages sharing the project root. (An EMPTY + * slot on another page leaves no sidecar entry, so its id is not + * detectable here — a minted key can collide with it and that slot + * would show this photo. Same blast radius as two pages reusing an + * id by hand, which the shared sidecar already permits.) Returns null + * when no id could be minted (caller strips the id, today's + * behavior). */ + static cloneSlot(fromId, isFree) { + if (typeof fromId !== 'string' || !fromId) return null; + // Pre-hydration the store can't veto candidates or source the copy + // — degrade to the strip (today's behavior) rather than mint + // against keys we can't see yet. Any rendered (= droppable) slot + // means load() has already settled. + if (!loaded) return null; + const stem = fromId.replace(/-\d+$/, '') || fromId; + for (let n = 2; n < 100; n++) { + const toId = stem + '-' + n; + if (toId === fromId) continue; + if (slots[toId] !== undefined) { + // Reuse a key holding this exact value (bytes AND crop) if no + // live element here owns it — a duplicate op the host refused + // after minting leaves such a key behind, and reusing keeps + // refused retries from accumulating one orphaned copy per + // attempt. Full equality (not just bytes) so a byte-identical + // key another PAGE owns with its own crop is stepped past, not + // adopted or rewritten. (Entries without .u never match.) + const prev = getSlot(toId); + const cur = getSlot(fromId); + if (!(prev && cur && prev.u && prev.u === cur.u && + prev.s === cur.s && prev.x === cur.x && prev.y === cur.y && + (typeof isFree !== 'function' || isFree(toId)))) continue; + return toId; + } + if (typeof isFree === 'function' && !isFree(toId)) continue; + const v = getSlot(fromId); + if (v) setSlot(toId, Object.assign({}, v)); + return toId; + } + return null; + } + + constructor() { + super(); + // clonable: rail thumbnails deep-clone slides and carry this shadow + // along; reuse an already-cloned root so upgrade-after-clone works. + // (Deliberately NOT serializable — a getHTML consumer would embed + // multi-MB sidecar data-URLs into serialized page HTML.) + const root = this.shadowRoot || + this.attachShadow({ mode: 'open', clonable: true }); + // .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius + // on the frame (circle, pill, rounded) can't clip them. + root.innerHTML = + '' + + '
' + + ' ' + + '
' + icon + + '
' + + '
or browse files
' + + '
' + warnIcon + + '
This photo needs attribution
' + + '
' + + '
' + + '
' + + // Outside .frame, like .spill/.ctl — the frame's overflow:hidden + + // border-radius/clip-path would cut the credit off on circle/pill/mask. + // A SPAN, not an
: the prescribed Unsplash credit holds two links + // (photographer + Unsplash), built per-render in _render(). + '' + + '
' + + ' ' + + '
' + + '
' + + '
' + + // data-dc-edit-transparent: the DC editor's edit-mode picker lets + // clicks through for chrome marked with it (EDIT_TRANSPARENT_SEL) + // — without it, Replace/Edit clicks in Edit mode are swallowed by + // element selection and the controls look dead. + '
' + + '
' + + ''; + this._frame = root.querySelector('.frame'); + this._ring = root.querySelector('.ring'); + this._img = root.querySelector('.frame img'); + this._empty = root.querySelector('.empty'); + this._cap = root.querySelector('.cap'); + this._sub = root.querySelector('.sub'); + this._spill = root.querySelector('.spill'); + this._ctl = root.querySelector('.ctl'); + this._credit = root.querySelector('.credit'); + this._attrError = root.querySelector('.attr-error'); + // Credit clicks open the link, not browse/reframe. + this._credit.addEventListener('click', (e) => e.stopPropagation()); + this._credit.addEventListener('dblclick', (e) => e.stopPropagation()); + this._ghost = root.querySelector('.ghost'); + this._err = null; + this._input = root.querySelector('input'); + this._depth = 0; + this._gen = 0; + // Encode-in-flight marker (the owning _ingest generation): while set, + // the same-src "nothing in flight" clear in _render must not fire — + // the stored value still points at the OLD image until the encode + // lands, so that clear would unmask the stale image mid-replace. + this._swapGen = 0; + // Render-owned swap in flight: set when _render assigns a new src, + // cleared only by the img's own load/error (or the empty branch). + // img.complete CANNOT stand in for this — setting src only QUEUES + // the current-request swap (a microtask), so synchronously after an + // assignment, complete still reports the OLD settled request. The + // pick path does exactly that: the host sets src, credit, and + // credit-href back-to-back in one task, and renders #2/#3 would + // read the stale complete === true and drop the mask one render + // after it was set. + this._loadPending = false; + // See _render's empty branch: a transient attribution-error wipe of a + // showing image must make the follow-up render a replacement (spinner), + // not a first fill (blank frame). + this._hidShowing = false; + this._view = { s: 1, x: 0, y: 0 }; + this._subFn = () => this._render(); + // Shadow-DOM listeners live with the shadow DOM — bound once here so + // disconnect/reconnect (e.g. React remount) doesn't stack handlers. + this._empty.addEventListener('click', () => this._input.click()); + root.addEventListener('click', (e) => { + const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act'); + if (!act) return; + // The hidden controls are opacity-0 but still tabbable — without + // this gate a keyboard user could drive them on a read-only share + // link (mirrors the dblclick handler's editable gate). + if (!this.hasAttribute('data-editable')) return; + if (act === 'replace') { + this._exitReframe(true); + // Host-owned picker (Unsplash modal; it also offers local import). + this.dispatchEvent(new CustomEvent('image-slot:pick', { + bubbles: true, composed: true, detail: { id: this.id || null } + })); + } + if (act === 'edit') { + if (!this._reframes()) return; + if (this.hasAttribute('data-reframe')) this._exitReframe(true); + else this._enterReframe(); + } + }); + this._input.addEventListener('change', () => { + const f = this._input.files && this._input.files[0]; + if (f) this._ingest(f); + this._input.value = ''; + }); + // naturalWidth/Height aren't known until load — re-apply so the cover + // baseline is computed from real dimensions, not the 100%×100% fallback. + // load/error also release the replacement-in-flight mask (via the + // single discipline in _releaseMask): the swap is only revealed once + // the new image can actually paint (on error the frame shows its + // background, same as a fresh slot with a broken src). + this._img.addEventListener('load', () => { + this._loadPending = false; + this._releaseMask(true); + this._applyView(); + }); + this._img.addEventListener('error', () => { + this._loadPending = false; + this._releaseMask(true); + }); + // Gated only on editable — any filled slot can be repositioned/scaled, + // regardless of fit. Share links (no writeFile) stay static. + this.addEventListener('dblclick', (e) => { + if (!this.hasAttribute('data-editable') || !this._reframes()) return; + e.preventDefault(); + if (this.hasAttribute('data-reframe')) this._exitReframe(true); + else this._enterReframe(); + }); + // Pan + resize both originate on the spill layer. A handle pointerdown + // drives an aspect-locked resize anchored at the opposite corner; any + // other pointerdown on the spill pans. Offsets are frame-% so a + // reframed slot survives responsive resize / PPTX export. + this._spill.addEventListener('pointerdown', (e) => { + if (e.button !== 0 || !this.hasAttribute('data-reframe')) return; + e.preventDefault(); + e.stopPropagation(); + this._spill.setPointerCapture(e.pointerId); + const rect = this.getBoundingClientRect(); + const fw = rect.width || 1, fh = rect.height || 1; + const corner = e.target.getAttribute && e.target.getAttribute('data-c'); + let move; + if (corner) { + // Resize about the OPPOSITE corner. Viewport-px throughout (rect + // fw/fh, not clientWidth) so the math survives a transform:scale() + // ancestor — deck_stage renders slides scaled-to-fit. + const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1; + const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain'; + const base = contain ? Math.min(fw / iw, fh / ih) : Math.max(fw / iw, fh / ih); + const sx = corner.includes('e') ? 1 : -1; + const sy = corner.includes('s') ? 1 : -1; + const s0 = this._view.s; + const w0 = iw * base * s0, h0 = ih * base * s0; + const cx0 = (50 + this._view.x) / 100 * fw; + const cy0 = (50 + this._view.y) / 100 * fh; + const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2; + const diag0 = Math.hypot(w0, h0); + const ux = sx * w0 / diag0, uy = sy * h0 / diag0; + move = (ev) => { + const proj = (ev.clientX - rect.left - ox) * ux + + (ev.clientY - rect.top - oy) * uy; + const s = clampS(s0 * proj / diag0); + const d = diag0 * s / s0; + this._view.s = s; + this._view.x = (ox + ux * d / 2) / fw * 100 - 50; + this._view.y = (oy + uy * d / 2) / fh * 100 - 50; + this._clampView(); + this._applyView(); + }; + } else { + this.setAttribute('data-panning', ''); + const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y }; + move = (ev) => { + this._view.x = start.x + (ev.clientX - start.px) / fw * 100; + this._view.y = start.y + (ev.clientY - start.py) / fh * 100; + this._clampView(); + this._applyView(); + }; + } + const up = () => { + try { this._spill.releasePointerCapture(e.pointerId); } catch {} + this._spill.removeEventListener('pointermove', move); + this._spill.removeEventListener('pointerup', up); + this._spill.removeEventListener('pointercancel', up); + this.removeAttribute('data-panning'); + this._dragUp = null; + }; + // Stashed so _exitReframe (Escape / outside-click mid-drag) can + // tear the capture + listeners down synchronously. + this._dragUp = up; + this._spill.addEventListener('pointermove', move); + this._spill.addEventListener('pointerup', up); + this._spill.addEventListener('pointercancel', up); + }); + // Wheel zoom stays available inside reframe mode as a trackpad nicety — + // zooms toward the cursor (offset' = cursor·(1-k) + offset·k). + this.addEventListener('wheel', (e) => { + if (!this.hasAttribute('data-reframe')) return; + e.preventDefault(); + const r = this.getBoundingClientRect(); + const cx = (e.clientX - r.left) / r.width * 100 - 50; + const cy = (e.clientY - r.top) / r.height * 100 - 50; + const prev = this._view.s; + const next = clampS(prev * Math.pow(1.0015, -e.deltaY)); + if (next === prev) return; + const k = next / prev; + this._view.s = next; + this._view.x = cx * (1 - k) + this._view.x * k; + this._view.y = cy * (1 - k) + this._view.y * k; + this._clampView(); + this._applyView(); + }, { passive: false }); + } + + connectedCallback() { + // Warn once per page — an id-less slot works for the session but + // cannot persist, and two id-less slots would share nothing. + if (!this.id && !ImageSlot._warned) { + ImageSlot._warned = true; + console.warn(' without an id will not persist its dropped image.'); + } + this.addEventListener('dragenter', this); + this.addEventListener('dragover', this); + this.addEventListener('dragleave', this); + this.addEventListener('drop', this); + subs.add(this._subFn); + // The host may inject window.omelette.writeFile AFTER the first render; + // re-render on hover so the editable-gated controls reliably appear. + this.addEventListener('pointerenter', this._subFn); + // width%/height% in _applyView encode the frame aspect at call time — + // a host resize (responsive grid, pane divider) would stretch the + // image until the next _render. Re-render on size change: _render() + // re-seeds _view from stored before clamp/apply, so a shrink→grow + // cycle round-trips instead of ratcheting x/y toward the narrower + // frame's clamp range. + this._ro = new ResizeObserver(() => this._render()); + this._ro.observe(this); + load(); + this._render(); + } + + disconnectedCallback() { + subs.delete(this._subFn); + this.removeEventListener('pointerenter', this._subFn); + this.removeEventListener('dragenter', this); + this.removeEventListener('dragover', this); + this.removeEventListener('dragleave', this); + this.removeEventListener('drop', this); + if (this._ro) { this._ro.disconnect(); this._ro = null; } + // commit=false: a disconnect is not a user intent — committing here + // would persist whatever half-finished drag a React remount or DOM + // splice happened to interrupt. Deliberate exits commit on their own + // paths (Escape/click-out/toggle), and unloads commit via pagehide. + this._exitReframe(false); + } + + _enterReframe() { + if (this.hasAttribute('data-reframe')) return; + this.setAttribute('data-reframe', ''); + this._signalReframe(true); + // Best-effort commit when the document unloads mid-reframe (a host + // navigation racing the enter signal, a manual reload, tab close): + // the sidecar write rides the host bridge, which outlives this + // document, so the crop survives even though the mode dies with the + // DOM. Held on the instance so _exitReframe detaches exactly what + // was attached. + this._pagehide = () => { this._exitReframe(true); flushNow(); }; + window.addEventListener('pagehide', this._pagehide); + // Promote spill to the top layer, then keep it pinned over the frame: + // scroll/resize cover the common cases, and a per-frame rect check + // catches layout shifts that fire neither (an image above finishing + // load, streamed DOM pushing the slot down, an ancestor transform + // change) so the overlay can't detach from the frame. + try { this._spill.showPopover(); } catch {} + // After the spill, so the controls stack above it in the top layer. + try { this._ctl.showPopover(); } catch {} + this._reposition = () => { if (this.hasAttribute('data-reframe')) this._applyView(); }; + window.addEventListener('scroll', this._reposition, true); + window.addEventListener('resize', this._reposition); + this._lastRect = ''; + this._watch = () => { + if (!this.hasAttribute('data-reframe')) return; + const r = this.getBoundingClientRect(); + const key = r.left + ',' + r.top + ',' + r.width + ',' + r.height; + if (key !== this._lastRect) { this._lastRect = key; this._applyView(); } + this._watchId = requestAnimationFrame(this._watch); + }; + this._watchId = requestAnimationFrame(this._watch); + this._applyView(); + // Close on click outside (the spill handler stopPropagation()s so + // in-image drags don't reach this) and on Escape. Listeners are held + // on the instance so _exitReframe / disconnectedCallback can detach + // exactly what was attached. + this._outside = (e) => { + if (e.composedPath && e.composedPath().includes(this)) return; + this._exitReframe(true); + }; + this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); }; + document.addEventListener('pointerdown', this._outside, true); + document.addEventListener('keydown', this._esc, true); + } + + _exitReframe(commit) { + if (!this.hasAttribute('data-reframe')) return; + if (this._dragUp) this._dragUp(); + this.removeAttribute('data-reframe'); + this.removeAttribute('data-panning'); + if (this._outside) document.removeEventListener('pointerdown', this._outside, true); + if (this._esc) document.removeEventListener('keydown', this._esc, true); + this._outside = this._esc = null; + if (this._reposition) { + window.removeEventListener('scroll', this._reposition, true); + window.removeEventListener('resize', this._reposition); + this._reposition = null; + } + if (this._watchId) { cancelAnimationFrame(this._watchId); this._watchId = 0; } + if (this._pagehide) { + window.removeEventListener('pagehide', this._pagehide); + this._pagehide = null; + } + try { this._spill.hidePopover(); } catch {} + try { this._ctl.hidePopover(); } catch {} + this._ctl.style.left = ''; this._ctl.style.top = ''; + if (commit) this._commitView(); + this._signalReframe(false); + } + + // Reframe state lives only in this DOM until commit, invisible to the + // host's dirty signals — announce enter/exit so the host can hold + // auto-reloads for exactly the gesture (the guest bundle forwards + // image-slot:reframe to the host as imageSlotReframe). Dispatched on + // the element (composed, so it escapes shadow roots) while connected; + // a disconnected exit (disconnectedCallback) falls back to document so + // the host still hears it. + _signalReframe(active) { + const target = this.isConnected ? this : document; + target.dispatchEvent(new CustomEvent('image-slot:reframe', { + bubbles: true, composed: true, + detail: { active: active, id: this.id || null } + })); + } + + // Public: host's "Import from computer" calls this to run local browse. + openFilePicker() { this._exitReframe(true); this._input.click(); } + + attributeChangedCallback() { if (this.shadowRoot) this._render(); } + + // handleEvent — one listener object for all four drag events keeps the + // add/remove symmetric and the depth counter correct. + handleEvent(e) { + if (e.type === 'dragenter' || e.type === 'dragover') { + // Without preventDefault the browser never fires 'drop'. + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + if (e.type === 'dragenter') this._depth++; + this.setAttribute('data-over', ''); + } else if (e.type === 'dragleave') { + // dragenter/leave fire for every descendant crossing — count depth + // so hovering the icon inside the empty state doesn't flicker. + if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); } + } else if (e.type === 'drop') { + e.preventDefault(); + e.stopPropagation(); + this._depth = 0; + this.removeAttribute('data-over'); + const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; + if (f) this._ingest(f); + } + } + + async _ingest(file) { + this._setError(null); + if (!file || ACCEPT.indexOf(file.type) < 0) { + this._setError('Drop a PNG, JPEG, WebP, or AVIF image.'); + return; + } + // toDataUrl can take hundreds of ms on a large photo. A Clear or a + // newer drop during that window would be clobbered when this await + // resumes — bump + capture a generation so stale encodes bail. + const gen = ++this._gen; + // Replacing a shown image: surface the swap through the encode too, + // not just the decode — otherwise the old photo sits there with no + // feedback while the canvas re-encode runs. An empty slot keeps its + // placeholder (no spinner) until the encode lands, as before. + // _swapGen guards the mask against re-renders DURING the encode + // (pointerenter, ResizeObserver, another slot's store write): the + // stored value still resolves to the old image there, so _render's + // same-src clear would otherwise unmask it mid-replace. + if (this.hasAttribute('data-filled')) { + this.setAttribute('data-swapping', ''); + this._swapGen = gen; + } + try { + const w = this.clientWidth || this.offsetWidth || MAX_DIM; + const url = await toDataUrl(file, w); + if (gen !== this._gen) return; + // Only exit reframe once the new image is in hand — a rejected type + // or decode failure leaves the in-progress crop untouched. + this._exitReframe(false); + // Clear BEFORE setSlot: its synchronous re-render must see no + // pending encode, so a byte-identical re-upload (same data URL, no + // load event coming) still clears the mask via the complete branch. + this._swapGen = 0; + const val = { u: url, s: 1, x: 0, y: 0 }; + setSlot(this.id || '', val); + // Keep a session-local copy for id-less slots so the drop still + // shows, even though it cannot persist. + if (!this.id) { this._local = val; this._render(); } + } catch (err) { + if (gen !== this._gen) return; + this._swapGen = 0; + // Reveal the kept old image — unless another replacement (a + // remote pick's src swap) is still in flight, in which case the + // mask stays until THAT image settles (its load/error releases). + this._releaseMask(); + this._setError('Could not read that image.'); + console.warn(' ingest failed:', err); + } + } + + _setError(msg) { + if (this._err) { this._err.remove(); this._err = null; } + if (!msg) return; + const d = document.createElement('div'); + d.className = 'err'; d.textContent = msg; + this.shadowRoot.appendChild(d); + this._err = d; + setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000); + } + + // Reframing (pan/resize) is available on any filled slot — the user can + // always reposition/scale. `fit` only sets the initial baseline (see + // _geom): contain starts fully-visible, cover starts frame-filling. + _reframes() { + return this.hasAttribute('data-filled'); + } + + // The single release discipline for the replacement-in-flight mask + // (data-swapping). The mask comes off only when BOTH hold: + // - no encode is pending (_swapGen) — mid-encode the stored value + // still resolves to the old image, so any reveal paints it; + // - the frame img has settled on its current src — an unsettled src + // means some replacement is still in flight (e.g. a remote pick), + // whoever started it, and revealing would paint the previous + // frame. The load/error listeners pass settled=true (the event IS + // the settlement signal, per spec complete is true by then); + // other callers rely on the complete flag (covers loaded AND + // failed). + // Every release path funnels through here EXCEPT _render's empty + // branch (the img is being cleared — nothing will ever settle). + _releaseMask(settled) { + if ( + !this._swapGen && + !this._loadPending && + (settled || this._img.complete) + ) { + this.removeAttribute('data-swapping'); + } + } + + // Baseline geometry, shared by clamp/apply/resize. `base` is the scale at + // view-scale s=1: cover = fill the frame (overflow on the looser axis), + // contain = fit fully inside (letterboxed). Zooming a contain image past + // s where it overflows naturally becomes a crop. Null until the img has + // loaded (naturalWidth is 0 before that) or when the slot has no layout + // box — ResizeObserver fires with a 0×0 rect under display:none, and + // clamping against a degenerate 1×1 frame would silently pull the stored + // pan toward zero. + _geom() { + const iw = this._img.naturalWidth, ih = this._img.naturalHeight; + const fw = this.clientWidth, fh = this.clientHeight; + if (!iw || !ih || !fw || !fh) return null; + const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain'; + const base = contain + ? Math.min(fw / iw, fh / ih) + : Math.max(fw / iw, fh / ih); + return { iw, ih, fw, fh, base }; + } + + _clampView() { + // Pan range on each axis is half the overflow past the frame edge. + const g = this._geom(); + if (!g) return; + const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50); + const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50); + this._view.x = Math.max(-mx, Math.min(mx, this._view.x)); + this._view.y = Math.max(-my, Math.min(my, this._view.y)); + } + + _applyView() { + const g = this._geom(); + // Top-layer controls: pin to the frame's top-right in viewport px + // (the same 8px inset as the in-frame layout; unscaled — top-layer UI + // reads as chrome, not page content). BEFORE the geometry branch: + // placement needs only the frame rect, and a not-yet-loaded or broken + // src must not leave the promoted strip floating unpositioned. Gated + // on the popover actually being open: without the Popover API, + // showPopover() threw (swallowed in _enterReframe), .ctl stays in + // its in-frame absolute layout, and viewport-px coordinates would + // shove it off-frame — and matches(':popover-open') itself throws + // there (unknown pseudo-class), hence the try/catch. + if (this.hasAttribute('data-reframe')) { + let onTop = false; + try { onTop = this._ctl.matches(':popover-open'); } catch {} + if (onTop) { + const r = this.getBoundingClientRect(); + this._ctl.style.left = (r.right - 8) + 'px'; + this._ctl.style.top = (r.top + 8) + 'px'; + } + } + if (!g) { + // Dimensions not known yet (before img load) — centered fit so there + // is no flash of an unpositioned image before the geometry lands. + const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain'; + this._img.style.width = '100%'; + this._img.style.height = '100%'; + this._img.style.left = '50%'; + this._img.style.top = '50%'; + this._img.style.objectFit = contain ? 'contain' : 'cover'; + return; + } + // Baseline (cover-fill or contain-fit) × view scale. Width/height and + // left/top are all frame-% — depends only on the frame aspect ratio, so + // a responsive resize keeps the same crop. The spill layer mirrors the + // same box so its corners = image corners. + const k = g.base * this._view.s; + const w = (g.iw * k / g.fw * 100) + '%'; + const h = (g.ih * k / g.fh * 100) + '%'; + const l = (50 + this._view.x) + '%'; + const t = (50 + this._view.y) + '%'; + this._img.style.width = w; this._img.style.height = h; + this._img.style.left = l; this._img.style.top = t; + this._img.style.objectFit = ''; + if (this.hasAttribute('data-reframe')) { + // Top-layer spill: position in viewport px over the frame. The top + // layer escapes ancestor transforms entirely, so EVERY term must be + // in viewport units: getBoundingClientRect gives the frame's scaled + // origin AND size, and the rect/layout ratio rescales the ghost — + // sizing from layout px alone renders it 1/scale too large under a + // scaled deck slide. Inner ghost + handles stay box-relative. + const r = this.getBoundingClientRect(); + const sx = g.fw ? r.width / g.fw : 1; + const sy = g.fh ? r.height / g.fh : 1; + this._spill.style.width = (g.iw * k * sx) + 'px'; + this._spill.style.height = (g.ih * k * sy) + 'px'; + this._spill.style.left = (r.left + (50 + this._view.x) / 100 * r.width) + 'px'; + this._spill.style.top = (r.top + (50 + this._view.y) / 100 * r.height) + 'px'; + } + } + + _commitView() { + const v = { s: this._view.s, x: this._view.x, y: this._view.y }; + if (this._userUrl) v.u = this._userUrl; + // Framing-only (no u) persists too so an author-src slot remembers its + // crop; clearing the sidecar still falls through to src=. + if (this.id) setSlot(this.id, v); + else { this._local = v; } + } + + _render() { + // Shape / mask. Presets use border-radius so the dashed ring can + // follow the rounded outline; clip-path is only applied for an + // explicit `mask` (the ring is hidden there since a rectangle + // dashed border chopped by an arbitrary polygon looks broken). + const mask = this.getAttribute('mask'); + const shape = (this.getAttribute('shape') || 'rounded').toLowerCase(); + let radius = ''; + if (shape === 'circle') radius = '50%'; + else if (shape === 'pill') radius = '9999px'; + else if (shape === 'rounded') { + const n = parseFloat(this.getAttribute('radius')); + radius = (Number.isFinite(n) ? n : 12) + 'px'; + } + this._frame.style.borderRadius = mask ? '' : radius; + this._frame.style.clipPath = mask || ''; + this._ring.style.borderRadius = mask ? '' : radius; + this._ring.style.display = mask ? 'none' : ''; + + // Controls and reframe entry gate on this so share links stay read-only. + const editable = !!(window.omelette && window.omelette.writeFile); + this.toggleAttribute('data-editable', editable); + this._sub.style.display = editable ? '' : 'none'; + + // Content. The sidecar is also writable by the agent's write_file + // tool, so its value isn't guaranteed canvas-originated — only accept + // data:image/ URLs from it. The `src` attribute is author-controlled + // (Claude wrote it into the HTML) so it passes through unchanged. + let stored = this.id ? getSlot(this.id) : this._local; + if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null; + const srcAttr = this.getAttribute('src') || ''; + this._userUrl = (stored && stored.u) || null; + const url = this._userUrl || srcAttr; + // Don't clobber an in-flight reframe with a store-triggered re-render. + if (!this.hasAttribute('data-reframe')) { + this._view = { + s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1, + x: stored && Number.isFinite(stored.x) ? stored.x : 0, + y: stored && Number.isFinite(stored.y) ? stored.y : 0, + }; + } + this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image'; + // Toggle via style.display — the [hidden] attribute alone loses to + // the display:flex / display:block rules in the stylesheet above. + // An Unsplash src with no credit attribute must NOT render — showing + // the photo uncredited is the Unsplash-terms violation itself. The + // error tile replaces the photo until the credit is written. A + // user-dropped image is the user's own content and always renders. + // Trimmed: credit is agent/user-editable content, and a whitespace- + // only value must count as missing — otherwise it would suppress the + // error tile AND render an empty credit box (no text, no links), + // exactly the unattributed state this gate exists to prevent. + const credit = (this.getAttribute('credit') || '').trim(); + const attrError = !!( + !credit && !this._userUrl && srcAttr && isUnsplashHost(srcAttr) + ); + this.toggleAttribute('data-attribution-error', attrError); + if (url && !attrError) { + const prev = this._img.getAttribute('src'); + if (prev !== url) { + // Replacing an already-shown image: mark the swap BEFORE setting + // src so the stale frame is never revealed (see the data-swapping + // stylesheet rules). First fill (prev empty) keeps the existing + // placeholder-until-load behavior — no spinner. _hidShowing + // covers the pick path's transient attribution-error wipe: prev + // is gone, but an image WAS showing, so this is a replacement. + if (prev || this._hidShowing) this.setAttribute('data-swapping', ''); + // Mark the swap BEFORE assigning src: complete keeps reporting + // the old settled request until the browser's + // update-the-image-data microtask runs, so same-task re-renders + // (the pick path's credit/credit-href setAttributes) need this + // flag, not complete, to know a load is in flight. + this._loadPending = true; + this._img.src = url; + this._ghost.src = url; + } else { + // Same-src re-render — release if settled, so an ingest-set + // spinner can't stick after a byte-identical re-upload (same + // data URL, no further load event ever fires). + this._releaseMask(); + } + this._hidShowing = false; + this._img.style.display = 'block'; + this._empty.style.display = 'none'; + this.setAttribute('data-filled', ''); + this._clampView(); + this._applyView(); + } else { + this.removeAttribute('data-swapping'); + // The src is being removed — no load/error will ever fire for it. + this._loadPending = false; + // A transient attribution-error wipe of a showing image happens on + // the pick path: the host sets src one setAttribute before credit, + // so render N hides the old image (attrError) and render N+1 + // restores a URL. Remember the wipe so that restore renders as a + // replacement (spinner), not a first fill (blank frame). + this._hidShowing = attrError && !!this._img.getAttribute('src'); + this._img.style.display = 'none'; + this._img.removeAttribute('src'); + this._ghost.removeAttribute('src'); + // The error tile owns the blocked-photo state; .empty stays for + // the genuinely-empty slot. + this._empty.style.display = attrError ? 'none' : 'flex'; + this.removeAttribute('data-filled'); + } + + // Credit belongs to the author src, so a user drop hides it. + // textContent + the http(s)-only funnel keep external strings inert. + const showCredit = !!(url && credit && !this._userUrl && !attrError); + this._credit.textContent = ''; + if (showCredit) { + // Validate once (resolved against the document, http(s) only), + // then append the terms-required utm referral params to links + // that point back at unsplash.com. + let href = ''; + const rawHref = this.getAttribute('credit-href') || ''; + if (rawHref) { + try { + const u = new URL(rawHref, document.baseURI); + if (u.protocol === 'http:' || u.protocol === 'https:') { + href = withReferral(u.href); + } + } catch {} + } + const mkLink = (text, linkHref) => { + const a = document.createElement('a'); + a.setAttribute('target', '_blank'); + a.setAttribute('rel', 'noopener noreferrer'); + a.setAttribute('href', linkHref); + a.textContent = text; + return a; + }; + // Unsplash's prescribed credit is TWO links — the photographer's + // name to their profile (credit-href) and 'Unsplash' to the + // homepage. Render that split whenever the text has the canonical + // shape; other text keeps the legacy single-link rendering. + const m = /^Photo by (.+) on Unsplash$/.exec(credit); + if (m) { + this._credit.appendChild(document.createTextNode('Photo by ')); + this._credit.appendChild( + href ? mkLink(m[1], href) : document.createTextNode(m[1]) + ); + this._credit.appendChild(document.createTextNode(' on ')); + this._credit.appendChild(mkLink('Unsplash', UNSPLASH_HOMEPAGE_HREF)); + } else if (href) { + this._credit.appendChild(mkLink(credit, href)); + } else { + this._credit.textContent = credit; + } + } + this.toggleAttribute('data-credit', showCredit); + } + } + + if (!customElements.get('image-slot')) { + customElements.define('image-slot', ImageSlot); + } +})(); diff --git a/specs/design-reference/ios-frame.jsx b/specs/design-reference/ios-frame.jsx new file mode 100644 index 0000000..c4818bf --- /dev/null +++ b/specs/design-reference/ios-frame.jsx @@ -0,0 +1,350 @@ +// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design) +// Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected). + +/* BEGIN USAGE */ +// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame +// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps. +// Exports (to window): IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard +// +// Usage — wrap your screen content in to get the bezel, status bar +// and home indicator (props: title, dark, keyboard): +// +// +// ...your screen content... +// +// +/* END USAGE */ + +// ───────────────────────────────────────────────────────────── +// Status bar +// ───────────────────────────────────────────────────────────── +function IOSStatusBar({ dark = false, time = '9:41' }) { + const c = dark ? '#fff' : '#000'; + return ( +
+
+ {time} +
+
+ + + + + + + + + + + + + + + + +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Liquid glass pill — blur + tint + shine +// ───────────────────────────────────────────────────────────── +function IOSGlassPill({ children, dark = false, style = {} }) { + return ( +
+ {/* blur + tint */} +
+ {/* shine */} +
+
+ {children} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Navigation bar — glass pills + large title +// ───────────────────────────────────────────────────────────── +function IOSNavBar({ title = 'Title', dark = false, trailingIcon = true }) { + const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040'; + const text = dark ? '#fff' : '#000'; + const pillIcon = (content) => ( + +
+ {content} +
+
+ ); + return ( +
+
+ {/* back chevron */} + {pillIcon( + + + + )} + {/* trailing ellipsis */} + {trailingIcon && pillIcon( + + + + + + )} +
+ {/* large title */} +
{title}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Grouped list (inset card, r:26) + row (52px) +// ───────────────────────────────────────────────────────────── +function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark = false }) { + const text = dark ? '#fff' : '#000'; + const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)'; + const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)'; + return ( +
+ {icon && ( +
+ )} +
{title}
+ {detail && {detail}} + {chevron && ( + + + + )} + {!isLast && ( +
+ )} +
+ ); +} + +function IOSList({ header, children, dark = false }) { + const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const bg = dark ? '#1C1C1E' : '#fff'; + return ( +
+ {header && ( +
{header}
+ )} +
{children}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Device frame +// ───────────────────────────────────────────────────────────── +function IOSDevice({ + children, width = 402, height = 874, dark = false, + title, keyboard = false, +}) { + return ( +
+ {/* dynamic island */} +
+ {/* status bar (absolute) */} +
+ +
+ {/* nav + content */} +
+ {title !== undefined && } +
{children}
+ {keyboard && } +
+ {/* home indicator — always on top */} +
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Keyboard — iOS 26 liquid glass +// ───────────────────────────────────────────────────────────── +function IOSKeyboard({ dark = false }) { + const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959'; + const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333'; + const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)'; + + // special-key icons + const icons = { + shift: , + del: , + ret: , + }; + + const key = (content, { w, flex, ret, fs = 25, k } = {}) => ( +
{content}
+ ); + + const row = (keys, pad = 0) => ( +
+ {keys.map(l => key(l, { flex: true, k: l }))} +
+ ); + + return ( +
+ {/* liquid glass bg — same recipe as nav pills */} +
+
+ + {/* autocorrect bar */} +
+ {['"The"', 'the', 'to'].map((w, i) => ( + + {i > 0 &&
} +
{w}
+ + ))} +
+ + {/* key layout */} +
+ {row(['q','w','e','r','t','y','u','i','o','p'])} + {row(['a','s','d','f','g','h','j','k','l'], 20)} +
+ {key(icons.shift, { w: 45, k: 'shift' })} +
+ {['z','x','c','v','b','n','m'].map(l => key(l, { flex: true, k: l }))} +
+ {key(icons.del, { w: 45, k: 'del' })} +
+
+ {key('ABC', { w: 92.25, fs: 18, k: 'abc' })} + {key('', { flex: true, k: 'space' })} + {key(icons.ret, { w: 92.25, ret: true, k: 'ret' })} +
+
+ + {/* bottom spacer (emoji+mic area, icons omitted) */} +
+
+ ); +} + +Object.assign(window, { + IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard, +}); diff --git a/specs/design-reference/screenshots/01-ctx.png b/specs/design-reference/screenshots/01-ctx.png new file mode 100644 index 0000000..5fabed1 Binary files /dev/null and b/specs/design-reference/screenshots/01-ctx.png differ diff --git a/specs/design-reference/screenshots/01-ctx4.png b/specs/design-reference/screenshots/01-ctx4.png new file mode 100644 index 0000000..3ab51ef Binary files /dev/null and b/specs/design-reference/screenshots/01-ctx4.png differ diff --git a/specs/design-reference/screenshots/01-incontext.png b/specs/design-reference/screenshots/01-incontext.png new file mode 100644 index 0000000..4e91ef4 Binary files /dev/null and b/specs/design-reference/screenshots/01-incontext.png differ diff --git a/specs/design-reference/screenshots/01-triplet.png b/specs/design-reference/screenshots/01-triplet.png new file mode 100644 index 0000000..61bca8c Binary files /dev/null and b/specs/design-reference/screenshots/01-triplet.png differ diff --git a/specs/design-reference/screenshots/02-ctx.png b/specs/design-reference/screenshots/02-ctx.png new file mode 100644 index 0000000..02d75d1 Binary files /dev/null and b/specs/design-reference/screenshots/02-ctx.png differ diff --git a/specs/design-reference/screenshots/02-ctx4.png b/specs/design-reference/screenshots/02-ctx4.png new file mode 100644 index 0000000..7841c15 Binary files /dev/null and b/specs/design-reference/screenshots/02-ctx4.png differ diff --git a/specs/design-reference/screenshots/02-incontext.png b/specs/design-reference/screenshots/02-incontext.png new file mode 100644 index 0000000..6d9a7c8 Binary files /dev/null and b/specs/design-reference/screenshots/02-incontext.png differ diff --git a/specs/design-reference/screenshots/02-triplet.png b/specs/design-reference/screenshots/02-triplet.png new file mode 100644 index 0000000..91df742 Binary files /dev/null and b/specs/design-reference/screenshots/02-triplet.png differ diff --git a/specs/design-reference/screenshots/03-triplet.png b/specs/design-reference/screenshots/03-triplet.png new file mode 100644 index 0000000..95fad53 Binary files /dev/null and b/specs/design-reference/screenshots/03-triplet.png differ