Compare commits
3
Commits
1995b38b52
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c932814166 | ||
|
|
b3d8719874 | ||
|
|
2e1b454c09 |
@@ -17,6 +17,7 @@
|
||||
1F1CB72BBCFFB33B6533D5C9 /* LibraryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E093C878E702891C64D21FD5 /* LibraryListRow.swift */; };
|
||||
212F713DCC289C48087B79AE /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7728D15C17219ABFF3EFFE /* Log.swift */; };
|
||||
22C814FD55D29B88D227C987 /* SpotlightBookmarkSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */; };
|
||||
2F80DFB7298B733965FE04F5 /* MarksMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC8E834AB663A823D77BCB1 /* MarksMark.swift */; };
|
||||
337E8272EEB3B10FD0868F76 /* IngestedSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC9BBF1006495D75DE4A232 /* IngestedSource.swift */; };
|
||||
3528AF5CB690BBCCF337581B /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA428181B35885F7D9F4D55 /* Bookmark.swift */; };
|
||||
4153FBF538C1D3F4BC96E4C5 /* LibraryKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C9BFD9C0DD3876CC32B3A /* LibraryKit.swift */; };
|
||||
@@ -138,6 +139,7 @@
|
||||
23F172EC9977CD5C51B228B9 /* MarksWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = MarksWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
240DBB87940F8D255A812EB2 /* BookmarkActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkActions.swift; sourceTree = "<group>"; };
|
||||
2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightRetrievalTests.swift; sourceTree = "<group>"; };
|
||||
3EC8E834AB663A823D77BCB1 /* MarksMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarksMark.swift; sourceTree = "<group>"; };
|
||||
41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightBookmarkSearch.swift; sourceTree = "<group>"; };
|
||||
47CB3AAED5B64809B06A9650 /* RecentPodcastsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentPodcastsWidget.swift; sourceTree = "<group>"; };
|
||||
49685B8F3FEC72E8CF75843E /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||
@@ -257,6 +259,7 @@
|
||||
1BF2B010DADCCFBC282D37F0 /* LibraryGridView.swift */,
|
||||
AA5C9BFD9C0DD3876CC32B3A /* LibraryKit.swift */,
|
||||
E093C878E702891C64D21FD5 /* LibraryListRow.swift */,
|
||||
3EC8E834AB663A823D77BCB1 /* MarksMark.swift */,
|
||||
);
|
||||
path = Library;
|
||||
sourceTree = "<group>";
|
||||
@@ -600,6 +603,7 @@
|
||||
41F00F4E7FFC1C0ACF71E398 /* MarksApp.swift in Sources */,
|
||||
E10B0B4EC9580342830EC0D2 /* MarksAppIntents.swift in Sources */,
|
||||
8BD3FA025C082654D55374A2 /* MarksAuth.swift in Sources */,
|
||||
2F80DFB7298B733965FE04F5 /* MarksMark.swift in Sources */,
|
||||
14E1B3CE58D36BFF1A2199C1 /* OnboardingView.swift in Sources */,
|
||||
773C174263D08416248D0675 /* PodcastGenerationManager.swift in Sources */,
|
||||
0479C0AA16E3AFDBD4414AD0 /* PodcastIndex.swift in Sources */,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 14 KiB |
@@ -5,6 +5,30 @@
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "AppIcon-Dark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "AppIcon-Tinted.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -225,52 +225,71 @@ final class BookmarksViewModel {
|
||||
}
|
||||
|
||||
func enrichAll() async {
|
||||
let toEnrich = bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }
|
||||
// Track ids, not indices. Each iteration awaits the network, and
|
||||
// `bookmarks` is replaced wholesale by loads, searches and filter
|
||||
// toggles — an index captured before the await can be out of range by
|
||||
// the time it is used.
|
||||
let toEnrich = bookmarks.filter { $0.aiSummary == nil }.map(\.id)
|
||||
guard !toEnrich.isEmpty else { return }
|
||||
enrichmentProgress = 0
|
||||
for (done, i) in toEnrich.enumerated() {
|
||||
var completed = 0
|
||||
for id in toEnrich {
|
||||
guard let bookmark = bookmarks.first(where: { $0.id == id }) else { continue }
|
||||
do {
|
||||
let (summary, tags) = try await claude.enrich(bookmark: bookmarks[i])
|
||||
bookmarks[i].aiSummary = summary
|
||||
bookmarks[i].aiTags = tags
|
||||
saveAIData(for: bookmarks[i])
|
||||
let enriched = bookmarks[i]
|
||||
Task { await SpotlightIndexer.index([enriched]) }
|
||||
enrichmentProgress = Double(done + 1) / Double(toEnrich.count)
|
||||
let (summary, tags) = try await claude.enrich(bookmark: bookmark)
|
||||
apply(summary: summary, tags: tags, toId: id)
|
||||
completed += 1
|
||||
enrichmentProgress = Double(completed) / Double(toEnrich.count)
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
let enriched = toEnrich.count - bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }.count
|
||||
if enriched > 0 { Analytics.track("enrich.completed", ["count": enriched]) }
|
||||
if completed > 0 { Analytics.track("enrich.completed", ["count": completed]) }
|
||||
enrichmentProgress = 0
|
||||
}
|
||||
|
||||
/// Write an enrichment result back by id, skipping it if the bookmark is no
|
||||
/// longer loaded (deleted, archived, or filtered away mid-flight).
|
||||
private func apply(summary: String, tags: [String], toId id: Int) {
|
||||
guard let i = bookmarks.firstIndex(where: { $0.id == id }) else {
|
||||
Log.ai.notice("enrich result dropped: id=\(id, privacy: .public) no longer loaded")
|
||||
return
|
||||
}
|
||||
bookmarks[i].aiSummary = summary
|
||||
bookmarks[i].aiTags = tags
|
||||
saveAIData(for: bookmarks[i])
|
||||
let enriched = bookmarks[i]
|
||||
Task { await SpotlightIndexer.index([enriched]) }
|
||||
}
|
||||
|
||||
// Silently enrich new bookmarks that lack summaries (background, non-blocking)
|
||||
//
|
||||
// Everything here is keyed by bookmark id rather than array index. This loop
|
||||
// awaits a network call per bookmark, and `bookmarks` gets replaced under it
|
||||
// by loads, searches and the unread filter — an index captured up front used
|
||||
// to crash on the read (`bookmarks[i]`), which the write path already
|
||||
// guarded against but the read did not.
|
||||
private func startEnrichment() {
|
||||
enrichTask?.cancel()
|
||||
enrichTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let indices = await MainActor.run { bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }.prefix(5) }
|
||||
Log.ai.info("startEnrichment: \(indices.count, privacy: .public) to enrich")
|
||||
for i in indices {
|
||||
let ids = await MainActor.run {
|
||||
bookmarks.filter { $0.aiSummary == nil }.prefix(5).map(\.id)
|
||||
}
|
||||
Log.ai.info("startEnrichment: \(ids.count, privacy: .public) to enrich")
|
||||
for id in ids {
|
||||
guard !Task.isCancelled else { return }
|
||||
let claude = await MainActor.run(body: { self.claude })
|
||||
guard let bm = await MainActor.run(body: {
|
||||
bookmarks.first { $0.id == id }
|
||||
}) else { continue }
|
||||
do {
|
||||
let bm = await MainActor.run { bookmarks[i] }
|
||||
Log.ai.debug("startEnrichment enriching id=\(bm.id, privacy: .public)")
|
||||
Log.ai.debug("startEnrichment enriching id=\(id, privacy: .public)")
|
||||
let (summary, tags) = try await claude.enrich(bookmark: bm)
|
||||
Log.ai.debug("startEnrichment done id=\(bm.id, privacy: .public)")
|
||||
await MainActor.run {
|
||||
guard i < bookmarks.count else { return }
|
||||
bookmarks[i].aiSummary = summary
|
||||
bookmarks[i].aiTags = tags
|
||||
saveAIData(for: bookmarks[i])
|
||||
let enriched = bookmarks[i]
|
||||
Task { await SpotlightIndexer.index([enriched]) }
|
||||
}
|
||||
Log.ai.debug("startEnrichment done id=\(id, privacy: .public)")
|
||||
await MainActor.run { apply(summary: summary, tags: tags, toId: id) }
|
||||
} catch {
|
||||
Log.ai.error("startEnrichment failed idx=\(i, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
Log.ai.error("startEnrichment failed id=\(id, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - The mark
|
||||
//
|
||||
// One path, four tails.
|
||||
//
|
||||
// The brand mark is a single closed shape rather than a stack of separate
|
||||
// ribbons, and that is the whole design decision. A logo gets tested in two
|
||||
// places that have nothing to do with how it looks on a slide: at 16pt in a
|
||||
// Spotlight result, and in iOS 26's tinted and monochrome icon modes, where the
|
||||
// palette is thrown away and only the silhouette survives. Every arrangement of
|
||||
// N separate ribbons collapses into a blob under both tests, because the shapes
|
||||
// were only ever told apart by colour. A scalloped baseline is told apart by
|
||||
// its outline, so it reads the same tinted, monochrome, and 16pt tall.
|
||||
//
|
||||
// Geometry is defined in a canonical 88 x 98 box and fitted to whatever rect it
|
||||
// is handed, so the proportions can't drift with the container.
|
||||
|
||||
struct ScallopMark: Shape {
|
||||
/// Downward points along the baseline. Four is the drawn default: three
|
||||
/// reads as a plain banner, and five starts to look like bunting.
|
||||
var tails: Int = 4
|
||||
|
||||
/// How far the notches cut up from the baseline, as a fraction of height.
|
||||
/// Shallower than this and the tails read as damage rather than intent.
|
||||
var notchDepth: CGFloat = 16.0 / 98.0
|
||||
|
||||
/// Top-corner radius, as a fraction of width.
|
||||
var capRadius: CGFloat = 10.0 / 88.0
|
||||
|
||||
/// Canonical proportions of the drawn mark. Everything else derives from
|
||||
/// this, including the app icon artwork — see scripts/appicon.
|
||||
static let aspectRatio: CGFloat = 88.0 / 98.0
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
// Fit the canonical box into `rect` without distorting it, so callers
|
||||
// can hand this any frame and still get the drawn proportions.
|
||||
let height = min(rect.height, rect.width / Self.aspectRatio)
|
||||
let width = height * Self.aspectRatio
|
||||
let minX = rect.midX - width / 2
|
||||
let minY = rect.midY - height / 2
|
||||
let maxX = minX + width
|
||||
let maxY = minY + height
|
||||
|
||||
let radius = min(capRadius * width, min(width, height) / 2)
|
||||
let depth = notchDepth * height
|
||||
let points = max(2, tails)
|
||||
let step = width / CGFloat(points - 1)
|
||||
|
||||
var path = Path()
|
||||
path.move(to: CGPoint(x: minX + radius, y: minY))
|
||||
// Top edge, right cap, then straight down the right side.
|
||||
path.addArc(
|
||||
tangent1End: CGPoint(x: maxX, y: minY),
|
||||
tangent2End: CGPoint(x: maxX, y: minY + radius),
|
||||
radius: radius
|
||||
)
|
||||
path.addLine(to: CGPoint(x: maxX, y: maxY))
|
||||
// The tails, cut right to left so the winding stays consistent with
|
||||
// the caps above.
|
||||
for i in 1..<points {
|
||||
let apex = maxX - step * (CGFloat(i) - 0.5)
|
||||
path.addLine(to: CGPoint(x: apex, y: maxY - depth))
|
||||
path.addLine(to: CGPoint(x: maxX - step * CGFloat(i), y: maxY))
|
||||
}
|
||||
// Up the left side and through the left cap. The loop above already
|
||||
// left the pen on the bottom-left point.
|
||||
path.addArc(
|
||||
tangent1End: CGPoint(x: minX, y: minY),
|
||||
tangent2End: CGPoint(x: minX + radius, y: minY),
|
||||
radius: radius
|
||||
)
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Brand colour
|
||||
|
||||
extension Paper {
|
||||
/// The mark's bands, left to right: navy, vermilion, yellow.
|
||||
///
|
||||
/// These are palette swatches rather than bespoke brand colours on
|
||||
/// purpose. The app already hashes every bookmark into `swatchPairs`, so a
|
||||
/// mark built from three of them is made of the same ink as the content —
|
||||
/// which is also the only part of this design a competitor can't lift
|
||||
/// without lifting the system underneath it.
|
||||
static let markBands: [Color] = [swatch(5), swatch(1), swatch(0)]
|
||||
}
|
||||
|
||||
// MARK: - The mark, coloured
|
||||
|
||||
/// The mark as it should normally be used. Sizes itself to the drawn aspect
|
||||
/// ratio, so `.frame(height:)` is the natural way to scale it.
|
||||
struct MarksMark: View {
|
||||
var bands: [Color] = Paper.markBands
|
||||
var tails: Int = 4
|
||||
|
||||
/// A single-ink version, for anywhere the bands would be noise: monochrome
|
||||
/// contexts, the share sheet, a nav bar, print.
|
||||
static func monochrome(_ color: Color = Paper.ink) -> MarksMark {
|
||||
MarksMark(bands: [color])
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScallopMark(tails: tails)
|
||||
.fill(bandFill)
|
||||
.aspectRatio(ScallopMark.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
|
||||
/// Hard-stopped, not blended: the bands are three inks laid side by side,
|
||||
/// not a gradient. Doubling the stop at each boundary is what kills the
|
||||
/// ramp SwiftUI would otherwise interpolate.
|
||||
private var bandFill: LinearGradient {
|
||||
let stops = bands.enumerated().flatMap { index, color in
|
||||
[
|
||||
Gradient.Stop(color: color, location: Double(index) / Double(bands.count)),
|
||||
Gradient.Stop(color: color, location: Double(index + 1) / Double(bands.count)),
|
||||
]
|
||||
}
|
||||
return LinearGradient(stops: stops, startPoint: .leading, endPoint: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Size ladder") {
|
||||
VStack(spacing: 28) {
|
||||
HStack(alignment: .bottom, spacing: 22) {
|
||||
ForEach([128, 64, 40, 24, 16], id: \.self) { size in
|
||||
MarksMark().frame(height: CGFloat(size))
|
||||
}
|
||||
}
|
||||
// The test the shape was chosen to pass: no colour, same mark.
|
||||
HStack(alignment: .bottom, spacing: 22) {
|
||||
ForEach([128, 64, 40, 24, 16], id: \.self) { size in
|
||||
MarksMark.monochrome().frame(height: CGFloat(size))
|
||||
}
|
||||
}
|
||||
HStack(spacing: 22) {
|
||||
ForEach(3..<6) { tails in
|
||||
MarksMark(tails: tails).frame(height: 72)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(40)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Paper.sheet)
|
||||
}
|
||||
|
||||
#Preview("Lockup") {
|
||||
HStack(spacing: 18) {
|
||||
MarksMark().frame(height: 64)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Marks").font(.system(size: 46, design: .serif))
|
||||
Text("EVERYTHING YOU SAVED")
|
||||
.font(PaperType.micro)
|
||||
.tracking(3)
|
||||
.foregroundStyle(Paper.tertiary)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(Paper.ink)
|
||||
.padding(40)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Paper.sheet)
|
||||
}
|
||||
@@ -37,4 +37,23 @@ struct SpotlightRetrievalTests {
|
||||
"url did not survive the round trip: \(hit.url)")
|
||||
#expect(hit.host == "github.com", "host was \(hit.host)")
|
||||
}
|
||||
|
||||
@Test func indexedSourceIsRetrievable() async throws {
|
||||
let source = IngestedSource(
|
||||
kind: .text,
|
||||
title: "Wqvbnmtest meeting notes",
|
||||
bodyText: "A distinctive probe document about wqvbnmtest.",
|
||||
tags: ["wqvbnmtest"]
|
||||
)
|
||||
await SourceSpotlightIndexer.index([source])
|
||||
var hits: [RetrievedBookmark] = []
|
||||
for _ in 0..<20 {
|
||||
try? await Task.sleep(for: .milliseconds(700))
|
||||
hits = await SpotlightBookmarkSearch.run(query: "wqvbnmtest", limit: 5)
|
||||
if !hits.isEmpty { break }
|
||||
}
|
||||
await SourceSpotlightIndexer.remove(ids: [source.id])
|
||||
#expect(!hits.isEmpty, "Spotlight returned no hits for an indexed source")
|
||||
#expect(hits.first?.title.contains("Wqvbnmtest") == true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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 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>
|
||||
@@ -0,0 +1,447 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Marks — logo variants</title>
|
||||
<style>
|
||||
:root{
|
||||
--ink:#14110C; --sheet:#F8F5EF; --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:60ch;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:72ch}
|
||||
.tag{display:inline-block;font-size:10px;letter-spacing:.14em;text-transform:uppercase;opacity:.45;margin-bottom:10px}
|
||||
|
||||
/* ---- palettes: geometry is shared, colour is injected ---- */
|
||||
.pal{--c1:#22C7F0;--c2:#8B4DEE;--dot:#fff;--bg:#fff}
|
||||
.p-orig {--c1:#22C7F0;--c2:#8B4DEE;--dot:#fff; --bg:#fff}
|
||||
.p-paper{--c1:#001A55;--c2:#ED663F;--dot:#F8F5EF;--bg:#F8F5EF} /* navy + vermilion */
|
||||
.p-ink {--c1:#14110C;--c2:#ED663F;--dot:#F8F5EF;--bg:#F8F5EF} /* ink + vermilion */
|
||||
.p-warm {--c1:#FECD00;--c2:#ED663F;--dot:#14110C;--bg:#F8F5EF} /* yellow + vermilion */
|
||||
.p-blue {--c1:#115AB5;--c2:#033B00;--dot:#F8F5EF;--bg:#F8F5EF} /* blue + forest */
|
||||
.p-dark {--c1:#5B8FD0;--c2:#E07A5C;--dot:#15130F;--bg:#15130F} /* dark-mode pair */
|
||||
.p-mono1{--c1:#14110C;--c2:#14110C;--dot:#F8F5EF;--bg:#F8F5EF} /* single-colour test */
|
||||
|
||||
.grid{display:flex;flex-wrap:wrap;gap:18px}
|
||||
.card{background:var(--bg);border:1px solid var(--rule);border-radius:14px;padding:22px;width:236px}
|
||||
.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:.6}
|
||||
.p-dark h3,.p-dark p{color:#F1ECE1}
|
||||
.p-dark{border-color:rgba(241,236,225,.14)}
|
||||
|
||||
/* matrix */
|
||||
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:.65;white-space:nowrap}
|
||||
td{padding:6px}
|
||||
td .cell{width:104px;height:104px;border-radius:12px;background:var(--bg);border:1px solid var(--rule);display:flex;align-items:center;justify-content:center}
|
||||
|
||||
/* size ladder */
|
||||
.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:14px}
|
||||
.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:120px;opacity:.8}
|
||||
|
||||
/* app icons */
|
||||
.icons{display:flex;flex-wrap:wrap;gap:20px;align-items:flex-end}
|
||||
.icon{width:120px;height:120px;border-radius:26.5px;display:flex;align-items:center;justify-content:center;background:var(--bg);box-shadow:0 6px 18px rgba(20,17,12,.14)}
|
||||
.icon.sm{width:60px;height:60px;border-radius:13.5px}
|
||||
.icoWrap{display:flex;flex-direction:column;align-items:center;gap:10px}
|
||||
.icoWrap span{font-size:10px;opacity:.5}
|
||||
|
||||
/* wordmark */
|
||||
.lockup{display:flex;align-items:center;gap:20px;background:var(--bg);border:1px solid var(--rule);border-radius:14px;padding:34px 40px;margin-bottom:14px}
|
||||
.lockup .wm{font-family:var(--serif);font-size:52px;letter-spacing:-.02em;color:var(--c1)}
|
||||
.lockup .wm.ink{color:var(--ink)}
|
||||
.lockup.p-dark .wm.ink{color:#F1ECE1}
|
||||
.lockup.p-dark .tl{color:#F1ECE1;opacity:.5}
|
||||
.lockup .tl{font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.5;margin-top:6px}
|
||||
|
||||
.pattern{display:grid;grid-template-columns:repeat(13,54px);gap:0;background:var(--bg);border-radius:14px;padding:20px;width:742px;overflow:hidden}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>Marks — logo variants</h1>
|
||||
<p>Five geometries derived from the reference (letter + box files + bookmark ribbon), each drawn once and re-coloured through the app's existing <code>Paper</code> palette. Geometry lives in <code><symbol></code>; colour is injected with CSS custom properties, so any cell in the matrix is a real, buildable combination.</p>
|
||||
</header>
|
||||
|
||||
<!-- ============ GEOMETRY DEFINITIONS ============ -->
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
||||
<defs>
|
||||
|
||||
<!-- 1. Binder-B: faithful reconstruction. Violet ribbon stem, three cyan
|
||||
binder bars (wide / narrow / wide = the B), notch clears the bottom bar. -->
|
||||
<symbol id="binderB" viewBox="0 0 120 120">
|
||||
<g style="isolation:isolate">
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c2)"
|
||||
d="M31 10 H49 A7 7 0 0 1 56 17 V110 L40 96 L24 110 V17 A7 7 0 0 1 31 10 Z"/>
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M40 10 H79 A13 13 0 0 1 79 36 H40 Z"/>
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M40 40 H69 A13 13 0 0 1 69 66 H40 Z"/>
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M40 70 H83 A13 13 0 0 1 83 96 H40 Z"/>
|
||||
</g>
|
||||
<circle cx="32" cy="23" r="5" fill="var(--dot)"/>
|
||||
<circle cx="32" cy="53" r="5" fill="var(--dot)"/>
|
||||
<circle cx="32" cy="83" r="5" fill="var(--dot)"/>
|
||||
</symbol>
|
||||
|
||||
<!-- 2. Ribbon-B: same skeleton, but every terminal is a swallowtail.
|
||||
Consistent language, loses some of the B. -->
|
||||
<symbol id="ribbonB" viewBox="0 0 120 120">
|
||||
<g style="isolation:isolate">
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c2)"
|
||||
d="M31 10 H49 A7 7 0 0 1 56 17 V110 L40 96 L24 110 V17 A7 7 0 0 1 31 10 Z"/>
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M40 10 H92 L80 23 L92 36 H40 Z"/>
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M40 40 H82 L70 53 L82 66 H40 Z"/>
|
||||
<path style="mix-blend-mode:multiply" fill="var(--c1)" d="M40 70 H96 L84 83 L96 96 H40 Z"/>
|
||||
</g>
|
||||
<circle cx="32" cy="23" r="5" fill="var(--dot)"/>
|
||||
<circle cx="32" cy="53" r="5" fill="var(--dot)"/>
|
||||
<circle cx="32" cy="83" r="5" fill="var(--dot)"/>
|
||||
</symbol>
|
||||
|
||||
<!-- 3. Slab-M, tailed. A real M: the counters are wedges, apex at the cap
|
||||
line, widening to the baseline — that is what makes it a letter and not
|
||||
a slotted slab. Each of the three feet then takes a swallowtail. -->
|
||||
<symbol id="slabMdeep" viewBox="0 0 120 120">
|
||||
<clipPath id="clipMdeep">
|
||||
<path d="M24 16 H96 A10 10 0 0 1 106 26 V104 L97 92 L88 104 L86 20 L69 104 L60 92 L51 104 L34 20 L32 104 L23 92 L14 104 V26 A10 10 0 0 1 24 16 Z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#clipMdeep)">
|
||||
<rect x="0" y="0" width="42" height="120" fill="var(--c1)"/>
|
||||
<rect x="42" y="0" width="36" height="120" fill="var(--c2)"/>
|
||||
<rect x="78" y="0" width="42" height="120" fill="var(--c1)"/>
|
||||
</g>
|
||||
<circle cx="23" cy="68" r="5.5" fill="var(--dot)"/>
|
||||
<circle cx="60" cy="68" r="5.5" fill="var(--dot)"/>
|
||||
<circle cx="97" cy="68" r="5.5" fill="var(--dot)"/>
|
||||
</symbol>
|
||||
|
||||
<!-- 4. Slab-M, flat-footed. Same letter, no tails — the bookmark cue is
|
||||
carried by the rounded cap and the colour split alone. Letterform
|
||||
first; the ribbon reading is gone. -->
|
||||
<symbol id="slabMshallow" viewBox="0 0 120 120">
|
||||
<clipPath id="clipMshallow">
|
||||
<path d="M24 16 H96 A10 10 0 0 1 106 26 V104 H88 L86 20 L69 104 H51 L34 20 L32 104 H14 V26 A10 10 0 0 1 24 16 Z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#clipMshallow)">
|
||||
<rect x="0" y="0" width="42" height="120" fill="var(--c1)"/>
|
||||
<rect x="42" y="0" width="36" height="120" fill="var(--c2)"/>
|
||||
<rect x="78" y="0" width="42" height="120" fill="var(--c1)"/>
|
||||
</g>
|
||||
<circle cx="23" cy="68" r="5.5" fill="var(--dot)"/>
|
||||
<circle cx="60" cy="68" r="5.5" fill="var(--dot)"/>
|
||||
<circle cx="97" cy="68" r="5.5" fill="var(--dot)"/>
|
||||
</symbol>
|
||||
|
||||
<!-- 5. Ribbon + knockout: single ribbon, letter cut out of it.
|
||||
The one that survives 16px and tinted/monochrome icon modes. -->
|
||||
<symbol id="ribbonM" viewBox="0 0 120 120">
|
||||
<mask id="maskM">
|
||||
<rect width="120" height="120" fill="#000"/>
|
||||
<path fill="#fff" d="M38 12 H82 A12 12 0 0 1 94 24 V110 L60 92 L26 110 V24 A12 12 0 0 1 38 12 Z"/>
|
||||
<text x="60" y="70" text-anchor="middle" fill="#000"
|
||||
font-family="ui-serif,Georgia,serif" font-size="58" font-weight="600">M</text>
|
||||
</mask>
|
||||
<rect width="120" height="120" fill="var(--c1)" mask="url(#maskM)"/>
|
||||
</symbol>
|
||||
|
||||
<!-- 6. Three ribbons: no letterform at all. Stepped heights carry the
|
||||
rhythm; the only thing that has to survive at 16px is the silhouette. -->
|
||||
<symbol id="threeRibbons" viewBox="0 0 120 120">
|
||||
<path fill="var(--c1)" d="M24 14 H36 A6 6 0 0 1 42 20 V98 L30 84 L18 98 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(--c1)" d="M84 14 H96 A6 6 0 0 1 102 20 V90 L90 76 L78 90 V20 A6 6 0 0 1 84 14 Z"/>
|
||||
</symbol>
|
||||
|
||||
<!-- 7. Monoline: the Binder-B as strokes only. Closest to the app's
|
||||
archival/letterpress register; no fills to fight the paper ground. -->
|
||||
<symbol id="binderLine" viewBox="0 0 120 120">
|
||||
<g fill="none" stroke="var(--c1)" stroke-width="6" stroke-linejoin="round">
|
||||
<path d="M31 13 H49 A4 4 0 0 1 53 17 V104 L40 93 L27 104 V17 A4 4 0 0 1 31 13 Z"/>
|
||||
<path d="M50 13 H76 A11 11 0 0 1 76 35 H50"/>
|
||||
<path d="M50 42 H67 A11 11 0 0 1 67 64 H50"/>
|
||||
<path d="M50 71 H80 A11 11 0 0 1 80 93 H50"/>
|
||||
</g>
|
||||
<circle cx="34" cy="24" r="3" fill="var(--c2)"/>
|
||||
<circle cx="34" cy="53" r="3" fill="var(--c2)"/>
|
||||
<circle cx="34" cy="82" r="3" fill="var(--c2)"/>
|
||||
</symbol>
|
||||
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<!-- ============ 1. THE GEOMETRIES ============ -->
|
||||
<section>
|
||||
<span class="tag">01</span>
|
||||
<h2>Seven geometries</h2>
|
||||
<p class="sub">Shown in the reference palette (cyan #22C7F0 / violet #8B4DEE) with multiply overlap, so they're directly comparable to the original.</p>
|
||||
<div class="grid">
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#binderB"/></svg></div>
|
||||
<h3>A1 · Binder-B</h3>
|
||||
<p>Faithful reconstruction. Ribbon stem carries the notch; three bars go wide / narrow / wide to make the B. Closest to the source.</p>
|
||||
</div>
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div>
|
||||
<h3>A2 · Ribbon-B</h3>
|
||||
<p>Every terminal becomes a swallowtail. More internally consistent, but the bowls stop reading as bowls — the B gets weaker.</p>
|
||||
</div>
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div>
|
||||
<h3>B1 · Slab-M, tailed</h3>
|
||||
<p>Retargeted to the app's actual name. A real M — counters are wedges, apex at the cap line — with a swallowtail on each of the three feet.</p>
|
||||
</div>
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div>
|
||||
<h3>B2 · Slab-M, flat</h3>
|
||||
<p>Same letter, tails removed. Sturdier and more type-like, but only the rounded cap is left carrying the bookmark idea — probably too little.</p>
|
||||
</div>
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div>
|
||||
<h3>B3 · Ribbon knockout</h3>
|
||||
<p>Single ribbon, letter knocked out. One colour, one shape — the only variant that's trivially correct in iOS 26 tinted and mono icon modes.</p>
|
||||
</div>
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div>
|
||||
<h3>C1 · Three ribbons</h3>
|
||||
<p>Drops the letterform entirely. Stepped heights do the work. Most robust silhouette, least ownable — many apps could use this.</p>
|
||||
</div>
|
||||
|
||||
<div class="card pal p-orig">
|
||||
<div class="stage"><svg width="130" height="130" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div>
|
||||
<h3>C2 · Monoline</h3>
|
||||
<p>Binder-B as strokes. Sits in the same register as the app's serif/paper styling instead of fighting it. Fails below ~28px.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 2. PALETTE MATRIX ============ -->
|
||||
<section>
|
||||
<span class="tag">02</span>
|
||||
<h2>Palette matrix</h2>
|
||||
<p class="sub">Each geometry through the reference palette and five pairs drawn from <code>Paper.swatchPairs</code> in <code>LibraryKit.swift</code>. The <em>Dark</em> column uses the palette's own dark-mode counterparts, not dimmed light values.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="rowh"></th>
|
||||
<th>Reference</th><th>Navy + Verm.</th><th>Ink + Verm.</th>
|
||||
<th>Yellow + Verm.</th><th>Blue + Forest</th><th>Ink only</th><th>Dark</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">A1 Binder-B</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">A2 Ribbon-B</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonB"/></svg></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">B1 Slab-M deep</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">B2 Slab-M shallow</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMshallow"/></svg></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">B3 Knockout</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">C1 Three ribbons</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="rowh">C2 Monoline</th>
|
||||
<td><div class="cell pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
<td><div class="cell pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
<td><div class="cell pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
<td><div class="cell pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
<td><div class="cell pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
<td><div class="cell pal p-mono1"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
<td><div class="cell pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderLine"/></svg></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- ============ 3. SIZE LADDER ============ -->
|
||||
<section>
|
||||
<span class="tag">03</span>
|
||||
<h2>Size ladder</h2>
|
||||
<p class="sub">128 / 64 / 40 / 24 / 16 px. 40px is the Settings-row and tab-bar size; 16px is the favicon / Spotlight-result size. Anything that turns to mush at 24 is a wordmark, not a mark.</p>
|
||||
|
||||
<div class="ladder pal p-paper">
|
||||
<div class="name">A1 Binder-B</div>
|
||||
<div class="item"><svg width="128" height="128" viewBox="0 0 120 120"><use href="#binderB"/></svg><div class="n">128</div></div>
|
||||
<div class="item"><svg width="64" height="64" viewBox="0 0 120 120"><use href="#binderB"/></svg><div class="n">64</div></div>
|
||||
<div class="item"><svg width="40" height="40" viewBox="0 0 120 120"><use href="#binderB"/></svg><div class="n">40</div></div>
|
||||
<div class="item"><svg width="24" height="24" viewBox="0 0 120 120"><use href="#binderB"/></svg><div class="n">24</div></div>
|
||||
<div class="item"><svg width="16" height="16" viewBox="0 0 120 120"><use href="#binderB"/></svg><div class="n">16</div></div>
|
||||
</div>
|
||||
|
||||
<div class="ladder pal p-paper">
|
||||
<div class="name">B1 Slab-M deep</div>
|
||||
<div class="item"><svg width="128" height="128" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg><div class="n">128</div></div>
|
||||
<div class="item"><svg width="64" height="64" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg><div class="n">64</div></div>
|
||||
<div class="item"><svg width="40" height="40" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg><div class="n">40</div></div>
|
||||
<div class="item"><svg width="24" height="24" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg><div class="n">24</div></div>
|
||||
<div class="item"><svg width="16" height="16" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg><div class="n">16</div></div>
|
||||
</div>
|
||||
|
||||
<div class="ladder pal p-paper">
|
||||
<div class="name">B3 Knockout</div>
|
||||
<div class="item"><svg width="128" height="128" viewBox="0 0 120 120"><use href="#ribbonM"/></svg><div class="n">128</div></div>
|
||||
<div class="item"><svg width="64" height="64" viewBox="0 0 120 120"><use href="#ribbonM"/></svg><div class="n">64</div></div>
|
||||
<div class="item"><svg width="40" height="40" viewBox="0 0 120 120"><use href="#ribbonM"/></svg><div class="n">40</div></div>
|
||||
<div class="item"><svg width="24" height="24" viewBox="0 0 120 120"><use href="#ribbonM"/></svg><div class="n">24</div></div>
|
||||
<div class="item"><svg width="16" height="16" viewBox="0 0 120 120"><use href="#ribbonM"/></svg><div class="n">16</div></div>
|
||||
</div>
|
||||
|
||||
<div class="ladder pal p-paper">
|
||||
<div class="name">C1 Three ribbons</div>
|
||||
<div class="item"><svg width="128" height="128" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg><div class="n">128</div></div>
|
||||
<div class="item"><svg width="64" height="64" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg><div class="n">64</div></div>
|
||||
<div class="item"><svg width="40" height="40" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg><div class="n">40</div></div>
|
||||
<div class="item"><svg width="24" height="24" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg><div class="n">24</div></div>
|
||||
<div class="item"><svg width="16" height="16" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg><div class="n">16</div></div>
|
||||
</div>
|
||||
|
||||
<div class="ladder pal p-paper">
|
||||
<div class="name">C2 Monoline</div>
|
||||
<div class="item"><svg width="128" height="128" viewBox="0 0 120 120"><use href="#binderLine"/></svg><div class="n">128</div></div>
|
||||
<div class="item"><svg width="64" height="64" viewBox="0 0 120 120"><use href="#binderLine"/></svg><div class="n">64</div></div>
|
||||
<div class="item"><svg width="40" height="40" viewBox="0 0 120 120"><use href="#binderLine"/></svg><div class="n">40</div></div>
|
||||
<div class="item"><svg width="24" height="24" viewBox="0 0 120 120"><use href="#binderLine"/></svg><div class="n">24</div></div>
|
||||
<div class="item"><svg width="16" height="16" viewBox="0 0 120 120"><use href="#binderLine"/></svg><div class="n">16</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 4. APP ICON ============ -->
|
||||
<section id="s4">
|
||||
<span class="tag">04</span>
|
||||
<h2>App icon</h2>
|
||||
<p class="sub">iOS squircle at 120pt and 60pt. The mark is set at ~62% of the tile — Apple's grid wants generous margins, and every one of these was drawn edge-to-edge in its own box.</p>
|
||||
<div class="icons">
|
||||
<div class="icoWrap"><div class="icon pal p-paper"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div><span>A1 · navy</span></div>
|
||||
<div class="icoWrap"><div class="icon pal p-warm"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div><span>B1 · yellow</span></div>
|
||||
<div class="icoWrap"><div class="icon pal p-ink"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div><span>B3 · ink</span></div>
|
||||
<div class="icoWrap"><div class="icon pal p-blue"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div><span>C1 · blue</span></div>
|
||||
<div class="icoWrap"><div class="icon pal p-dark"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div><span>A1 · dark</span></div>
|
||||
<div class="icoWrap"><div class="icon pal p-orig"><svg width="76" height="76" viewBox="0 0 120 120"><use href="#binderB"/></svg></div><span>A1 · reference</span></div>
|
||||
</div>
|
||||
<div class="icons" style="margin-top:22px">
|
||||
<div class="icoWrap"><div class="icon sm pal p-paper"><svg width="38" height="38" viewBox="0 0 120 120"><use href="#binderB"/></svg></div><span>60pt</span></div>
|
||||
<div class="icoWrap"><div class="icon sm pal p-warm"><svg width="38" height="38" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg></div><span>60pt</span></div>
|
||||
<div class="icoWrap"><div class="icon sm pal p-ink"><svg width="38" height="38" viewBox="0 0 120 120"><use href="#ribbonM"/></svg></div><span>60pt</span></div>
|
||||
<div class="icoWrap"><div class="icon sm pal p-blue"><svg width="38" height="38" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg></div><span>60pt</span></div>
|
||||
<div class="icoWrap"><div class="icon sm pal p-dark"><svg width="38" height="38" viewBox="0 0 120 120"><use href="#binderB"/></svg></div><span>60pt</span></div>
|
||||
<div class="icoWrap"><div class="icon sm pal p-orig"><svg width="38" height="38" viewBox="0 0 120 120"><use href="#binderB"/></svg></div><span>60pt</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 5. LOCKUP ============ -->
|
||||
<section>
|
||||
<span class="tag">05</span>
|
||||
<h2>Lockups</h2>
|
||||
<p class="sub">Wordmark set in the app's own voice — serif for the name, mono letterspaced for the descriptor — rather than the reference's geometric sans, which belongs to a different product.</p>
|
||||
|
||||
<div class="lockup pal p-paper">
|
||||
<svg width="66" height="66" viewBox="0 0 120 120"><use href="#binderB"/></svg>
|
||||
<div><div class="wm ink">Marks</div><div class="tl">Everything you saved</div></div>
|
||||
</div>
|
||||
|
||||
<div class="lockup pal p-ink">
|
||||
<svg width="66" height="66" viewBox="0 0 120 120"><use href="#ribbonM"/></svg>
|
||||
<div><div class="wm ink">Marks</div><div class="tl">Everything you saved</div></div>
|
||||
</div>
|
||||
|
||||
<div class="lockup pal p-warm">
|
||||
<svg width="66" height="66" viewBox="0 0 120 120"><use href="#slabMdeep"/></svg>
|
||||
<div><div class="wm ink">Marks</div><div class="tl">Everything you saved</div></div>
|
||||
</div>
|
||||
|
||||
<div class="lockup pal p-dark">
|
||||
<svg width="66" height="66" viewBox="0 0 120 120"><use href="#threeRibbons"/></svg>
|
||||
<div><div class="wm ink">Marks</div><div class="tl" style="color:#F1ECE1;opacity:.5">Everything you saved</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 6. PATTERN ============ -->
|
||||
<section style="border-bottom:none">
|
||||
<span class="tag">06</span>
|
||||
<h2>Pattern test</h2>
|
||||
<p class="sub">The reference proves itself by tiling. Same test here — if the mark only works alone, it isn't a system.</p>
|
||||
<div class="pattern pal p-paper" id="pat"></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// 13 columns x 5 rows. The row offset stops the palette cycle from lining up
|
||||
// into vertical stripes, which is what makes it read as a field and not a grid.
|
||||
const cycle = ['p-paper', 'p-warm', 'p-ink', 'p-blue'], COLS = 13
|
||||
const NS = 'http://www.w3.org/2000/svg', pat = document.getElementById('pat')
|
||||
for (let i = 0; i < COLS * 5; i++) {
|
||||
const svg = document.createElementNS(NS, 'svg')
|
||||
svg.setAttribute('width', 54); svg.setAttribute('height', 54)
|
||||
svg.setAttribute('viewBox', '0 0 120 120')
|
||||
svg.setAttribute('class', 'pal ' + cycle[(i + Math.floor(i / COLS)) % cycle.length])
|
||||
const use = document.createElementNS(NS, 'use')
|
||||
use.setAttribute('href', '#threeRibbons')
|
||||
svg.appendChild(use); pat.appendChild(svg)
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
App icon artwork for Marks. Rendered to PNG by generate.sh — do not edit the
|
||||
PNGs in AppIcon.appiconset directly, they are build output.
|
||||
|
||||
The path below is the same geometry as ScallopMark in
|
||||
Marks/Views/Library/MarksMark.swift, in the same canonical 88 x 98 box. If you
|
||||
change one, change the other; there is no shared source between Swift and SVG.
|
||||
|
||||
Three variants, per Apple's iOS 18+ app icon model:
|
||||
light — full-bleed artwork on the paper ground, flattened (icons may not
|
||||
carry an alpha channel).
|
||||
dark — artwork only, on transparency. The system supplies the dark
|
||||
background, so the bands are the palette's dark counterparts,
|
||||
which are lifted rather than dimmed.
|
||||
tinted — greyscale on transparency. The system maps luminance to the user's
|
||||
chosen tint, so the three bands are carried as three greys whose
|
||||
relative luminance matches the colour version. Flattening them to
|
||||
one grey would throw away the band reading in exactly the mode
|
||||
that has nothing else left.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Marks app icon</title>
|
||||
<style>
|
||||
html,body{margin:0;padding:0;background:transparent}
|
||||
#tile{width:1024px;height:1024px;display:flex;align-items:center;justify-content:center}
|
||||
svg{display:block}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="tile"></div>
|
||||
<script>
|
||||
// 88 x 98 canonical box. 4 tails => 3 steps of 88/3.
|
||||
const W = 88, H = 98, R = 10, DEPTH = 16, TAILS = 4
|
||||
const step = W / (TAILS - 1)
|
||||
|
||||
let d = `M${R} 0 H${W - R} A${R} ${R} 0 0 1 ${W} ${R} V${H}`
|
||||
for (let i = 1; i < TAILS; i++) {
|
||||
d += ` L${(W - step * (i - 0.5)).toFixed(3)} ${H - DEPTH}`
|
||||
d += ` L${(W - step * i).toFixed(3)} ${H}`
|
||||
}
|
||||
d += ` V${R} A${R} ${R} 0 0 1 ${R} 0 Z`
|
||||
|
||||
const VARIANTS = {
|
||||
// navy, vermilion, yellow on the paper sheet
|
||||
light: {ground: '#F8F5EF', bands: ['#001A55', '#ED663F', '#FECD00']},
|
||||
// the palette's dark counterparts, lifted so they carry on a dark ground
|
||||
dark: {ground: null, bands: ['#5B8FD0', '#E07A5C', '#D8AD10']},
|
||||
// greys ordered by the colour version's relative luminance
|
||||
tinted: {ground: null, bands: ['#737373', '#B8B8B8', '#F2F2F2']},
|
||||
}
|
||||
|
||||
const v = VARIANTS[new URLSearchParams(location.search).get('v') || 'light']
|
||||
// Mark occupies 62% of the tile's height, which lands it inside Apple's grid
|
||||
// with the margin the squircle mask wants.
|
||||
const markH = 1024 * 0.62
|
||||
|
||||
const svg = [`<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">`]
|
||||
if (v.ground) svg.push(`<rect width="1024" height="1024" fill="${v.ground}"/>`)
|
||||
svg.push(`<defs><clipPath id="mark"><path d="${d}"/></clipPath></defs>`)
|
||||
svg.push(`<g transform="translate(${512 - markH * W / H / 2} ${512 - markH / 2}) scale(${markH / H})">`)
|
||||
svg.push(`<g clip-path="url(#mark)">`)
|
||||
v.bands.forEach((color, i) => {
|
||||
svg.push(`<rect x="${(W / 3 * i).toFixed(3)}" y="0" width="${(W / 3).toFixed(3)}" height="${H}" fill="${color}"/>`)
|
||||
})
|
||||
svg.push(`</g></g></svg>`)
|
||||
document.getElementById('tile').innerHTML = svg.join('')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Renders the three app-icon variants from appicon.html into the asset catalog.
|
||||
#
|
||||
# scripts/appicon/generate.sh
|
||||
#
|
||||
# Chrome is the rasteriser because it is the only thing on a stock Mac that
|
||||
# renders SVG the same way the browser preview sheets in docs/ do. The PNGs it
|
||||
# writes are build output — edit appicon.html, not them.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT="$HERE/../../Marks/Assets.xcassets/AppIcon.appiconset"
|
||||
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
|
||||
[ -x "$CHROME" ] || { echo "Google Chrome not found at $CHROME" >&2; exit 1; }
|
||||
command -v magick >/dev/null || { echo "ImageMagick (magick) not found" >&2; exit 1; }
|
||||
|
||||
render() {
|
||||
local variant=$1 name=$2
|
||||
"$CHROME" --headless=new --disable-gpu --hide-scrollbars \
|
||||
--default-background-color=00000000 \
|
||||
--screenshot="$OUT/$name" --window-size=1024,1024 \
|
||||
--virtual-time-budget=2000 \
|
||||
"file://$HERE/appicon.html?v=$variant" >/dev/null 2>&1
|
||||
echo " $name"
|
||||
}
|
||||
|
||||
echo "rendering app icon ->"
|
||||
render light AppIcon.png
|
||||
render dark AppIcon-Dark.png
|
||||
render tinted AppIcon-Tinted.png
|
||||
|
||||
# The light icon is the one shipped as the home-screen artwork, and iOS rejects
|
||||
# an alpha channel there. The dark and tinted variants are composited by the
|
||||
# system over a background it supplies, so they must keep theirs.
|
||||
magick "$OUT/AppIcon.png" -background '#F8F5EF' -alpha remove -alpha off "$OUT/AppIcon.png"
|
||||
|
||||
echo "done"
|
||||
Reference in New Issue
Block a user