Classic 1984

Classic 1984 — design system

The look of the 1984 Macintosh, rebuilt for modern apps. It started in Agentrix (Classic theme, renderer/classic.css and ios/AgentDeck/Classic.swift). Ekybe then took it further: the Mac window chrome, people's pictures, Greek text, per-device settings and a text size setting. This repository is the reference for copying it into any other app: the rules, a drop-in stylesheet and script, the fonts with full Greek, and a live example.

Live: justaicode.app/classic-1984 · example page · picture styles

Status Live in Ekybe (web, Mac, iPhone, Android) and Agentrix (Mac, iPhone), October 2026
Approved by Roberto, from mockups, 3 and 4 Oct 2026
Reference implementations Ekybe (src/classic.css, src/lib/classic.ts, src/components/ClassicTitleBar.tsx, src/components/Avatar.tsx) and Agentrix (renderer/classic.css, ios/AgentDeck/Classic.swift); both private
This repository classic-1984.css, classic-1984.js, fonts/, specimen.html, pictures.html, tools/, images/
Licence Code and documentation MIT; fonts public domain (see LICENSE and §17)

Specimen, day


Contents

  1. Principles
  2. Quick start
  3. Colour
  4. Patterns
  5. Typography
  6. Shape, lines and elevation
  7. Layout and metrics
  8. Components
  9. People's pictures
  10. Interaction and motion
  11. Settings a person can change
  12. Platforms
  13. Re-skinning an existing app
  14. Accessibility
  15. Do and don't
  16. Checklist
  17. Fonts: origin, licence, rebuilding
  18. Decision log
  19. Files

1. Principles

The 1984 Mac had a 1-bit screen, 512 × 342 pixels, black or white per pixel. Everything in this system follows from that limit. It is applied with judgement, not literally.

  1. Two colours. There is an ink and a paper, and nothing else. By default they are black and white. A person may tint them (dark green on pale green, for example), but there are still only two. Every grey, highlight and border is the ink, the paper, or a mix of them.
  2. Grey is a pattern. Where a modern UI uses a grey fill, 1984 used a dither: alternating ink and paper pixels. Use the dither for tracks, the desktop and frames, never a flat grey fill. Text that has to be secondary is the one exception: it uses a 64% ink mix, because dithered text cannot be read.
  3. Square corners, hard shadows. Corners are square. Shadows are a solid 2px step down and to the right, with no blur. The few round things are listed in §6 and are deliberate.
  4. Lines are 1px. The line under a title is 2px. That is the whole hierarchy of lines.
  5. Selection is reversal. A chosen row, a hovered menu item, a pressed button all swap ink and paper. Nothing gets a colour to show it is selected.
  6. Chicago speaks. Names, titles, buttons, menus and labels are set in Chicago. Running text is set in Chicago Light, a thinned Chicago that can be read for long stretches.
  7. Colour is content, never chrome. Emoji, photos and pictures people post keep their colour. The interface never adds any.
  8. No motion. Things appear and disappear. Nothing fades, slides or bounces.
  9. Modern where it matters. Text can be resized, Greek is fully supported, and the night look follows the system's dark mode. Focus is visible. Authenticity never outranks reading.

2. Quick start

  1. Copy classic-1984.css, classic-1984.js and fonts/ into your app, or add this repository as a git submodule:

    git submodule add https://github.com/justaicode/classic-1984.git design/classic-1984
    

    Keep the relative path fonts/ next to the CSS, or edit the two @font-face URLs.

  2. Add class="c84" to <html>. Any container works too, if only part of the app should be in the style.

  3. Set the colours:

    import { followSystem } from './classic-1984.js'
    // prefs: { hue, strength, night, avatars } — see §3 and §11
    const stop = followSystem(document.documentElement, () => prefs)
    

    If you skip this step, the CSS defaults are black on white, day only.

  4. Build with the c84-* classes in §8. Open specimen.html to see every one of them. Serve the folder (python3 -m http.server in it), because fonts and 1-bit photos need same-origin.

  5. Ask questions and show menus through the kit, never the browser's own confirm() / alert() / context menu:

    import { showMenu, showDialog, showPrompt } from './classic-1984.js'
    const id = await showMenu(items, { x: e.clientX, y: e.clientY })            // §8.7
    const n = await showDialog({ message, detail, buttons: ['Delete', 'Cancel'], destructive: 0 })  // §8.8
    const text = await showPrompt({ message: 'Rename', value, confirm: 'Rename' })
    

A bare window:

<div class="c84-window">
  <div class="c84-window-bar">
    <button class="c84-box is-close" aria-label="Close"></button>
    <button class="c84-box is-mini" aria-label="Minimise"></button>
    <button class="c84-box is-zoom" aria-label="Zoom"></button>
    <span class="c84-window-title">Untitled</span>
  </div>
  <div class="c84-pane-bar is-plain">Inbox</div>
  <div class="c84-row is-selected">First item <span class="c84-badge">3</span></div>
  <div class="c84-row">Second item</div>
</div>

3. Colour

3.1 The two colours

Token Meaning
--ink Text, lines, fills that mean "on", the selected row's background
--paper Backgrounds, text on an ink fill, the inside of boxes

Everything else in the system is derived from these two.

3.2 Derived tokens

Token Value Used for
--c84-muted 64% ink in paper Timestamps, hints, placeholders, disabled labels
--c84-half 50% ink in paper The "away" presence dot
--c84-tint 12% ink in paper A soft accent fill (a mention, a highlighted message)
--c84-hover 9% ink in paper A row under the pointer
--c84-dither 2 × 2 checkerboard of ink and paper Grey surfaces: desktop, slider track, scroll track, picture frame
--c84-stripes 1px ink / 1px paper horizontal lines Title bars
--c84-dots-h 1px ink / 1px gap, horizontal Dotted rule between menu groups

The mixes use color-mix(in srgb, …). They are the only "greys" allowed, and only where a pattern cannot work: text, and one-pixel dots.

3.3 Tinting: the recipe

A person picks a hue (0–359) and a strength (0–100). Strength 0 is plain black and white whatever the hue. The recipe below is identical in Ekybe, Agentrix and classic-1984.js. Keep it identical, or the apps stop matching.

k = strength / 100

Day:   paper = hsl(hue, 35k%, (100 − 9k)%)     ink = hsl(hue, 60k%, 15k%)
Night: paper = hsl(hue, 45k%, 6k%)             ink = hsl(hue, 80k%, (100 − 32k)%)

Values are rounded to one decimal place.

3.4 Presets

The settings offer five swatches. Picking one sets the hue, and sets the strength to 100 if it was 0 (Paper sets strength 0).

Preset Hue Day ink Day paper Night ink Night paper
Paper (default) — hsl(0 0% 0%) #000 hsl(0 0% 100%) #fff hsl(0 0% 100%) #fff hsl(0 0% 0%) #000
Green 135 hsl(135 60% 15%) hsl(135 35% 91%) hsl(135 80% 68%) hsl(135 45% 6%)
Teal 180 hsl(180 60% 15%) hsl(180 35% 91%) hsl(180 80% 68%) hsl(180 45% 6%)
Blue 215 hsl(215 60% 15%) hsl(215 35% 91%) hsl(215 80% 68%) hsl(215 45% 6%)
Amber 35 hsl(35 60% 15%) hsl(35 35% 91%) hsl(35 80% 68%) hsl(35 45% 6%)

Below the swatches, a hue slider (with a rainbow track) and a strength slider ("From plain black and white to fully tinted") allow any value in between.

Green, day Blue, night

3.5 Night

The night look is glowing ink on dark paper, like a phosphor screen. It applies only when both are true:

At night the root gets data-c84-night and color-scheme: dark, so native controls and scroll bars follow. Night is the same recipe with the night formula. Nothing else changes: no component has a separate night rule.

Night, black and white

3.6 Colour that is allowed through

Only content keeps its own colour:

The interface itself never adds a colour: no red for errors, no green for success, no blue for links. Errors are ink text, and links are ink with an underline.

White text in an existing app. If the app writes color: #fff for "text on the accent", re-map that to --paper. The accent is now the ink, which is near-white at night, so white-on-ink would vanish. Ekybe does this with one rule; see §13.


4. Patterns

All patterns are CSS gradients sized in whole pixels, so they stay crisp at 1x and 2x.

--c84-dither:  repeating-conic-gradient(var(--ink) 0 25%, var(--paper) 0 50%) 0 0 / 2px 2px;
--c84-stripes: repeating-linear-gradient(var(--ink) 0 1px, var(--paper) 1px 2px);
--c84-dots-h:  repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 2px);
Pattern Where
Dither (50% grey) Desktop behind windows, slider track, scroll-bar track, the 3px frame of an "Ink only" picture
Stripes The window title bar (between 7px paper margins); a 9px band along the top of pane headers
Dotted rule Between groups in a pull-down menu

Never use a pattern behind text.


5. Typography

5.1 Faces

Role Family File Notes
Display: names, titles, buttons, menus, labels, badges 'Chicago' fonts/ChicagoGreek.ttf ChicagoFLF plus the Greek it lacked (§17). One weight only.
Text: messages, body, fields 'Chicago Light' fonts/ChicagoLight.ttf Chicago with upright strokes thinned for reading. Identical Latin to Agentrix's iPhone chat font.
Text, optional 'Geneva' fonts/Geneva.ttf A 1992 copy of Geneva (PostScript GenevaPlain), Latin only, wide word spaces. An extra choice for chat text in Agentrix's iPhone app; see fonts/README.Geneva.
Code Monaco → ui-monospace, Menlo, monospace system Monaco is on every Mac; elsewhere the fallback is fine.

Fallback after both Chicagos: -apple-system, 'Helvetica Neue', sans-serif. With the bundled files, the fallback should only ever be reached for scripts the fonts don't cover (Cyrillic, CJK, and so on).

Ship the fonts. Do not rely on system fonts for this look. Geneva exists only on a Mac, and only as a smooth modern redraw, and phones have neither font. Before Oct 2026 Ekybe's body text was "Geneva" and looked modern everywhere. Bundling the two TTFs fixed that.

One weight. Chicago has a single weight. Always set font-weight: normal on Chicago text, and font-synthesis: none where available: a browser-synthesised bold smears the pixel shapes. Emphasis is the face (Chicago against Chicago Light), not weight.

Greek in both faces

5.2 Scale

Sizes from Ekybe's type tokens (--fs-* in its index.css). The section label, menu and button sizes are this kit's. Line heights are unitless, so they follow the text size setting.

Role Face Size Line height
Window title Chicago 14px 20px
Pane title (channel name) Chicago 19px —
Workspace name Chicago 18px —
Author name Chicago 15px —
Body / message Chicago Light 14.5px × text scale 1.55
Navigation row Chicago Light 13.5px —
Menu item Chicago 13px 18px
Button Chicago 13px 18px
Section label (CHANNELS) Chicago 11px, letter-spacing 0.5px, upper case —
Meta: time, hint Chicago Light 11px, muted —
Badge Chicago 11px 1

5.3 Pixel crispness

Chicago is drawn on a 12-pixel em: one 1984 pixel is 1/12 of the font size. It is sharpest at 12, 18 and 24px, but every size from 11 up reads well on a 2x screen. Don't fight the anti-aliasing; turning it off makes Chicago Light uneven.

5.4 Greek

Ekybe's team writes a quarter of its messages in Greek, so Greek is first-class. Both faces cover the full monotonic Greek alphabet in Chicago's own grid: lower case, capitals, tonos, dialytika, final sigma, and ano teleia. Polytonic Greek is not covered and falls back. See §17 to extend coverage to another script.


6. Shape, lines and elevation

6.1 Corners

Everything is square (border-radius: 0). The exceptions, which are what 1984 itself made round:

Thing Radius
Presence dot 50% (circle)
Radio button 50%
Default button (§8.5) 8px
Unread badge 6px

6.2 Lines

Line Width Colour
Border of any box, field, button, row divider 1px ink
Rule under a window title bar or a pane header 2px ink
Rule above the composer / a footer bar 2px ink
Rule between menu groups 1px dotted ink
Separator between two panes side by side 1px (1pt on a phone), full height; on a phone it melts into the status bar's blur, sharp from below it ink
Disabled outline 1px (2px on the default button) muted

Panes side by side are parted by one thin ink line running their full height: no shadow under it, no dither gutter, no thick edge. On a phone the line never runs sharp through the status bar. A drawer (284pt wide) dims the conversation with rgba(10,8,20,.5) and casts one soft shadow on it (8px 0 28px rgba(0,0,0,.3)). Under the status bar iOS blurs a web view's content, strongly in the top third and fading to sharp at the bottom, so the edge between drawer and dimmed conversation goes soft at the top and the line melts away before the clock and Wi-Fi. A native app has to draw that blur itself (Agentrix's iPhone app does it row by row: a 4pt core inside a 24pt blur). A straight fade looks wrong. The drawer's header is the same height as the conversation's title bar, so their rules run straight across the edge. Drawn up through the status bar, the line or a hard dimming edge looked broken (Roberto, 5 Oct 2026). This is how Ekybe draws its sidebar against the conversation, and Roberto preferred it to Agentrix's iPhone drawer, which had a 3pt edge starting under the status bar plus a hard shadow (4 Oct 2026). A pane that can be dragged wider keeps the same line; the drag target is an invisible strip a few pixels either side of it.

The pane line on a phone (Agentrix's drawer)

The top of the line beside Ekybe's

6.3 Elevation

There are two shadows, both solid and unblurred:

Nothing has a blurred shadow, a glow, or a translucent overlay. A modal backdrop is the dither, or nothing.


7. Layout and metrics

Metric Value Why
Window title bar 28px The Mac app's own bar (§8.1)
Pane header 60px exactly, box-sizing: border-box, no vertical padding All pane headers share one height so their 2px rules meet in a single line across the window. Ekybe's sidebar header came out 62px against the channel header's 60, and the two rules visibly missed (Roberto: "why these two lines are misaligned").
Stripe band in a pane header 9px at the top Dropped when the pane is directly under a window bar (stripes on stripes)
Gap above the search field under a header 12px Other themes have no rule there; here the field would sit on it
Row padding 5px × 12px
Avatar in a message 34px 22px in lists, 18px stacked in a thread summary
Scroll bar 14px
Window box (close/mini/zoom) 13px square with a 3px paper moat
Default button outer ring 2px paper gap + 3px ink Needs 5px of margin around it

The spacing scale is the host app's. This system changes how things are drawn; where controls go is fixed by §7.1–7.4, in every look (Classic or not) and on every screen (Mac and phone).

7.1 Order of controls in a row

Every row of controls reads the same way, left to right:

Place What Examples
Leading Add things to what you're writing + (files, photos), paperclip
Then Options that change how it's sent Autonomy (Edits only / Full auto)
Middle The field, taking the free width Message box, "Type your own answer"
Trailing, in this order Status · choice of model · Queue · Stop · microphone · the primary action Working 1:12 · Opus 5.5 High · Queue · ■ · 🎙 · ↑ / Send / Next

Rules:

  1. The primary action is always last (rightmost): Send, Next, OK, the default dialog button.
  2. Add/attach is always first (leftmost), before the field, never after it.
  3. What's attached sits above the field it belongs to, each item with its own ×; pictures as thumbnails that open full screen on a tap.
  4. A control keeps its place when it appears or hides. Controls shown only in some states (Queue and Stop while the agent works) slot into their fixed position in the order above; nothing else moves round them.
  5. When the screen is too narrow, the choice of model gives way first, then status. Never the field, the add button or the primary action.
  6. A control that can't act right now is dimmed, not hidden (Queue with nothing typed, Send with an empty box), unless the whole state it belongs to is gone (Queue and Stop when nothing runs).
  7. Same words everywhere: the same label on Mac and phone ("Queue", "Next", "Send", "Type your own answer"), and the same word for the same thing across screens.

7.2 The message box

[ attached things, each with × ]
[ text, growing with what's typed                                         ]
[ + ] [autonomy]  ·····  [Working 1:12] [model ▾] [Queue] [■] [🎙] [ ↑ ]
State Mac Phone
Idle + · autonomy · model · Send + · model · mic · Send
Agent working + · autonomy · Working m:ss · model · Queue · Stop · Send + · Queue · Stop · mic · Send

While the agent works, Send interrupts it and sends now, Queue waits for its turn to end, and Stop stops it; on the Mac Return is Send and ⌥Return is Queue. Queue shows for the whole time the agent works, dimmed until there's something typed. The two differ only where the device does: the phone has a microphone, and it's too narrow for the model chip beside Queue (rule 5).

7.3 The question form

Question 2 of 4                                    ● ● ○ ○
The question, in the title face
[ picture, if the question has one ]
( ) Option one
    one short line of detail
( ) Option two
[ attached things, each with × ]
[ 📎 ] [ Type your own answer                         ] [ Next | Send ]
1. Earlier answer · 2. …                                        Back

7.4 Mac and phone are one app


8. Components

Every component is in classic-1984.css and shown in specimen.html.

8.1 Window and window title bar

.c84-window · .c84-window-bar · .c84-window-title · .c84-box.is-close | .is-mini | .is-zoom

The native platform bar must be removed, not hidden (§12.1).

8.2 Pane header

.c84-pane-bar (.is-plain drops the stripe band)

8.3 List rows and selection

.c84-row · .is-selected

8.4 Badge and presence

8.5 Buttons

.c84-button · .is-default · :disabled

State Look
Normal Paper, 1px ink border, square corners, Chicago 13/18, padding 4px 14px
Hover and pressed Reversed: ink fill, paper label
Default (what Return does: Send, OK) 2px ink border, 8px radius, plus the outer ring: box-shadow: 0 0 0 2px var(--paper), 0 0 0 5px var(--ink). Leave 5px of margin for the ring.
Disabled Muted label and muted border on paper. The default button loses its ring but keeps its rounded 2px border, so it still reads as "the button that will send". Ekybe's Send button with an empty composer is exactly this.

Icon buttons (attach, emoji, microphone) stay as the host app draws them, in ink. A pressed or "on" icon button is reversed.

8.6 Fields and controls

8.7 Menus: pull-down, context menu, submenu

.c84-menu · .c84-menu-item (.is-on, [aria-disabled]) · .c84-menu-icon · .c84-menu-check · .c84-menu-label · .c84-menu-key · .c84-menu-cap · .c84-menu-sub · .c84-menu-sep · .c84-menu-head · .c84-menu-seg — built and run by showMenu() in classic-1984.js.

Approved 4 Oct 2026 in Agentrix as version B: pure 1984, but the app's line icons are kept.

Part Spec
Box Paper, 1px ink border, 2px hard shadow, 2px vertical padding, min 180px, max 340px wide
Type Chicago 13px, line height 1
Row 27px tall, padding 0 14px 0 12px, 10px between columns, no wrapping (21px read as cramped; Roberto, 5 Oct 2026)
Columns, left to right Tick (11px, only if the menu has a checkable item) · icon (16 × 14px, only if any item has one) · label (ellipsis when too long) · shortcut (right, 18px gap) · submenu arrow (a solid right-pointing triangle, 14px gap)
Icons Line icons used as a mask, so they draw in the row's colour and reverse with it. A row without an icon keeps the empty column, so labels stay in line.
Tick A square-capped check, 11px
Under the pointer Reversed: ink row, paper text and icon
Disabled 40% ink: dimmer than muted text, never hidden
Separator 1px dotted rule, 5px above and below
Group heading Chicago 11px, muted, padding 6px 14px 3px 12px
Letter key .c84-menu-cap: the letter in a 1px box like a key cap, Chicago 11px, 16px wide at least, 18px from the label; it reverses with the row
Switch .c84-menu-seg: a choice of one inside a menu (a notification level), boxed cells in a row, the chosen one reversed

Version B

Groups and letter keys (Ekybe, approved 8 Oct 2026). A menu longer than a handful of rows is split into groups by what the rows do:

Grouped menus with letter keys and a switch

Behaviour:

The Mac title bar's zoom-box menu (§8.1) is one of these menus, opened by hovering.

On a phone (Agentrix's iPhone app, ClassicMenu.swift, 4 Oct 2026) the same menu, sized for a finger:

Part Spec
Box As above: paper, 1pt ink edge, 2pt hard shadow; up to 300pt wide (screen width − 24pt)
Type and rows Chicago 16pt on rows at least 44pt tall, 14pt side padding
Placement Next to the button that opened it: below it when the button is in the top half of the screen, above it in the bottom half; aligned to the button's nearer side. Scrolls if it is taller than the room
Pressed row Reversed while the finger is down; the choice happens on release
Sections A muted Chicago 12pt heading (Model, Effort), rows under it
Submenus Open in place, replacing the list, with a back row (‹ and the submenu's name) on top and a dotted rule under it. There is no room beside the row on a phone
Closing A tap outside closes it and does nothing else. A chosen action runs only after the menu has gone, so a sheet or alert it opens appears normally
Long-press menus Stay the system's own (they are about the pressed thing, not the app)

The 1984 menu on an iPhone

8.8 Dialog

.c84-dialog · .is-alert · .c84-dialog-message · .c84-dialog-detail · .c84-dialog-field · .c84-dialog-buttons — alerts built and run by showDialog() and showPrompt() in classic-1984.js.

The 1984 double border: 1px ink, 3px paper, 2px ink, then the 2px hard shadow. Padding is 16px × 18px. In Ekybe, a poll inside a message is drawn as a dialog box, and the button that votes is the default button.

Alerts, confirmations and prompts. Every question the app asks (Move to Trash?, Delete?, Couldn't save…, Rename…) is this box while Classic is on. The platform's own alert is never used for them: on iOS it took Classic's fonts and tint and came out broken (the field ran past its edge, the buttons went grey), and on a Mac or in a browser it is the one modern thing left on screen (Roberto, 6 Oct 2026: "every dialog themed").

Part Spec
Box .c84-dialog, 440px wide (window − 40px at most). Phone: screen − 48pt
Message Chicago 15/1.35 (phone: Chicago 17pt). It says what will happen, as a question or a statement
Detail Optional, under the message, 8px apart: Chicago Light 13/1.45, the consequences in one or two sentences
Field (prompts only) A square .c84-field the box's width, 12px under the message, with the current value selected
Buttons On the right, 12px apart, 18px under the text, at least 76px wide, labels on one line. Verbs, not Yes/No: "Move to Trash", "Delete project", "Rename"
Default button Ringed (§8.5), always the rightmost. Return presses it
Which is default The action, unless the action deletes or can't be undone: then Cancel is the default, so Return never deletes (Roberto, 6 Oct 2026). A prompt's default is its confirming button
Cancel Esc presses it. A one-button alert's only button (OK) is both
Placement Centred across, in the upper part of the window (16% down; phone: ~150–170pt from the top, so the keyboard never covers it)
Behind it Nothing dims and nothing blurs. A press outside does nothing: a modal dialog waits for an answer
Motion None: it appears and disappears
Mac iPhone
Mac confirmation iPhone confirmation iPhone prompt
import { showDialog, showPrompt } from './classic-1984.js'
if (await showDialog({ message: 'Move "Notes" to the Trash?', detail: 'You can restore it from the Trash.',
                       buttons: ['Move to Trash', 'Cancel'], destructive: 0 }) === 0) trash()
const name = await showPrompt({ message: 'Rename chat', value: oldName, confirm: 'Rename' })  // string or null

Out of scope: file and folder pickers (open, save, choose folder) stay the system's own; they are whole windows with their own navigation, not questions. Long-press menus on a phone stay the system's too (§8.7).

8.9 Scroll bars

On every scrolling area, not only some:

Scroll-bar parts are pseudo-elements, so a blanket radius reset doesn't reach them. Set them explicitly, as the kit does. Roberto, 3 Oct: "all scroll bars have rounded endings, original one had square endings."

8.10 Images and links

8.11 Notice / toast / banner

.c84-notice

Paper, 1px ink border, 2px hard shadow, Chicago 13/18. It doesn't slide in; it appears. Status is expressed in words and icons, never colour. "Notifications are off" and "Notifications are on" differ by their words and icon (a struck bell, a check), not by orange and green.

8.12 Settings panel (Appearance)

How Ekybe lays out the Classic settings, worth copying:

  1. Theme: a row of theme buttons. A small caption beside the heading says whether this device is phone or computer, set apart from the other.
  2. Message text: five buttons, each showing "Aa" at its own size with the step name under it (§11.2).
  3. These appear only while Classic is the theme:
    • Tint: five swatches, each an "Aa" in Chicago on that preset's paper and ink.
    • Hue: a slider with a rainbow track.
    • Strength: a slider, captioned "From plain black and white to fully tinted."
    • Night look: a checkbox, captioned "when your device is in dark mode. Glowing ink on dark paper."
    • Pictures: four choices in a 2 × 2 grid, each with a label and a one-line hint (§9).

A link "Appearance settings…" in the theme menu opens the panel scrolled to this section.


9. People's pictures

Each person chooses how pictures of people are drawn. Roberto liked all four, so it is a setting rather than a ruling.

The four picture styles

Live: pictures.html.

Value Label Hint Drawing
colour Colour tiles Initials on each person's colour The person's own colour tile with initials, even if they have a photo.
ink Ink only Initials in a dotted frame Chicago initials on paper, inside a 3px dithered frame with a 1px ink outline. Fully 1-bit.
photo (default) Real photos Profile photos in colour The photo, cover-cropped. No photo: the person's colour tile.
bit 1-bit photos Profile photos as dots, like MacPaint The photo turned into ink dots by Atkinson dithering, at one dot per CSS pixel. No photo: ink only.

In this kit every picture sits in a 1px ink frame (.c84-avatar). In Ekybe, pictures inside messages get it from the image-frame rule.

1-bit photos, how:


10. Interaction and motion


11. Settings a person can change

11.1 Classic preferences

Stored as one JSON object:

{ "hue": 135, "strength": 0, "night": true, "avatars": "photo" }
Key Range Default
hue 0–359, integer 135 (only matters once strength > 0)
strength 0–100, integer 0 (black and white)
night boolean true
avatars colour · ink · photo · bit photo

Always pass stored values through normalizeClassic(); it clamps and fills in defaults.

11.2 Message text size

Five steps, stored as an integer from −2 to 2. The setting changes message text and the composer only; names, sidebar and menus stay put.

Step Label Scale Body size
−2 Smaller 0.86 12.5px
−1 Small 0.93 13.5px
0 Default 1 14.5px
1 Large 1.15 16.7px
2 Largest 1.32 19.1px

11.3 Per device, per account

Ekybe keeps separate values for phones and for computers, for the theme, the Classic preferences and the text size alike (Roberto: Classic on the Mac without the phone following).


12. Platforms

12.1 Mac desktop app (Tauri)

The native title bar must go entirely; hiding parts of it does not work. Ekybe draws its own 28px bar (§8.1) and turns the window's decorations off.

Reference: Ekybe src/components/ClassicTitleBar.tsx.

12.1b Mac desktop app (Electron)

Agentrix does the same in Electron (src/main.js, renderer/classic.css):

12.2 Browser

No window bar of our own: the browser has its chrome. Pane headers keep their 9px stripe band. Scroll-bar styling is WebKit/Blink only; Firefox shows its own thin bars, coloured by color-scheme.

12.3 iPhone and Android (Capacitor web view)

12.4 Native iOS (SwiftUI)

Agentrix's iPhone app implements the same system natively in ios/AgentDeck/Classic.swift. It is the reference for a SwiftUI port.

Web SwiftUI (Agentrix)
classicColors() Look.colors(hue:strength:dark:) — the same formula
--ink, --paper Color extensions reading Look.shared
Chicago, Chicago Light Font.chicago(_:), the bundled TTFs in UIAppFonts
.c84-window + shadow ClassicBox / .box(_:_:shadow:)
.c84-dialog DialogBox
Stripes Stripes, StripedTitle
Buttons ClassicButtonStyle, ProminentButton
Lists, bars ClassicList, ClassicBar
showMenu(), .c84-menu AppMenu with MenuEntry data (ClassicMenu.swift): iOS's Menu in the other looks, the 1984 menu in Classic, from one definition
Pane separator A 1pt ink line, full height (the drawer's edge in DrawerView.swift)
showDialog(), showPrompt() .classicDialog(_:isPresented:message:sheet:buttons:) with DialogButtons, and .textPrompt(_:isPresented:text:confirm:): iOS's alert or action sheet in the other looks, the 1984 box in Classic, from one call

Presenting over everything. Menus and dialogs are a fullScreenCover with .presentationBackground(.clear) and animations off (Transaction.disablesAnimations), so they also work inside sheets and appear at once. Two things learned the hard way (Agentrix, 5–6 Oct 2026):

Register fonts by PostScript name. Agentrix uses ChicagoFLF and ChicagoFLF-Light. This kit's files are ChicagoFLF-Greek (display) and ChicagoFLF-Light (text, now with Greek); swap them in to get Greek natively too.


13. Re-skinning an existing app

This is how Ekybe added Classic as a sixth theme without rewriting its components. It is useful when the app already has design tokens.

  1. Scope everything to :root[data-theme='classic'], so other themes are untouched.

  2. Map the app's existing tokens to ink and paper. Ekybe's mapping:

    --bg: var(--paper);  --sb: var(--paper);  --card: var(--paper);
    --line: var(--ink);  --tx: var(--ink);    --ac: var(--ink);
    --mut: color-mix(in srgb, var(--ink) 64%, var(--paper));
    --acs: color-mix(in srgb, var(--ink) 12%, var(--paper));
    --hov: color-mix(in srgb, var(--ink) 9%, var(--paper));
    --unread: var(--ink);  --unread-tx: var(--paper);
    --p-active: var(--ink); --p-away: color-mix(in srgb, var(--ink) 50%, var(--paper)); --p-off: var(--paper);
    --bad: var(--ink);  --send-bg: var(--paper);  --send-fg: var(--ink);
    --radius: 0px;  --shadow: 2px 2px 0 var(--ink);  --shadow-sm: 1px 1px 0 var(--ink);
    --serif: 'Chicago', …;  --font: 'Chicago Light', …;  --mono: Monaco, …;
    
  3. Square everything with one blanket rule. Most radii in a React app are inline numbers no token reaches:

    :root[data-theme='classic'] *, …::before, …::after { border-radius: 0 !important; text-shadow: none !important; }
    

    Then bring back the round exceptions (§6.1) with !important too.

  4. Re-map inline white. Apps write color: #fff for text on the accent, and React serialises it as rgb(255, 255, 255):

    :root[data-theme='classic'] [style*='color: rgb(255, 255, 255)'] { color: var(--paper) !important; }
    
  5. Hook components with marker classes, not new components. Ekybe added classes such as:

    • cf-titlebar (pane headers)
    • cf-navrow-on (selected row)
    • cf-emit-send (default button)
    • cf-send (disabled send)
    • cf-dialog (poll)
    • cf-display (titles)
    • cf-dot, cf-emit-unread
    • cf-av-ink, cf-av-bit (pictures)
    • cf-msg (message, for image frames and links)
    • cf-chrome (Mac bar present)

    They do nothing in other themes.

  6. Set the two colours from script on <html>. Cache the prefs locally, so the first paint uses the right ink before the account loads.

  7. Pictures: route every avatar through one component that reads the picture style from a tiny store. Ekybe draws Avatar in 19 places, so this is a store rather than a prop.


14. Accessibility