imax-view
A design system that treats the artifact canvas the way IMAX 15/70 treats the screen: taller than you expect, larger than you expect, and quiet everywhere except one place.
The reference is the 15-perf 70mm film format, the one that expands vertically to 1.43:1 rather than stretching sideways. Every token below traces back to something real about that format. See references/format.md for the derivations if a decision needs justifying or extending.
The four rules
- Frame first. Pick the aspect ratio before writing any other CSS. Default to 1.43:1. Do not default to 16:9.
- Type is large. Body copy starts at 18px. Nothing readable goes below 14px.
- One bright thing. The palette is near-monochrome. Accent is light bleed, not fill.
- Copy is unmarked. No em dashes, no semicolons, no hard wraps.
Frame
| Ratio | Use | Suggested canvas |
|---|---|---|
| 1.43:1 | Default. Heroes, cards, standalone artifacts, single-screen pieces. | 1100 × 769 |
| 1.90:1 | Genuinely wide content only: timelines, wide tables, comparison rails. | 1100 × 579 |
| free | Long scrolling documents. Keep --measure and the 15-col grid. |
n/a |
Set it explicitly with aspect-ratio, never by accident via content height.
Below 640px, release the ratio and stack. A 1.43:1 frame on a phone is a letterbox, which is the exact opposite of the intent.
The 15-column grid
15/70 means fifteen perforations per frame. The grid is 15 columns, not 12. It reads slightly off to anyone used to Bootstrap, which is the point, and odd column counts give a true center column for free.
display: grid;
grid-template-columns: repeat(15, 1fr);
gap: var(--perf);
Fit and overflow
Content escaping its box is a layout bug, not a scroll affordance. Fix it at the source before reaching for overflow.
Diagrams and charts first. If nodes, labels, or an SVG viewBox overflow, resize and respread rather than clip. In order: grow the box to fit the longest label, spread the nodes across the full width and height that is actually available instead of clustering them in the middle, shorten the label text, then drop one type step. Give every SVG width: 100%, height: auto, and a viewBox that genuinely bounds the drawing rather than a guessed one. Text inside SVG does not wrap, so size each node from its longest string.
A diagram is done when nothing is clipped at 1440px and the page does not scroll sideways at 360px.
Gap at the top and the bottom. Every page keeps open ground above the first element and below the last: --act at minimum, --intermission on anything long. Content flush against the viewport edge reads as a cropped screenshot. The same holds in full screen, where the letterbox bars are that gap, so never fill them.
Themed scrollbars when a scrollbar is unavoidable. A wide table, a code block, or a long panel can legitimately scroll. Scope the overflow to that one element, never the page, and dress the scrollbar. The browser default is a light gray slab and on a #08080A ground it becomes the loudest thing on the screen.
.scroll {
overflow: auto;
scrollbar-width: thin;
scrollbar-color: var(--edge) transparent;
}
.scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.scroll::-webkit-scrollbar-track { background: transparent; }
.scroll::-webkit-scrollbar-thumb {
background: var(--edge);
border-radius: var(--radius);
}
.scroll::-webkit-scrollbar-thumb:hover { background: var(--halation); }
Set both forms. The standard properties cover Firefox and current Chromium, the ::-webkit- pseudo-elements cover Safari. Both read theme tokens, so the scrollbar follows the theme instead of fighting it.
Type
Monospace throughout, in the Berkeley Mono lineage: squarish, terminal-derived, low stroke contrast, tall x-height. The stack degrades to whatever the OS ships, so nothing needs downloading:
--font: "Berkeley Mono", "TX-02", "Commit Mono", "JetBrains Mono",
ui-monospace, "SF Mono", Menlo, Consolas, monospace;
ui-monospace resolves to SF Mono on macOS and Cascadia Mono on Windows, both close enough in feel to carry the design.
Roles are separated by size, weight, and tracking, not by family:
| Role | Size | Weight | Tracking | Leading |
|---|---|---|---|---|
| Display | 91-137px | 500 | −0.035em | 0.92 |
| Title | 41-61px | 500 | −0.02em | 1.05 |
| Lead | 27px | 400 | −0.01em | 1.4 |
| Body | 18px | 400 | 0 | 1.65 |
| Caption | 14px | 400 | +0.01em | 1.5 |
| Eyebrow | 12px | 600 | +0.18em, uppercase | 1 |
Scale ratio is 1.5. The gap between display and body is deliberately violent, and that contrast is the cinematic register. Do not fill the middle of the scale with in-between sizes to soften it.
Measure caps at 62ch. Monospace runs wide, so longer lines stop scanning.
Numerals: font-variant-numeric: tabular-nums on anything columnar.
Copy
The words inside the frame are part of the design, so they carry rules too. Two prohibitions, both absolute:
- No em dashes. Not in headings, body copy, captions, labels, button text, alt text, tooltips, chart annotations, code comments, or commit messages. Recast the sentence instead. A period, a comma, a colon, or the word "because" almost always reads better, and none of them carry the tell.
- No semicolons. Split the clause into two sentences, or join it with a conjunction.
- No hard wraps. A paragraph is one unbroken line in the source. Let the renderer decide where it breaks.
En dashes go the same way. Numeric ranges take a plain hyphen (91-137px) or the word "to".
This covers prose only. Semicolons in CSS, JavaScript, and every other language are syntax and stay exactly where they are, and so is the line wrapping in a code block, a table, or an ASCII diagram. Reflowing those breaks them.
The hard-wrap rule is the same argument as the frame. Prose wrapped by hand to 80 columns is prose measured for a terminal, and it re-wraps wrong in every viewport that is not one. --measure already caps the line at 62ch where it renders, which is the only place the cap belongs. Wrapping the source as well fixes the break points to a width nobody is reading at, and it turns a one-word edit into a reflowed paragraph in the diff.
The rule holds in every piece the skill touches, throwaway mockups included, and it holds in the surrounding chat reply as much as in the artifact. Em dashes and semicolons are the two loudest punctuation tells of generated writing, and a system whose entire budget goes on not looking generated cannot afford either.
Palette
A projection booth, not a UI. Warm light against cold dark, because film print stock throws highlights warm and shadows cyan.
--void #08080A auditorium black, the page background
--gate #121318 surface, cards, the film gate
--emulsion #1D1E24 raised surface, inputs, hover
--edge #2A2C34 hairlines, frame lines
--silver #9A968E secondary text, screen gain
--lamp #F2ECE0 primary text, xenon white, never pure #FFF
--halation #D8451A accent, print halation
--cyan #4E7F8C cold accent, data series, links
The halation rule: --halation is a bleed, not a fill. It belongs in 1 to 3px edges, underlines, glows, a single active state, one number that matters. The moment it becomes a filled button background, the whole thing collapses into generic dark-mode-with-an-orange-accent. Filled surfaces use --emulsion with a --halation hairline instead.
Light: the light table
Light mode is not an inversion of dark. Inverting produces a warm cream ground with a serif-adjacent warm accent, which is the most common generated design look there is.
Instead it derives from the other half of the format: a positive print on a light table. Denser emulsion reads darker than the surround, so surfaces go down from the ground, not up. Cards are darker than the page. This is the inverse of the usual light-mode convention and it's the thing that makes the theme recognizable.
--void #E9E7E1 light table, page ground (neutral warm gray, not cream)
--gate #DEDBD3 the frame, darker than ground
--emulsion #D2CEC5 raised surface, darker still
--edge #B8B3A8
--silver #6E6A62
--lamp #14151A ink
--halation #B5390F deepened, #D8451A is too hot on light ground
--cyan #3A6270
The halation prohibition holds in both themes.
Choosing a theme
Do not ask the user which theme they want. A skill that opens with a question on every visual request is friction, and the answer is inferable almost every time. Resolve in this order:
- The request says so. "dark", "light", "for print", "for a deck", "for the docs site". Take it and move on.
- The output has an obvious home. Documentation, print, anything embedded in an existing light product, or long-form reading → light. Dashboards, monitoring, terminals, media, demos, hero pieces → dark.
- Otherwise: ship both. Default dark, follow
prefers-color-schemewhen no theme is set, and include the icon toggle described under Controls so the viewer decides. This is the default behavior inassets/tokens.cssand costs about fifteen lines.
Ask only when the piece is a real deliverable going into a context you can't see, such as a client site or someone else's product, and the request gives no signal at all. Even then, ask once alongside other open questions, never as a standalone blocking turn.
Controls
Viewer controls are projection booth controls: small, square, iconic, parked in one corner. They are page furniture rather than page content, so they never carry a text label.
- Icon only, 32 to 36px square, transparent ground,
--edgehairline,--silverglyph that goes--lampwith a--halationborder on hover. No filled background, the halation rule covers these too. - Draw the glyph as inline SVG at 16px with
fill: none,stroke: currentColor,stroke-width: 1.5. Emoji are the wrong register for an instrument panel. - Every control carries
aria-labelandtitle, plusaria-pressedwhen it toggles. - Group them as one cluster, top right of the page or in the frame's foot. Two is usually the whole set.
Theme. An icon toggle, never a button with words on it. Sun while the page is dark, moon while it is light, so the glyph shows what the click does. The phrasing goes in the aria-label and title, not on the control.
Full screen. Ships next to the theme toggle on any piece that has a frame. Corner brackets pointing out, swapping to brackets pointing in once full screen. It calls requestFullscreen() on the frame wrapper, not on the document body, and listens to fullscreenchange so the glyph stays honest when the viewer leaves with Escape.
In full screen the frame presents at IMAX 70mm size: 1.43:1, centered, as large as the height allows, with the leftover width left as ground colored letterbox. Never stretch it to the display's own ratio, which is the exact thing the format exists to refuse.
.projector:fullscreen {
display: grid;
place-items: center;
background: var(--void);
padding: var(--act) var(--reel);
}
.projector:fullscreen .frame {
width: min(100%, calc((100vh - var(--act) * 2) * var(--ratio-imax)));
height: auto;
aspect-ratio: var(--ratio-imax);
}
The --act padding is the gap rule again. The frame never touches the top or bottom edge of the screen, so the bars read as letterbox rather than as a clipped image.
Spacing
Named for the format, ramped hard rather than linearly:
--perf 6px grid gutter, the base unit
--frame 12px
--gate-sp 20px
--reel 36px
--act 64px
--intermission 112px section breaks only
Section breaks use --intermission and nothing smaller. Cramped vertical rhythm is what makes a dark layout feel like a terminal instead of a theater.
Signature
Every piece gets exactly one. Pick one and do not stack them.
- The expand. Hero mounts at 1.90:1 and animates to 1.43:1 over ~700ms with the content already in place. This is the actual IMAX sequence transition and it is the strongest option when motion is available.
- Perf rail. A column of 15 small notches down one frame edge,
--edgecolored, marking the grid. Structural, not decorative, so align content to it. - Frame line. A 1px
--edgeborder on the canvas with a soft--halationglow bleeding inward from one corner only.
Respect prefers-reduced-motion: the expand becomes a static 1.43:1 mount.
Quality floor
Assume all of this without being asked: responsive to 360px, no horizontal page scroll and nothing clipped at any width, themed scrollbars wherever one appears, open ground at the top and bottom of the page, visible keyboard focus using --halation outline, reduced motion honored, and contrast at least 4.5:1 for body text.
Contrast trap, both themes: --silver passes on --void but fails on --emulsion. Secondary text on a raised surface uses --lamp at reduced opacity, not --silver.
When to break it
- Brief specifies a look. The brief wins, always. Keep only the type scale.
- Existing codebase. Match the codebase. Do not import this into someone else's design system.
- Data density. Dashboards with 20+ metrics need the 1.90:1 frame and may drop body to 16px. Say so rather than silently shrinking.
- Print or document output. Ratios and dark ground don't transfer. Carry the type scale and measure only.
Files
assets/tokens.css: drop-in custom properties, base reset, page gutters, grid, type roles, themed scrollbars, the icon controls, and the full screen projector. Paste into any artifact and start using the variable names.references/format.md: where each number comes from. Read when extending the system or when a choice needs defending.