Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 188 additions & 1 deletion .okf/design/house-visual-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resource: .stitch/prompts/course-inpost-visual-prompt.md
tags: [svg, design, course]
generated:
by: process:okf-migrate
at: 2026-07-13T00:00:00Z
at: 2026-08-01T00:00:00Z
---

# The spec in one table
Expand Down Expand Up @@ -46,3 +46,190 @@ print-ready PDF (Wave M3 ships a download link on every printable page;
print is the native format for pen-and-paper artifacts). Revisit only if
pilot recordings show phone readers pinch-zooming worksheets instead of
downloading.

# v3 exhibit spec

Source of authority: ADR 30.09 (premium-editorial pilot). This section is
the executable spec its Phase-A A/B pilot demanded - an SVG author renders
from it without guessing px, ratios, or weights. It ADDS to (does not
replace) the hand-drawn grammar above: O1 keeps that style, disciplined to
the numbers here; O2 is the flat-vector challenger. Everything below is
style-agnostic unless a row splits O1/O2. Dashes stay "-" everywhere,
green stays money-only, labels stay inside shapes.

## The governing constant: the >=9px @390px legibility floor

The current SVGs render at a 960-1000 viewBox and their smallest text lands
at ~5.5-7px on a 390px phone (ADR "Measured state"). That is the defect v3
fixes. The fix is a formula, not a wish.

An SVG scales to its container width, so rendered text size is:

rendered_px = font_viewBox * (W_render / W_viewBox)

Reference render width is **W_render = 390** (a phone's CSS viewport; the
real content column after page padding is ~358px, so 390 carries ~9%
built-in headroom). Require the SMALLEST rung (basis) to clear 9px:

rendered_px(basis) >= 9
=> basis_viewBox >= 9 * W_viewBox / 390 = 0.02308 * W_viewBox
=> equivalently W_viewBox <= basis_viewBox * 43.33

**Canonical exhibit viewBox width = 720.** At 720 the floor is
0.02308 * 720 = 16.62px, so the basis rung is set to **17px**, which renders
17 * 390/720 = **9.21px** at 390 - above floor by design. Only the basis rung
(the smallest) needs the check; every larger rung clears it automatically.

If an exhibit needs a wider viewBox W, scale ALL five rungs by W/720 so the
floor still holds; never place text smaller than `ceil(9 * W/390)` px.
Widening the viewBox to fit more nodes is the trap that caused the 5.5px
defect - prefer fewer nodes at W=720 over more nodes at W=960.

**O1 Caveat correction (calibration knob):** Caveat's x-height is ~15%
smaller than Space Grotesk at the same point size, so O1 multiplies the
required floor by 1.15 - O1 basis rung = ceil(1.15 * 17) = **20px** (renders
10.8px, perceived ~9.4px). This is a real font-metric compensation, not slack.

## 1. Grid / column system

| Property | Value (viewBox units at W=720) |
|---|---|
| Canonical viewBox width | 720 |
| Columns | 12 |
| Outer safe margin (no critical content past it) | 24 all sides |
| Gutter | 12 |
| Column width | (720 - 48 - 11*12)/12 = 45 |
| Vertical rhythm baseline | 8 |

Nodes and axes snap to column edges; multi-column nodes span whole columns
plus the gutters between them (2 cols = 45+12+45 = 102). Vertical positions
snap to the 8px baseline.

## 2. Spacing scale

8px-based rung set (matches JetVelocity's 8px rhythm; 4 and 12 are the only
half-steps, reserved for tight in-node text padding):

4 - 8 - 12 - 16 - 24 - 32 - 48 - 64

- 4/8: in-node text padding, gap between a label and its icon.
- 12/16: node internal padding, gap between a bar and its value label.
- 24/32: gap between sibling nodes, chart plot inset.
- 48/64: gap between exhibit regions (title block -> body -> basis line).

Every gap in an exhibit is one of these eight values. No off-rung spacing.

## 3. Connector spec

| Property | O2 flat-vector | O1 hand-drawn-normalized |
|---|---|---|
| Line weight | 2px uniform | 2-2.5px hand stroke |
| Routing | orthogonal (right-angle) | organic curve |
| Corner radius on turns | 4px | n/a (curve) |
| Arrowhead | filled triangle, 9 long x 7 base, same fill as line | open hand-drawn caret, ~10px |
| Attach point | node edge midpoint | node edge, loose |
| Turn clearance | >=16px straight off a node before the first turn | ~16px |
| Connector label | in a paper-tone pill centered ON the line, never crossing it | same |

Line color = ink (#121212 O2 / #1a1a1a O1) for neutral flow; ruby only when
the connector itself is the anti-pattern / action edge; purple for the
alternate path (carried from the hand-drawn semantics above).

## 4. Data-viz rules

- **Chart-vs-diagram trigger:** message is a quantity / comparison / trend
-> chart. Message is a flow / relationship / sequence -> diagram. One
message per exhibit decides which.
- **Fill vs stroke (hard rule):** DATA marks are FILLED, no stroke (bars,
dots, bands, meter fill). STRUCTURE is STROKED, no fill (axes, gridlines,
connectors, node outlines). Never both on one mark.
- **Bars:** filled semantic color; inter-bar gap = 0.5x bar width; category
label below (label rung), value label above (annotation rung).
- **Dots/scatter:** 6px-radius filled dots, no stroke.
- **Gauge / meter / signal read:** horizontal track, bands filled by
semantic tone, 2px pointer tick, threshold marked with a labeled tick
(this is the `smoke-test-signal.svg` archetype).
- **Ruby-for-signal convention:** ruby marks the ONE actionable reading -
the threshold to act on, the kill zone, the number the exhibit exists to
show. Everything else stays ink or gray. If two things are ruby, the
exhibit has two messages - split it.
- **Threshold vs band:** a threshold is a 2px dashed ink line perpendicular
to the axis with a labeled tick. A band is a filled semantic-tone rect
BEHIND the data, its label inside at the label rung.
- **Axis:** single baseline axis 2px ink; no gridlines unless precise values
are read (then 1px #e5e5e5); no 3D, no chart junk. Plain-language chart
title sits in the action-title slot (see grammar).

## 5. Aspect-ratio guidance

| Exhibit type | Ratio | viewBox at W=720 |
|---|---|---|
| Signal meter / single-axis chart | 12:5 | 720 x 300 |
| Flow / process diagram | 16:9 | 720 x 405 |
| Comparison (bars) | 3:2 | 720 x 480 |
| Relationship / node map | 3:2 | 720 x 480 |
| Program map / synthesis | up to 4:5 portrait | 720 x 900 max |

**Mobile-safe default = 3:2 (720 x 480)** - fills the phone column at ~1.3x
viewport height. Never exceed 4:5 portrait on a phone, and never breach the
hand-drawn spec's "taller than 2x viewport = wall, not hook" ceiling above.

## 6. Five-rung type scale

Sizes in viewBox units at the canonical W=720. Rendered column = value *
390/720 = value * 0.5417 (the >=9px floor check).

| Rung | Role in exhibit | O2 size / font | O1 size / font | Rendered @390 (O2) |
|---|---|---|---|---|
| 1 Title | action title (the takeaway sentence) | 34 / Space Grotesk 700 | 34 / Caveat 700 | 18.4px |
| 2 Message | the one point proved (subtitle) | 24 / Space Grotesk 600 | 24 / Caveat 700 | 13.0px |
| 3 Label | in-node / axis labels | 20 / Space Grotesk 600 | 22 / Caveat 700 | 10.8px |
| 4 Annotation | callouts, value labels | 18 / system-ui 400 | 22 / Caveat 700 | 9.75px |
| 5 Basis | source/scope line | 17 / system-ui 400 | 20 / Caveat 700 | 9.21px |

O2 uses Space Grotesk for title/message/label only; annotation and basis
(running text) stay system-ui per ADR ("Space Grotesk labels only, body
stays system-ui"). O1 keeps the full Caveat/Patrick Hand/Comic Sans stack at
the x-height-corrected sizes (rungs 3-5 bumped to satisfy the O1 floor).

## Exhibit grammar (every exhibit, both styles)

Zelazny doctrine, from ADR driver D2. Non-negotiable in either style:

1. **One action title** - a sentence stating the takeaway, not a label.
"Under 3% conversion means kill the idea", never "Conversion signal".
2. **One message** - the single point the exhibit proves. If you need two,
make two exhibits.
3. **One basis line** - the data's source and scope, bottom of the exhibit.
"Basis: 7-day cold-traffic test, one channel."

## O1 vs O2 scoring rubric (for the T2 A/B pair)

Both variants MUST pass the grammar checklist and the measured >=9px @390px
floor - those are pass/fail gates, not scored preferences. The rows below
are where the two styles legitimately diverge, so T2 (and Paul's eye-test)
can score them on the same axes at equal zoom.

| Axis | O1 hand-drawn-normalized | O2 premium-editorial flat-vector |
|---|---|---|
| Heading/label font | Caveat / Patrick Hand / Comic Sans stack | Space Grotesk 600-700 |
| Running-text font | same cursive stack | system-ui |
| Stroke | 2-2.5px hand feel | 1.5-2px uniform |
| Corner radius | organic / none | 4px |
| Texture filter | rough filter allowed | none (clean) |
| Background | paper tones (#fff5f5 / #faf7f2 / #f0f9f0 / #fbe9ff) | paper white #ffffff on #faf7f2 page |
| Ink color | #1a1a1a | #121212 |
| Accents | ruby=action/anti-pattern, purple=alt, green=money, amber=warn | same semantics |
| Basis-rung floor | 20px (Caveat x-height corrected) | 17px |
| Brand-identity fit | preserves the "handwritten = non-negotiable" mermaid brand rule | deliberately re-tests it (ADR: weak prior) |
| Sam-trust read (ADR) | risked reading "amateur / unfinished" | risked reading "generic startup" if overdone |
| When it wins | authenticity/warmth valued over polish; cheapest (no restyle) | craft/consistency read as premium; consulting-grade synthesis |

## Verification against ADR 30.09

All six demanded components are present: (1) grid, (2) spacing scale,
(3) connector spec, (4) data-viz rules, (5) aspect-ratio, (6) 5-rung type
scale with the measured >=9px @390px floor formula - plus the exhibit
grammar and the O1/O2 rubric. Nothing in the ADR's Phase-A spec requirement
was left aspirational; the floor is a formula (basis 17px @ W=720 -> 9.21px)
and every dimension is a number an SVG author can render from.
14 changes: 14 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,3 +580,17 @@ untouched.

## 2026-08-01 - Removed toolchain drift-gate unit tests (config-mirror anti-pattern)
* **Remove**: `test/unit/toolchain_pins_test.rb` deleted in full. It was a config-mirror "drift gate" asserting `.mise.toml` version pins equal the literal pins in the setup-hugo action, `_hugo.yml`, `.dev/compose.yml`, and `.ruby-version`. `test_setup_hugo_action_matches_mise` red-built on a FALSE invariant: `.mise.toml` `node = "latest"` (devs want latest) vs CI `node-version: '22'` (pinned on purpose in #393) - a correct divergence, not drift. Tests config agreement, not behavior; violates FIRST + the CLAUDE.md "no fragile config assertions" rule. Comparing CI-vs-local node explicitly was considered and rejected as overkill (Paul). Refs cleaned: Rakefile `:guards` list, `.mise.toml` / setup-hugo `action.yml` header comments, README, `docs/SETUP.md`, this bundle's [ci-gates](/build/ci-gates.md). Pins still need manual sync when bumping - now by convention, not a gate.

## 2026-08-01 - W3-T1: v3 exhibit spec appended to house-visual-spec

Added a `# v3 exhibit spec` section to `.okf/design/house-visual-spec.md`
(hand-drawn spec untouched, O1 still needs it) covering the six components
ADR 30.09 Phase A demanded: 12-col grid at a canonical 720 viewBox, 8px
spacing scale, connector spec (2px orthogonal + 4px radius O2 / hand-curve
O1), data-viz rules (fill=data / stroke=structure, ruby-for-signal,
threshold-vs-band), aspect-ratio table (mobile-safe default 3:2), and the
5-rung type scale. The >=9px @390px floor is a formula, not prose:
`rendered = font_viewBox * 390/W_viewBox`; basis rung 17px @ W=720 renders
9.21px. O1 bumps the floor x1.15 (basis 20px) for Caveat's small x-height.
Includes the action-title/one-message/basis-line grammar and an O1-vs-O2
scoring rubric so the T2 A/B pair scores both on the same axes.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Each blank is an assumption, and each assumption has a test waiting for it later
- **`[COMPETITION]`**: what they currently use (a spreadsheet, a billing service, "doing nothing")
- **`[DIFFERENTIATION]`**: why they'd switch - faster or cheaper, with numbers

![5 blanks of the Mad Libs hypothesis sentence with a filled example below](hypothesis-mad-libs.svg)
![The founding-hypothesis frame as five outlined blanks - customer, problem, approach, competition, differentiation - with the customer blank filled two ways to show specific beats vague: "small businesses" gets a shrug, "solo chiropractors" gets "that's me"](hypothesis-mad-libs.svg)

Can't tell whether a blank is specific enough to bet on? The [validation tools field guide](/course/tech-for-non-technical-founders-2026/validation-tools-field-guide/) walks three AI tools that stress-test your five blanks in about ten minutes - a structured second opinion before you commit the sentence and start booking interviews.

Expand Down
Loading
Loading