Brand mark: scallop ribbon shape, app icon in light / dark / tinted
CI / build-and-deploy (push) Successful in 37s
CI / build-and-deploy (pull_request) Successful in 20s

The mark is one closed path with a scalloped baseline — four tails, rounded
cap — rather than a stack of separate ribbons. That is the whole design
decision. A logo gets tested at 16pt in a Spotlight result and in iOS 26's
tinted and monochrome icon modes, where the palette is discarded and only the
silhouette survives. Arrangements of N separate ribbons collapse into a blob
under both, because the shapes were only ever told apart by colour; a
scalloped outline is told apart by its own edge, so it reads the same tinted,
monochrome, and 16pt tall.

ScallopMark defines geometry in a canonical 88x98 box and fits it to whatever
rect it is handed, so proportions cannot drift with the container. MarksMark
adds the three bands as a hard-stopped gradient, keeping it a single fillable
shape. The bands are Paper.swatchPairs entries rather than bespoke brand
colours: the app already hashes every bookmark into that palette, so the mark
is made of the same ink as the content.

App icon ships all three variants per Apple's iOS 18+ model — light flattened
(iOS rejects an alpha channel there), dark and tinted on transparency for the
system to composite. The tinted variant carries three greys ordered by the
colour version's relative luminance rather than one flat grey, so the band
reading survives the one mode that has nothing else left.

The PNGs are build output: edit scripts/appicon/appicon.html and re-run
generate.sh. docs/logo-variants.html and docs/logo-c1-variants.html are the
exploration the choice came out of.

Verified: SwiftUI shape rendered through ImageRenderer matches the SVG at
every size from 160pt to 16pt; assetutil confirms all three renditions in the
compiled catalog with the correct appearance tags; installed on a simulator
and checked against the home screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-08-03 11:55:39 -05:00
co-authored by Claude Opus 5
parent b3d8719874
commit bc65bb586c
10 changed files with 1073 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Marks — C1 development</title>
<style>
:root{--ink:#14110C;--rule:rgba(20,17,12,.16);
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
--serif:ui-serif,Georgia,"Times New Roman",serif}
*{box-sizing:border-box}
body{margin:0;background:#EFEBE3;color:var(--ink);font-family:var(--mono);font-size:12px;-webkit-font-smoothing:antialiased}
header{padding:56px 40px 28px;border-bottom:1px solid var(--rule)}
h1{font-family:var(--serif);font-size:34px;font-weight:400;margin:0 0 8px}
header p{margin:0;max-width:66ch;line-height:1.7;opacity:.62}
section{padding:40px;border-bottom:1px solid var(--rule)}
h2{font-family:var(--serif);font-size:22px;font-weight:400;margin:0 0 4px}
.sub{opacity:.55;margin:0 0 28px;line-height:1.7;max-width:74ch}
.tag{display:inline-block;font-size:10px;letter-spacing:.14em;text-transform:uppercase;opacity:.45;margin-bottom:10px}
em{font-style:normal;border-bottom:1px solid rgba(20,17,12,.3)}
/* geometry is shared; colour is injected */
.pal {--c1:#22C7F0;--c2:#8B4DEE;--c3:#5E7CF5;--bg:#fff}
.p-orig {--c1:#22C7F0;--c2:#8B4DEE;--c3:#5E7CF5;--bg:#fff}
.p-paper{--c1:#001A55;--c2:#ED663F;--c3:#FECD00;--bg:#F8F5EF}
.p-ink {--c1:#14110C;--c2:#ED663F;--c3:#E0D1BB;--bg:#F8F5EF}
.p-tri {--c1:#FECD00;--c2:#ED663F;--c3:#115AB5;--bg:#F8F5EF}
.p-mono {--c1:#14110C;--c2:#14110C;--c3:#14110C;--bg:#F8F5EF}
.p-dark {--c1:#5B8FD0;--c2:#E07A5C;--c3:#D8AD10;--bg:#15130F}
.grid{display:flex;flex-wrap:wrap;gap:18px}
.card{background:var(--bg);border:1px solid var(--rule);border-radius:14px;padding:22px;width:243px}
.card .stage{display:flex;align-items:center;justify-content:center;height:150px}
.card svg{display:block}
.card h3{font-family:var(--serif);font-size:15px;font-weight:400;margin:14px 0 5px}
.card p{margin:0;font-size:10.5px;line-height:1.65;opacity:.62}
.card .own{margin-top:8px;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;opacity:.5}
table{border-collapse:collapse}
th{font-weight:400;font-size:10px;letter-spacing:.1em;text-transform:uppercase;opacity:.45;padding:0 0 12px;text-align:center}
th.rowh{text-align:right;padding-right:16px;text-transform:none;letter-spacing:0;font-size:11px;opacity:.7;white-space:nowrap}
td{padding:5px}
td .cell{width:92px;height:92px;border-radius:11px;background:var(--bg);border:1px solid var(--rule);display:flex;align-items:center;justify-content:center}
.ladder{display:flex;align-items:flex-end;gap:22px;background:var(--bg);border:1px solid var(--rule);border-radius:14px;padding:22px 26px;margin-bottom:12px}
.ladder .n{font-size:9.5px;opacity:.4;text-align:center;margin-top:8px}
.ladder .item{display:flex;flex-direction:column;align-items:center}
.ladder .name{font-family:var(--serif);font-size:14px;width:118px;opacity:.8}
.icons{display:flex;flex-wrap:wrap;gap:20px;align-items:flex-end}
.icon{width:118px;height:118px;border-radius:26px;display:flex;align-items:center;justify-content:center;background:var(--bg);box-shadow:0 6px 18px rgba(20,17,12,.14)}
.icon.sm{width:58px;height:58px;border-radius:13px}
.icoWrap{display:flex;flex-direction:column;align-items:center;gap:10px}
.icoWrap span{font-size:10px;opacity:.5}
.lockup{display:flex;align-items:center;gap:20px;background:var(--bg);border:1px solid var(--rule);border-radius:14px;padding:32px 40px;margin-bottom:12px}
.lockup .wm{font-family:var(--serif);font-size:50px;letter-spacing:-.02em;color:var(--ink)}
.lockup.p-dark .wm{color:#F1ECE1}
.lockup .tl{font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.5;margin-top:6px}
.lockup.p-dark .tl{color:#F1ECE1;opacity:.5}
.pattern{display:grid;grid-template-columns:repeat(13,54px);background:var(--bg);border-radius:14px;padding:20px;width:742px;overflow:hidden}
</style>
</head>
<body>
<header>
<h1>C1 — three ribbons, developed</h1>
<p>C1 had the most robust silhouette of the seven and the least ownership: three bookmark ribbons is a shape any read-later app could reach for. Ownability is the thing to attack, so each variant below adds one specific idea — a rhythm, a structure, a colour rule — and the caption says whether it actually buys anything.</p>
</header>
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
<!-- a. Stepped — the baseline from the first sheet. Tops aligned, bottoms
stepped long/longest/short. -->
<symbol id="v-step" viewBox="0 0 120 120">
<path fill="var(--c1)" d="M24 14 H36 A6 6 0 0 1 42 20 V100 L30 86 L18 100 V20 A6 6 0 0 1 24 14 Z"/>
<path fill="var(--c2)" d="M54 14 H66 A6 6 0 0 1 72 20 V112 L60 98 L48 112 V20 A6 6 0 0 1 54 14 Z"/>
<path fill="var(--c3)" d="M84 14 H96 A6 6 0 0 1 102 20 V92 L90 78 L78 92 V20 A6 6 0 0 1 84 14 Z"/>
</symbol>
<!-- b. Stair — monotonic descent instead of an arbitrary step. Directional,
so it reads as progression rather than decoration. -->
<symbol id="v-stair" viewBox="0 0 120 120">
<path fill="var(--c1)" d="M24 14 H36 A6 6 0 0 1 42 20 V92 L30 78 L18 92 V20 A6 6 0 0 1 24 14 Z"/>
<path fill="var(--c2)" d="M54 14 H66 A6 6 0 0 1 72 20 V102 L60 88 L48 102 V20 A6 6 0 0 1 54 14 Z"/>
<path fill="var(--c3)" d="M84 14 H96 A6 6 0 0 1 102 20 V112 L90 98 L78 112 V20 A6 6 0 0 1 84 14 Z"/>
</symbol>
<!-- c. Tally — four thin equal ribbons. Tally marks and bookmarks are the
same shape, and the app is called Marks. The pun is the ownership. -->
<symbol id="v-tally" viewBox="0 0 120 120">
<path fill="var(--c1)" d="M21 16 H27 A4 4 0 0 1 31 20 V104 L24 94 L17 104 V20 A4 4 0 0 1 21 16 Z"/>
<path fill="var(--c2)" d="M45 16 H51 A4 4 0 0 1 55 20 V104 L48 94 L41 104 V20 A4 4 0 0 1 45 16 Z"/>
<path fill="var(--c1)" d="M69 16 H75 A4 4 0 0 1 79 20 V104 L72 94 L65 104 V20 A4 4 0 0 1 69 16 Z"/>
<path fill="var(--c3)" d="M93 16 H99 A4 4 0 0 1 103 20 V104 L96 94 L89 104 V20 A4 4 0 0 1 93 16 Z"/>
</symbol>
<!-- d. Rail — hung from a bar. Restores the shelf/box-file reading the
reference had and C1 threw away, for the cost of one rectangle. -->
<symbol id="v-rail" viewBox="0 0 120 120">
<path fill="var(--c2)" d="M22 30 H44 V104 L33 91 L22 104 Z"/>
<path fill="var(--c3)" d="M49 30 H71 V112 L60 99 L49 112 Z"/>
<path fill="var(--c2)" d="M76 30 H98 V96 L87 83 L76 96 Z"/>
<path fill="var(--c1)" d="M20 14 H100 A6 6 0 0 1 106 20 V34 H14 V20 A6 6 0 0 1 20 14 Z"/>
</symbol>
<!-- e. Overlap — wide ribbons that cross, multiplied. This is the trick the
reference actually runs on: depth from two flat colours, no shadow. -->
<symbol id="v-overlap" viewBox="0 0 120 120">
<g style="isolation:isolate">
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M16 14 H42 A8 8 0 0 1 50 22 V100 L29 84 L8 100 V22 A8 8 0 0 1 16 14 Z"/>
<path style="mix-blend-mode:multiply" fill="var(--c2)" d="M47 14 H73 A8 8 0 0 1 81 22 V112 L60 96 L39 112 V22 A8 8 0 0 1 47 14 Z"/>
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M78 14 H104 A8 8 0 0 1 112 22 V94 L91 78 L70 94 V22 A8 8 0 0 1 78 14 Z"/>
</g>
</symbol>
<!-- f. Foreground — one ribbon pulled forward and keylined in the ground
colour. Asymmetry is cheap ownership; symmetry is what makes C1 generic. -->
<symbol id="v-front" viewBox="0 0 120 120">
<path fill="var(--c1)" d="M26 14 H40 A6 6 0 0 1 46 20 V94 L33 81 L20 94 V20 A6 6 0 0 1 26 14 Z"/>
<path fill="var(--c1)" d="M80 14 H94 A6 6 0 0 1 100 20 V94 L87 81 L74 94 V20 A6 6 0 0 1 80 14 Z"/>
<path fill="var(--c2)" stroke="var(--bg)" stroke-width="5" stroke-linejoin="round"
d="M50 24 H70 A8 8 0 0 1 78 32 V114 L60 96 L42 114 V32 A8 8 0 0 1 50 24 Z"/>
</symbol>
<!-- g. Fan — the same ribbon rotated. Reads as pages fanned rather than
objects lined up; the only variant with any motion in it. -->
<symbol id="v-fan" viewBox="0 0 120 120">
<path fill="var(--c1)" transform="rotate(-15 60 110)" d="M53 18 H67 A6 6 0 0 1 73 24 V104 L60 90 L47 104 V24 A6 6 0 0 1 53 18 Z"/>
<path fill="var(--c3)" transform="rotate(15 60 110)" d="M53 18 H67 A6 6 0 0 1 73 24 V104 L60 90 L47 104 V24 A6 6 0 0 1 53 18 Z"/>
<path fill="var(--c2)" d="M53 14 H67 A6 6 0 0 1 73 20 V108 L60 94 L47 108 V20 A6 6 0 0 1 53 14 Z"/>
</symbol>
<!-- h. Pair — two, not three. Fewer parts survive smaller; the question is
whether two still reads as "a collection". -->
<symbol id="v-pair" viewBox="0 0 120 120">
<path fill="var(--c1)" d="M33 14 H49 A7 7 0 0 1 56 21 V100 L41 84 L26 100 V21 A7 7 0 0 1 33 14 Z"/>
<path fill="var(--c2)" d="M71 14 H87 A7 7 0 0 1 94 21 V112 L79 96 L64 112 V21 A7 7 0 0 1 71 14 Z"/>
</symbol>
<!-- i. Nested — concentric rather than side by side. A notch inside a notch
inside a notch: an archive, not a row. -->
<symbol id="v-nest" viewBox="0 0 120 120">
<path fill="var(--c1)" d="M26 14 H94 A10 10 0 0 1 104 24 V112 L60 92 L16 112 V24 A10 10 0 0 1 26 14 Z"/>
<path fill="var(--c3)" d="M42 14 H78 A8 8 0 0 1 86 22 V92 L60 77 L34 92 V22 A8 8 0 0 1 42 14 Z"/>
<path fill="var(--c2)" d="M54 14 H66 A6 6 0 0 1 72 20 V68 L60 58 L48 68 V20 A6 6 0 0 1 54 14 Z"/>
</symbol>
<!-- j. Scallop — one shape, four tails. Everything above is N objects; this
is a single path, which is what the tinted/mono icon modes want. -->
<symbol id="v-scallop" viewBox="0 0 120 120">
<clipPath id="clipScallop">
<path d="M26 14 H94 A10 10 0 0 1 104 24 V112 L89 96 L75 112 L60 96 L45 112 L31 96 L16 112 V24 A10 10 0 0 1 26 14 Z"/>
</clipPath>
<g clip-path="url(#clipScallop)">
<rect x="0" y="0" width="45" height="120" fill="var(--c1)"/>
<rect x="45" y="0" width="30" height="120" fill="var(--c2)"/>
<rect x="75" y="0" width="45" height="120" fill="var(--c3)"/>
</g>
</symbol>
</defs></svg>
<section>
<span class="tag">01</span>
<h2>Ten developments</h2>
<p class="sub">Shown in the app's own palette (navy / vermilion / yellow from <code>Paper.swatchPairs</code>) rather than the reference cyan-violet, since that was the conclusion of the last sheet. The <em>ownable?</em> line is the honest verdict, not a summary.</p>
<div class="grid" id="cards"></div>
</section>
<section>
<span class="tag">02</span>
<h2>Palette</h2>
<p class="sub">The three-swatch column is the interesting one. The app already hashes bookmarks into a twelve-swatch palette — a mark built from three of those swatches is describing the app's actual data model, which is ownership no competitor can copy without copying the system underneath it.</p>
<table id="matrix"></table>
</section>
<section>
<span class="tag">03</span>
<h2>Size ladder</h2>
<p class="sub">128 / 64 / 40 / 24 / 16&nbsp;px. C1's whole argument is that it survives this; the developments have to survive it too or they've bought ownership with legibility.</p>
<div id="ladders"></div>
</section>
<section>
<span class="tag">04</span>
<h2>App icon</h2>
<p class="sub">Squircle at 118 and 58&nbsp;px, mark at ~62% of the tile.</p>
<div class="icons" id="icons"></div>
<div class="icons" id="iconsSm" style="margin-top:22px"></div>
</section>
<section>
<span class="tag">05</span>
<h2>Lockups</h2>
<div id="lockups"></div>
</section>
<section style="border-bottom:none">
<span class="tag">06</span>
<h2>Pattern</h2>
<p class="sub">The test C1 is most likely to win: a repeating field is where a simple silhouette beats a clever one.</p>
<div class="pattern pal p-paper" id="pat"></div>
</section>
<script>
const VARIANTS = [
{id:'v-step', name:'C1·a Stepped', desc:'The baseline. Tops aligned, bottoms stepped. Balanced and readable, but the step pattern is arbitrary — nothing decides it, so nothing defends it.', own:'ownable? — no'},
{id:'v-stair', name:'C1·b Stair', desc:'Same three ribbons, monotonic descent. Now the rhythm means something (progression, a queue getting shorter) and the eye has a direction to travel.', own:'ownable? — a little'},
{id:'v-tally', name:'C1·c Tally', desc:'Four thin ribbons. A tally mark and a bookmark ribbon are already the same shape, and the app is called Marks — the mark states the name instead of illustrating the category.', own:'ownable? — yes'},
{id:'v-rail', name:'C1·d Rail', desc:'Hung from a bar. Buys back the shelf/box-file reading that made the reference work three ways, for the cost of one rectangle.', own:'ownable? — yes'},
{id:'v-overlap', name:'C1·e Overlap', desc:'Wide ribbons crossing, multiplied. This is the trick the reference actually runs on: depth out of two flat colours and no shadow at all.', own:'ownable? — a little'},
{id:'v-front', name:'C1·f Foreground', desc:'One ribbon pulled forward and keylined in the ground colour. Asymmetry is cheap ownership — the symmetry is most of why the baseline feels anonymous.', own:'ownable? — a little'},
{id:'v-fan', name:'C1·g Fan', desc:'The same ribbon rotated about a point below the mark. Reads as pages fanned rather than objects filed. The only one with motion, and the only one that animates for free.', own:'ownable? — yes'},
{id:'v-pair', name:'C1·h Pair', desc:'Two instead of three. Calmer and it holds smaller, but two things read as a pair, not a collection — which is the wrong idea for an archive.', own:'ownable? — no'},
{id:'v-nest', name:'C1·i Nested', desc:'Concentric instead of side by side: a notch inside a notch inside a notch. Says archive rather than row, and it is the only one that suggests depth without overlap.', own:'ownable? — yes'},
{id:'v-scallop', name:'C1·j Scallop', desc:'One path, four tails. Everything above is N objects fighting for space; this is a single shape, which is what iOS 26 tinted and monochrome icon modes actually want.', own:'ownable? — yes'},
]
const PALETTES = [
{cls:'p-paper', label:'Navy / Verm.'},
{cls:'p-tri', label:'Three swatches'},
{cls:'p-ink', label:'Ink / Verm.'},
{cls:'p-mono', label:'Ink only'},
{cls:'p-dark', label:'Dark'},
{cls:'p-orig', label:'Reference'},
]
const NS = 'http://www.w3.org/2000/svg'
const mark = (id, size, cls) => {
const svg = document.createElementNS(NS, 'svg')
svg.setAttribute('width', size); svg.setAttribute('height', size)
svg.setAttribute('viewBox', '0 0 120 120')
if (cls) svg.setAttribute('class', cls)
const use = document.createElementNS(NS, 'use')
use.setAttribute('href', '#' + id)
svg.appendChild(use)
return svg
}
const el = (tag, cls, html) => {
const n = document.createElement(tag)
if (cls) n.className = cls
if (html != null) n.innerHTML = html
return n
}
// 01 — cards
const cards = document.getElementById('cards')
for (const v of VARIANTS) {
const card = el('div', 'card pal p-paper')
const stage = el('div', 'stage'); stage.appendChild(mark(v.id, 132))
card.append(stage, el('h3', null, v.name), el('p', null, v.desc), el('div', 'own', v.own))
cards.appendChild(card)
}
// 02 — matrix
const matrix = document.getElementById('matrix')
const head = document.createElement('tr')
head.appendChild(el('th', 'rowh'))
for (const p of PALETTES) head.appendChild(el('th', null, p.label))
matrix.appendChild(head)
for (const v of VARIANTS) {
const row = document.createElement('tr')
row.appendChild(el('th', 'rowh', v.name))
for (const p of PALETTES) {
const td = document.createElement('td')
const cell = el('div', 'cell pal ' + p.cls)
cell.appendChild(mark(v.id, 68))
td.appendChild(cell); row.appendChild(td)
}
matrix.appendChild(row)
}
// 03 — size ladder, for the variants that claimed ownership
const ladders = document.getElementById('ladders')
for (const v of VARIANTS.filter(v => v.own.endsWith('yes'))) {
const row = el('div', 'ladder pal p-paper')
row.appendChild(el('div', 'name', v.name))
for (const s of [128, 64, 40, 24, 16]) {
const item = el('div', 'item')
item.appendChild(mark(v.id, s))
item.appendChild(el('div', 'n', s))
row.appendChild(item)
}
ladders.appendChild(row)
}
// 04 — icons
const ICONS = [
['v-tally', 'p-paper'], ['v-rail', 'p-tri'], ['v-fan', 'p-paper'],
['v-nest', 'p-ink'], ['v-scallop','p-tri'], ['v-scallop', 'p-dark'],
]
for (const [wrap, size, big] of [['icons', 72, true], ['iconsSm', 36, false]]) {
const host = document.getElementById(wrap)
for (const [id, cls] of ICONS) {
const w = el('div', 'icoWrap')
const tile = el('div', 'icon pal ' + cls + (big ? '' : ' sm'))
tile.appendChild(mark(id, size))
w.append(tile, el('span', null, big ? id.slice(2) + ' · ' + cls.slice(2) : '58pt'))
host.appendChild(w)
}
}
// 05 — lockups
const lockups = document.getElementById('lockups')
for (const [id, cls] of [['v-tally','p-paper'], ['v-rail','p-tri'], ['v-scallop','p-ink'], ['v-fan','p-dark']]) {
const row = el('div', 'lockup pal ' + cls)
row.appendChild(mark(id, 64))
const txt = el('div')
txt.append(el('div', 'wm', 'Marks'), el('div', 'tl', 'Everything you saved'))
row.appendChild(txt)
lockups.appendChild(row)
}
// 06 — pattern. Row offset stops the palette cycle lining up into stripes.
const cycle = ['p-paper', 'p-tri', 'p-ink', 'p-dark'], COLS = 13
const pat = document.getElementById('pat')
for (let i = 0; i < COLS * 5; i++) {
pat.appendChild(mark('v-tally', 54, 'pal ' + cycle[(i + Math.floor(i / COLS)) % cycle.length]))
}
</script>
</body>
</html>