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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ turns the section for that version into the release notes shown in the app.
fix still corrects spelling with macOS's own spell checker, leaving names
and your taught words alone.

### Changed
- The icon's bars are a little heavier, to match the wordmark (GDR-0021).

### Fixed
- Fix, shorten and plain no longer touch Slack mentions and channels, links,
email addresses, dates, times, units, versions or abbreviations like
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/Timbre/Sources/Assets.xcassets/AppIcon.appiconset/icon_128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/Timbre/Sources/Assets.xcassets/AppIcon.appiconset/icon_256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/Timbre/Sources/Assets.xcassets/AppIcon.appiconset/icon_32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/Timbre/Sources/Assets.xcassets/AppIcon.appiconset/icon_512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/Timbre/Sources/Assets.xcassets/AppIcon.appiconset/icon_64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ Two kinds of records live here. The boundary rule:
| [0018](gdr/0018-words-appear-as-you-speak.md) | Words appear as you speak; the cleanup replaces them on release | Accepted |
| [0019](gdr/0019-outfit-and-a-simpler-drawing.md) | Outfit, served by the site; the keyboard goes; keys are flat (amends 0017) | Accepted |
| [0020](gdr/0020-nunito.md) | Nunito: the rounded face (amends 0019) | Accepted |
| [0021](gdr/0021-the-wordmark.md) | The wordmark: bars beside "timbre", stroke 3; an underlined secondary (amends 0019, 0017) | Accepted |
| [0022](gdr/0022-nunito-sans-for-text.md) | Nunito Sans for text; Nunito keeps the headlines (amends 0020) | Accepted |
23 changes: 23 additions & 0 deletions docs/decisions/gdr/0021-the-wordmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 0021 — The wordmark: the bare bars beside "timbre", stroke 3, with an underlined secondary

- **Status:** Accepted
- **Date:** 2026-10-14
- **Amends:** [GDR-0019](0019-outfit-and-a-simpler-drawing.md), point 5; [GDR-0017](0017-retro-identity-one-colour-per-gesture.md), point 3 (where the rainbow sits)

## Context

With Nunito chosen (GDR-0020), the wordmark was laid out on the Brand room in every arrangement that was still open: title case and lowercase, the mark tiled and bare, the rounded rainbow under the word, beside it, or absent, and the bars at three weights. Each round narrowed by looking: lowercase first, then the tile dropped beside the word ("if we show the icon with the wordmark we shouldn't show it with a background"), then the doubled rainbow dropped, then the weight.

## Decision

1. **The primary wordmark is the bare bars beside "timbre"**, lowercase, Nunito at its heaviest, no underline. The rainbow appears once in a lockup: in the bars. **The secondary is "timbre" with the rounded rainbow beneath it**, no bars — for the places the tiled icon already stands (an App Store listing, a window with the icon in its title) or where the bars would repeat. Never both rainbows in one lockup.
2. **The bars are drawn at stroke 3 of the 32-unit grid**, up from 2.6, everywhere they appear: the favicon, the app icon, the header, the share image. Heavy enough to sit beside Nunito's extra-bold; light enough that the sixteen-pixel icon keeps its gaps.
3. **The tile stays for the icon alone** — the favicon, the Dock, the update prompt — where a box is wanted. Beside the word the tile would be a second box.
4. **The name in prose stays "Timbre."** Lowercase is the drawing, not the spelling.
5. The rounded rainbow underline keeps its place in the headline, under one word, and the stripe keeps its places between sections. The primary wordmark does not carry it; the secondary is where it belongs.

## Consequences

- One lockup, drawn once (`Mark.svelte` with `tile={false}`), used by the site header and footer, the Backstage sidebar, the login card and the share image. The secondary is the same word with the `rainbow` bar beneath; the Brand room shows both.
- The app icon set and the favicon are regenerated from the same stroke; `tools/app-icon.swift` and `tools/og-image.swift` carry the number.
- The Brand room keeps every round so the next question starts from the comparison.
21 changes: 21 additions & 0 deletions docs/decisions/gdr/0022-nunito-sans-for-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 0022 — Nunito Sans for text; Nunito keeps the headlines

- **Status:** Accepted
- **Date:** 2026-10-14
- **Amends:** [GDR-0020](0020-nunito.md), point 1

## Context

Nunito everywhere was friendly and, at paragraph size, a little tiring: rounded terminals on every letter of a long line. Six pairings went on the Brand room, Nunito over a text face each, with the wordmark, a headline, a paragraph and a button. Nunito Sans was chosen, with Inter named as also good.

## Decision

1. **Headlines and the wordmark stay Nunito**, at its heaviest, as GDR-0020 and GDR-0021 have it. They are `--font-display`.
2. **Text is Nunito Sans**: regular for paragraphs, semibold for emphasis and buttons. It was drawn as Nunito's companion — the same proportions, square terminals — so the two read as one voice at two sizes. It is `--font-sans`.
3. **Inter is the recorded runner-up.** If Nunito Sans ever reads wrong, Inter is the next thing tried, not another round.
4. Both are served by the site, under the OFL, like everything else in `web/static/fonts`.

## Consequences

- Two faces, one family, no decorative third. Labels stay IBM Plex Mono.
- The Brand room marks the pairing in use and keeps the other five.
2 changes: 1 addition & 1 deletion tools/app-icon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ NSBezierPath(roundedRect: NSRect(x: inset, y: inset, width: tileSide, height: ti
let grid = tileSide / 32
let centres: [Double] = [8, 13.3, 18.6, 24]
let heights: [Double] = [6, 14, 9, 3]
let width = 2.6 * grid
let width = 3.0 * grid
for (i, cx) in centres.enumerated() {
let h = heights[i] * grid + width // round caps extend a stroke by half its width each end
let x = inset + cx * grid - width / 2
Expand Down
23 changes: 18 additions & 5 deletions tools/og-image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func drawMark(x: Double, y: Double, side: Double) {
caseColor.setFill()
NSBezierPath(roundedRect: NSRect(x: x, y: y, width: side, height: side), xRadius: side * 7 / 32, yRadius: side * 7 / 32).fill()
let grid = side / 32
let barWidth = 2.6 * grid
let barWidth = 3.0 * grid
for (i, cx) in [8.0, 13.3, 18.6, 24.0].enumerated() {
let h = [6.0, 14.0, 9.0, 3.0][i] * grid + barWidth
[rainbow[0], rainbow[2], rainbow[3], rainbow[4]][i].setFill()
Expand All @@ -81,20 +81,33 @@ func drawMark(x: Double, y: Double, side: Double) {
}
drawMark(x: 880, y: 200, side: 260)

// Wordmark, with the small mark beside it, as the header shows it.
drawMark(x: 80, y: 446, side: 64)
draw("Timbre", at: NSPoint(x: 160, y: 436), font: nunito(72, weight: 800), color: ink, kern: -1)
// The tiled icon already stands on this card, so the wordmark is the
// secondary: the word with the rounded rainbow beneath, no bars (GDR-0021).
let wordmark = nunito(76, weight: 800)
draw("timbre", at: NSPoint(x: 80, y: 430), font: wordmark, color: ink, kern: -1.5)
let wordWidth = size(of: "timbre", font: wordmark, kern: -1.5).width
let wordBand = wordWidth / Double(rainbow.count)
NSGraphicsContext.saveGraphicsState()
NSBezierPath(roundedRect: NSRect(x: 82, y: 420, width: wordWidth - 4, height: 9), xRadius: 4.5, yRadius: 4.5).addClip()
for (i, color) in rainbow.enumerated() {
color.setFill()
NSRect(x: 82 + Double(i) * wordBand, y: 420, width: wordBand + 0.5, height: 9).fill()
}
NSGraphicsContext.restoreGraphicsState()

// The claim, wrapped by hand, with the rainbow under "never".
let headline = nunito(58, weight: 800)
draw("Dictation that", at: NSPoint(x: 80, y: 318), font: headline, color: ink, kern: -1)
draw("never leaves your Mac.", at: NSPoint(x: 80, y: 250), font: headline, color: ink, kern: -1)
let neverWidth = size(of: "never", font: headline, kern: -1).width
let band = neverWidth / Double(rainbow.count)
NSGraphicsContext.saveGraphicsState()
NSBezierPath(roundedRect: NSRect(x: 80, y: 242, width: neverWidth, height: 8), xRadius: 4, yRadius: 4).addClip()
for (i, color) in rainbow.enumerated() {
color.setFill()
NSRect(x: 80 + Double(i) * band, y: 246, width: band + 0.5, height: 6).fill()
NSRect(x: 80 + Double(i) * band, y: 242, width: band + 0.5, height: 8).fill()
}
NSGraphicsContext.restoreGraphicsState()

draw("Zero network requests* · No account · Free · Open source", at: NSPoint(x: 82, y: 170), font: mono, color: muted)
draw("timbre.hugopretorius.dev", at: NSPoint(x: 82, y: 124), font: mono, color: ink)
Expand Down
53 changes: 33 additions & 20 deletions web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);

--font-sans: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-sans: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: "Nunito", "Nunito Sans", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
}

Expand All @@ -169,12 +170,13 @@
-webkit-font-smoothing: antialiased;
}
/* Headlines: Nunito at its heaviest, rounded terminals and all — the
friendliness Hugo asked for, in the same family as the text so the
page has one voice (GDR-0020). Rounded faces want less tightening. */
friendliness Hugo asked for (GDR-0020). Text is Nunito Sans, its own
companion: the same bones, square terminals, easier at small sizes
(GDR-0022). Rounded faces want less tightening. */
h1,
h2,
h3 {
font-family: var(--font-sans);
font-family: var(--font-display);
font-weight: 800;
letter-spacing: -0.015em;
line-height: 1.08;
Expand All @@ -187,7 +189,7 @@

/* The headline cut, for anything that is not a heading but should carry it. */
@utility display {
font-family: var(--font-sans);
font-family: var(--font-display);
font-weight: 800;
letter-spacing: -0.015em;
line-height: 1.08;
Expand Down Expand Up @@ -215,21 +217,32 @@
var(--retro-violet) 83.33% 100%
);
}
/* A word underlined with the rainbow: the one emphasis the headline gets. */
@utility underline-rainbow {
background-image: linear-gradient(
90deg,
var(--retro-red) 0 16.66%,
var(--retro-orange) 16.66% 33.33%,
var(--retro-yellow) 33.33% 50%,
var(--retro-green) 50% 66.66%,
var(--retro-blue) 66.66% 83.33%,
var(--retro-violet) 83.33% 100%
);
background-repeat: no-repeat;
background-size: 100% 0.14em;
background-position: 0 92%;
padding-bottom: 0.04em;
/* A word underlined with the rainbow: the one emphasis the headline gets.
A rounded bar beneath the word (GDR-0020's rounded face wants rounded
ends everywhere the rainbow sits). */
@layer components {
.underline-rainbow {
position: relative;
display: inline-block;
}
.underline-rainbow::after {
content: "";
position: absolute;
left: 0.02em;
right: 0.02em;
bottom: -0.06em;
height: 0.14em;
border-radius: 999px;
background-image: linear-gradient(
90deg,
var(--retro-red) 0 16.66%,
var(--retro-orange) 16.66% 33.33%,
var(--retro-yellow) 33.33% 50%,
var(--retro-green) 50% 66.66%,
var(--retro-blue) 66.66% 83.33%,
var(--retro-violet) 83.33% 100%
);
}
}
@utility rainbow-text {
background-image: linear-gradient(
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 19 additions & 14 deletions web/src/lib/components/marketing/Mark.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
<script lang="ts">
/// The mark: four bars, the shape of a voice, on a charcoal tile — the
/// favicon and the app icon, drawn inline so it takes the palette from
/// the page and stays crisp at any size.
let { size = 28, class: className = '' }: { size?: number; class?: string } = $props();
/// The mark: four bars, the shape of a voice. On a charcoal tile it is the
/// favicon and the app icon; without the tile it is the bare glyph, for
/// sitting beside the wordmark where a tile would be a second box.
let {
size = 28,
tile = true,
weight = 3,
class: className = ''
}: { size?: number; tile?: boolean; weight?: number; class?: string } = $props();

// Without the tile the box hugs the bars (they span x 6.7–25.3, y 7.7–24.3
// of the 32-unit grid), so the glyph sits on the text's line.
const box = $derived(tile ? '0 0 32 32' : `${8 - weight / 2 - 0.4} ${9 - weight / 2 - 0.4} ${16 + weight + 0.8} ${14 + weight + 0.8}`);
const width = $derived(tile ? size : Math.round((size * (16 + weight + 0.8)) / (14 + weight + 0.8)));
</script>

<svg
viewBox="0 0 32 32"
width={size}
height={size}
class={className}
aria-hidden="true"
focusable="false"
>
<rect width="32" height="32" rx="7" fill="var(--case)" />
<g stroke-width="2.6" stroke-linecap="round">
<svg viewBox={box} {width} height={size} class={className} aria-hidden="true" focusable="false">
{#if tile}
<rect width="32" height="32" rx="7" fill="var(--case)" />
{/if}
<g stroke-width={weight} stroke-linecap="round">
<line x1="8" y1="13" x2="8" y2="19" stroke="var(--retro-red)" />
<line x1="13.3" y1="9" x2="13.3" y2="23" stroke="var(--retro-yellow)" />
<line x1="18.6" y1="11.5" x2="18.6" y2="20.5" stroke="var(--retro-green)" />
Expand Down
Loading
Loading