feat: add darkmatter css create 16eff2fc
- Adds a new create that embeds unified css across apps
- Updates all existing apps to use new crate
Steve · 2026-04-18 19:24 61 file(s) · +1410 −1810
Cargo.lock +16 −0
71 71
]
72 72
73 73
[[package]]
74 +
name = "andromeda-darkmatter-css"
75 +
version = "0.1.0"
76 +
dependencies = [
77 +
 "axum",
78 +
 "rust-embed",
79 +
]
80 +
81 +
[[package]]
74 82
name = "andromeda-db"
75 83
version = "0.1.0"
76 84
dependencies = [
690 698
version = "0.2.0"
691 699
dependencies = [
692 700
 "andromeda-auth",
701 +
 "andromeda-darkmatter-css",
693 702
 "andromeda-db",
694 703
 "askama 0.15.6",
695 704
 "askama_web",
1364 1373
version = "0.2.0"
1365 1374
dependencies = [
1366 1375
 "andromeda-auth",
1376 +
 "andromeda-darkmatter-css",
1367 1377
 "andromeda-db",
1368 1378
 "askama 0.13.1",
1369 1379
 "axum",
2212 2222
version = "0.2.0"
2213 2223
dependencies = [
2214 2224
 "andromeda-auth",
2225 +
 "andromeda-darkmatter-css",
2215 2226
 "andromeda-db",
2216 2227
 "arboard",
2217 2228
 "askama 0.15.6",
2800 2811
name = "og"
2801 2812
version = "0.1.0"
2802 2813
dependencies = [
2814 +
 "andromeda-darkmatter-css",
2803 2815
 "askama 0.13.1",
2804 2816
 "axum",
2805 2817
 "dotenvy",
2918 2930
version = "0.1.3"
2919 2931
dependencies = [
2920 2932
 "andromeda-auth",
2933 +
 "andromeda-darkmatter-css",
2921 2934
 "andromeda-db",
2922 2935
 "anyhow",
2923 2936
 "askama 0.12.1",
3136 3149
version = "0.1.4"
3137 3150
dependencies = [
3138 3151
 "andromeda-auth",
3152 +
 "andromeda-darkmatter-css",
3139 3153
 "andromeda-db",
3140 3154
 "askama 0.15.6",
3141 3155
 "askama_web",
4137 4151
name = "shrink"
4138 4152
version = "0.1.2"
4139 4153
dependencies = [
4154 +
 "andromeda-darkmatter-css",
4140 4155
 "askama 0.15.6",
4141 4156
 "axum",
4142 4157
 "image",
4205 4220
version = "0.2.0"
4206 4221
dependencies = [
4207 4222
 "andromeda-auth",
4223 +
 "andromeda-darkmatter-css",
4208 4224
 "andromeda-db",
4209 4225
 "arboard",
4210 4226
 "askama 0.15.6",
Cargo.toml +2 −0
10 10
    "apps/posts",
11 11
    "crates/auth",
12 12
    "crates/db",
13 +
    "crates/darkmatter-css",
13 14
]
14 15
resolver = "3"
15 16
52 53
# Workspace crates
53 54
andromeda-auth = { path = "crates/auth" }
54 55
andromeda-db = { path = "crates/db" }
56 +
andromeda-darkmatter-css = { path = "crates/darkmatter-css" }
apps/cellar/Cargo.toml +1 −0
22 22
tracing-subscriber = { workspace = true }
23 23
andromeda-auth = { workspace = true }
24 24
andromeda-db = { workspace = true, features = ["session"] }
25 +
andromeda-darkmatter-css = { workspace = true }
25 26
askama = "0.15"
26 27
askama_web = { version = "0.15", features = ["axum-0.8"] }
27 28
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
apps/cellar/src/server/mod.rs +1 −0
573 573
        .route("/admin/analyze-image", post(admin::post_analyze_image))
574 574
        // Static assets
575 575
        .route("/static/{*path}", get(public::serve_static))
576 +
        .merge(andromeda_darkmatter_css::router::<Arc<AppState>>())
576 577
        .layer(DefaultBodyLimit::max(10 * 1024 * 1024))
577 578
        .with_state(state);
578 579
apps/cellar/static/styles.css +5 −170
1 -
@font-face {
2 -
  font-family: "Commit Mono";
3 -
  src: url("/static/fonts/CommitMono-400-Regular.otf") format("opentype");
4 -
  font-weight: 400;
5 -
  font-style: normal;
6 -
}
7 -
8 -
@font-face {
9 -
  font-family: "Commit Mono";
10 -
  src: url("/static/fonts/CommitMono-700-Regular.otf") format("opentype");
11 -
  font-weight: 700;
12 -
  font-style: normal;
13 -
}
14 -
15 -
* {
16 -
  padding: 0;
17 -
  margin: 0;
18 -
  box-sizing: border-box;
19 -
  font-family: "Commit Mono", monospace, sans-serif;
20 -
  scrollbar-width: none;
21 -
  -ms-overflow-style: none;
22 -
}
23 -
24 -
html {
25 -
  background: #121113;
26 -
  color: #ffffff;
27 -
  font-size: 14px;
28 -
  line-height: 1.6;
29 -
}
30 -
31 -
html::-webkit-scrollbar {
32 -
  display: none;
33 -
}
34 -
35 -
body {
36 -
  display: flex;
37 -
  flex-direction: column;
38 -
  justify-content: start;
39 -
  align-items: start;
40 -
  gap: 1.5rem;
41 -
  min-height: 100vh;
42 -
  max-width: 700px;
43 -
  margin: auto;
44 -
  padding: 0 1rem;
45 -
}
46 -
47 -
@media (max-width: 480px) {
48 -
  body {
49 -
    padding: 1rem;
50 -
    gap: 1rem;
51 -
  }
52 -
}
53 -
54 -
a {
55 -
  color: #ffffff;
56 -
  text-decoration: none;
57 -
}
58 -
59 -
a:hover {
60 -
  opacity: 0.7;
61 -
}
62 -
63 -
/* Header */
64 -
65 -
.header {
66 -
  display: flex;
67 -
  flex-direction: column;
68 -
  gap: 0.5rem;
69 -
  width: 100%;
70 -
  margin-top: 2rem;
71 -
  border-bottom: 1px solid #333;
72 -
  padding-bottom: 1rem;
73 -
}
1 +
/* cellar — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
74 4
75 -
.logo {
76 -
  font-size: 28px;
77 -
  font-weight: 700;
78 -
  text-decoration: none;
79 -
  text-transform: uppercase;
80 -
}
81 -
82 -
.links {
83 -
  display: flex;
84 -
  align-items: center;
85 -
  gap: 0.75rem;
86 -
  font-size: 12px;
87 -
}
88 -
89 -
/* Main content */
90 -
91 -
main {
92 -
  width: 100%;
93 -
  display: flex;
94 -
  flex-direction: column;
95 -
  gap: 1rem;
96 -
}
97 -
98 -
/* Forms */
99 -
100 -
.form {
101 -
  display: flex;
102 -
  flex-direction: column;
103 -
  gap: 0.5rem;
104 -
  width: 100%;
105 -
}
106 -
107 -
label {
108 -
  font-size: 12px;
109 -
  opacity: 0.7;
110 -
}
111 -
112 -
input, textarea {
113 -
  background: #121113;
114 -
  color: #ffffff;
115 -
  border: 1px solid white;
116 -
  padding: 0.4rem 0.75rem;
117 -
  font-size: 16px;
118 -
  width: 100%;
119 -
  border-radius: 0;
5 +
textarea {
6 +
  min-height: 120px;
120 7
}
121 8
122 9
input[type="file"] {
125 12
  font-size: 12px;
126 13
}
127 14
128 -
textarea {
129 -
  min-height: 120px;
130 -
  resize: vertical;
131 -
}
132 -
133 -
button {
134 -
  background: #121113;
135 -
  color: #ffffff;
136 -
  padding: 0.4rem 0.75rem;
137 -
  border: 1px solid white;
138 -
  cursor: pointer;
139 -
  width: fit-content;
140 -
  font-size: 14px;
141 -
  border-radius: 0;
142 -
}
143 -
144 -
button:hover {
145 -
  opacity: 0.7;
146 -
}
147 -
148 15
button:disabled {
149 16
  opacity: 0.3;
150 17
  cursor: not-allowed;
151 -
}
152 -
153 -
/* Error */
154 -
155 -
.error {
156 -
  color: #ffffff;
157 -
  border-left: 2px solid #ffffff;
158 -
  padding-left: 0.5rem;
159 -
  font-size: 13px;
160 -
  opacity: 0.8;
161 -
}
162 -
163 -
.empty {
164 -
  opacity: 0.5;
165 -
  font-size: 12px;
166 18
}
167 19
168 20
/* Wine list (public) */
320 172
  display: flex;
321 173
  gap: 1rem;
322 174
  font-size: 12px;
323 -
}
324 -
325 -
.inline-form {
326 -
  display: inline;
327 -
}
328 -
329 -
.link-button {
330 -
  background: none;
331 -
  border: none;
332 -
  color: #ffffff;
333 -
  cursor: pointer;
334 -
  font-size: 12px;
335 -
  padding: 0;
336 -
}
337 -
338 -
.link-button:hover {
339 -
  opacity: 0.7;
340 175
}
341 176
342 177
/* Score inputs */
apps/cellar/templates/base.html +1 −0
13 13
  <meta property="og:image" content="/static/og.png">
14 14
  <meta property="og:type" content="website">
15 15
  <meta name="theme-color" content="#121113" />
16 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
16 17
  <link rel="stylesheet" href="/static/styles.css">
17 18
  <link rel="alternate" type="application/rss+xml" title="Cellar RSS" href="/feed.xml">
18 19
</head>
apps/cellar/templates/login.html +1 −0
5 5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6
  <title>Cellar</title>
7 7
  <meta name="theme-color" content="#121113" />
8 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
8 9
  <link rel="stylesheet" href="/static/styles.css">
9 10
</head>
10 11
<body>
apps/feeds/Cargo.toml +1 −0
21 21
tracing-subscriber = { workspace = true, features = ["env-filter"] }
22 22
andromeda-auth = { workspace = true }
23 23
andromeda-db = { workspace = true, features = ["axum", "session", "feeds"] }
24 +
andromeda-darkmatter-css = { workspace = true }
24 25
askama = "0.13"
25 26
reqwest = { version = "0.12", features = ["json"] }
26 27
feed-rs = "2"
apps/feeds/src/main.rs +2 −1
559 559
        .init();
560 560
561 561
    let db_path =
562 -
        std::env::var("FEEDS_DB_PATH").unwrap_or_else(|_| "/data/feeds.sqlite".to_string());
562 +
        std::env::var("FEEDS_DB_PATH").unwrap_or_else(|_| "feeds.sqlite".to_string());
563 563
    let conn = Connection::open(&db_path).expect("open sqlite");
564 564
    conn.execute_batch(SESSION_SCHEMA).expect("session schema");
565 565
    conn.execute_batch(fdb::FEEDS_SCHEMA).expect("feeds schema");
647 647
        .route("/static/{*path}", get(static_handler))
648 648
        .merge(admin_router)
649 649
        .merge(api_router)
650 +
        .merge(andromeda_darkmatter_css::router::<Arc<AppState>>())
650 651
        .with_state(state);
651 652
652 653
    let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
apps/feeds/src/templates/admin.html +7 −6
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 6
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
7 8
    <link rel="stylesheet" href="/static/styles.css" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
9 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
21 22
    </div>
22 23
23 24
    {% if let Some(msg) = success %}
24 -
    <p class="success-msg">{{ msg }}</p>
25 +
    <p class="success">{{ msg }}</p>
25 26
    {% endif %}
26 27
    {% if let Some(err) = error %}
27 -
    <p class="error-msg">{{ err }}</p>
28 +
    <p class="error">{{ err }}</p>
28 29
    {% endif %}
29 30
30 31
    <section class="admin-form">
94 95
            <a href="{% if let Some(url) = sub.site_url %}{{ url }}{% else %}{{ sub.feed_url }}{% endif %}" target="_blank" rel="noopener noreferrer">{{ sub.title }}</a>
95 96
          </h3>
96 97
          {% if let Some(last) = sub.last_fetched_at %}
97 -
          <p class="feed-meta"><span class="feed-date">last: {{ last }}</span>{% if let Some(err) = sub.last_error %} <span class="error-msg">· {{ err }}</span>{% endif %}</p>
98 +
          <p class="feed-meta"><span class="feed-date">last: {{ last }}</span>{% if let Some(err) = sub.last_error %} <span class="error">· {{ err }}</span>{% endif %}</p>
98 99
          {% endif %}
99 100
          <form method="POST" action="/admin/feeds/{{ sub.id }}/category" class="inline">
100 101
            <input type="text" name="category_name" placeholder="category" list="categories-list"
140 141
          const data = await resp.json();
141 142
          if (!resp.ok) {
142 143
            status.textContent = data.error || 'No feeds found';
143 -
            status.className = 'discover-status error-msg';
144 +
            status.className = 'discover-status error';
144 145
            status.style.display = 'block';
145 146
            return;
146 147
          }
147 148
          feedInput.value = data[0];
148 149
          status.textContent = data.length + ' feed(s) found';
149 -
          status.className = 'discover-status success-msg';
150 +
          status.className = 'discover-status success';
150 151
          status.style.display = 'block';
151 152
          if (data.length > 1) {
152 153
            results.style.display = 'flex';
167 168
          }
168 169
        } catch (e) {
169 170
          status.textContent = 'Request failed';
170 -
          status.className = 'discover-status error-msg';
171 +
          status.className = 'discover-status error';
171 172
          status.style.display = 'block';
172 173
        } finally {
173 174
          btn.disabled = false;
apps/feeds/src/templates/index.html +2 −1
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 6
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
7 8
    <link rel="stylesheet" href="/static/styles.css" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
9 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
42 43
    {% endif %}
43 44
44 45
    {% if let Some(err) = error %}
45 -
    <div id="error" style="color: #ff6b6b;">
46 +
    <div id="error" class="error">
46 47
      <p>{{ err }}</p>
47 48
    </div>
48 49
    {% elif items.is_empty() %}
apps/feeds/src/templates/login.html +2 −1
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 6
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
7 8
    <link rel="stylesheet" href="/static/styles.css" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
9 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
16 17
      <h1>FEEDS</h1>
17 18
    </a>
18 19
    {% if let Some(err) = error %}
19 -
    <p class="error-msg">{{ err }}</p>
20 +
    <p class="error">{{ err }}</p>
20 21
    {% endif %}
21 22
22 23
    <form class="admin-form" method="POST" action="/admin/login">
apps/feeds/static/fonts/CommitMono-400-Italic.otf (deleted) +0 −0

Binary file — no preview.

apps/feeds/static/fonts/CommitMono-400-Regular.otf (deleted) +0 −0

Binary file — no preview.

apps/feeds/static/fonts/CommitMono-700-Italic.otf (deleted) +0 −0

Binary file — no preview.

apps/feeds/static/fonts/CommitMono-700-Regular.otf (deleted) +0 −0

Binary file — no preview.

apps/feeds/static/styles.css +131 −262
1 -
* {
2 -
	padding: 0;
3 -
	margin: 0;
4 -
	box-sizing: border-box;
5 -
	font-family: "Commit Mono", monospace, sans-serif;
6 -
	scrollbar-width: none;
7 -
	-ms-overflow-style: none;
8 -
}
9 -
10 -
html {
11 -
	background: #121113;
12 -
	color: #ffffff;
13 -
}
14 -
15 -
html::-webkit-scrollbar {
16 -
	display: none;
17 -
}
18 -
19 -
body {
20 -
	display: flex;
21 -
	flex-direction: column;
22 -
	justify-content: start;
23 -
	align-items: start;
24 -
	gap: 1.5rem;
25 -
	min-height: 100vh;
26 -
	max-width: 700px;
27 -
	margin: auto;
28 -
}
1 +
/* feeds — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
29 4
30 -
.header {
31 -
	display: flex;
32 -
	flex-direction: column;
33 -
	gap: 0.5rem;
34 -
	margin-top: 2rem;
35 -
}
5 +
/* The logo wraps an h1 in feeds markup. */
36 6
37 -
.logo {
38 -
	text-decoration: none !important;
39 -
}
40 -
41 -
.links {
42 -
	display: flex;
43 -
	align-items: center;
44 -
	gap: 0.75rem;
45 -
	font-size: 12px;
46 -
	text-decoration: none;
7 +
.logo h1 {
8 +
  font-size: 28px;
9 +
  font-weight: 700;
10 +
  text-transform: uppercase;
47 11
}
48 12
49 13
.about {
50 -
	display: flex;
51 -
	flex-direction: column;
52 -
	gap: 0.5rem;
53 -
	font-size: 14px;
54 -
	line-height: 1.25rem;
14 +
  display: flex;
15 +
  flex-direction: column;
16 +
  gap: 0.5rem;
17 +
  font-size: 14px;
18 +
  line-height: 1.25rem;
55 19
}
56 20
57 -
button {
58 -
	background: #121113;
59 -
	color: #ffffff;
60 -
	padding: 6px;
61 -
	border: 1px solid white;
62 -
	cursor: pointer;
63 -
	width: fit-content;
64 -
}
65 -
66 -
a {
67 -
	background: #121113;
68 -
	color: #ffffff;
69 -
	text-decoration: none !important;
70 -
}
71 -
72 -
ul {
73 -
	margin-left: 1.5rem;
74 -
}
75 -
76 -
li {
77 -
	padding: 0.5rem 0;
78 -
}
79 -
80 -
code {
81 -
	background: #333;
82 -
	padding: 3px;
83 -
}
21 +
/* Feeds list */
84 22
85 23
.feeds-list {
86 -
	width: 100%;
87 -
	display: flex;
88 -
	flex-direction: column;
89 -
	gap: 1.5rem;
24 +
  width: 100%;
25 +
  display: flex;
26 +
  flex-direction: column;
27 +
  gap: 1.5rem;
90 28
}
91 29
92 30
.feed-item {
93 -
	display: flex;
94 -
	flex-direction: column;
95 -
	gap: 0.5rem;
96 -
	padding: 1rem 0;
97 -
	border-bottom: 1px solid #333;
98 -
}
99 -
100 -
#feed-urls {
101 -
	font-size: 12px;
102 -
	color: #888;
31 +
  display: flex;
32 +
  flex-direction: column;
33 +
  gap: 0.5rem;
34 +
  padding: 1rem 0;
35 +
  border-bottom: 1px solid #333;
103 36
}
104 37
105 38
.feed-item:last-child {
106 -
	border-bottom: none;
39 +
  border-bottom: none;
107 40
}
108 41
109 42
.feed-meta {
110 -
	display: flex;
111 -
	justify-content: space-between;
112 -
	align-items: center;
113 -
	font-size: 12px;
114 -
	color: #888;
43 +
  display: flex;
44 +
  justify-content: space-between;
45 +
  align-items: center;
46 +
  font-size: 12px;
47 +
  opacity: 0.5;
115 48
}
116 49
117 50
.feed-source {
118 -
	font-weight: 700;
119 -
}
120 -
121 -
.feed-date {
122 -
	color: #666;
51 +
  font-weight: 700;
123 52
}
124 53
125 54
.feed-title {
126 -
	font-size: 16px;
127 -
	font-weight: 400;
128 -
	line-height: 1.4;
55 +
  font-size: 16px;
56 +
  font-weight: 400;
57 +
  line-height: 1.4;
129 58
}
130 59
131 60
.feed-title a {
132 -
	text-decoration: none;
133 -
	color: #ffffff;
134 -
	transition: color 0.2s ease;
135 -
}
136 -
137 -
.feed-title a:hover {
138 -
	color: #ccc;
61 +
  text-decoration: none;
139 62
}
140 63
141 64
.feed-author {
142 -
	font-size: 12px;
143 -
	color: #888;
144 -
	font-style: italic;
65 +
  font-size: 12px;
66 +
  opacity: 0.5;
67 +
  font-style: italic;
145 68
}
146 69
147 -
.no-feeds {
148 -
	text-align: center;
149 -
	color: #888;
150 -
	padding: 2rem;
70 +
#feed-urls {
71 +
  font-size: 12px;
72 +
  opacity: 0.5;
151 73
}
152 74
75 +
.no-feeds,
153 76
#loading {
154 -
	text-align: center;
155 -
	color: #888;
156 -
	padding: 2rem;
77 +
  text-align: center;
78 +
  opacity: 0.5;
79 +
  padding: 2rem;
157 80
}
158 81
159 82
#error {
160 -
	text-align: center;
161 -
	padding: 2rem;
83 +
  text-align: center;
84 +
  padding: 2rem;
162 85
}
163 86
164 -
@media (max-width: 480px) {
165 -
	.feed-meta {
166 -
		flex-direction: column;
167 -
		align-items: flex-start;
168 -
		gap: 0.25rem;
169 -
	}
170 -
171 -
	.feed-title {
172 -
		font-size: 14px;
173 -
	}
174 -
}
175 -
176 -
@media (max-width: 480px) {
177 -
	body {
178 -
		padding: 1rem;
179 -
		gap: 1rem;
180 -
	}
181 -
}
87 +
/* Admin forms */
182 88
183 89
.admin-form {
184 -
	display: flex;
185 -
	flex-direction: column;
186 -
	gap: 0.75rem;
187 -
	width: 100%;
90 +
  display: flex;
91 +
  flex-direction: column;
92 +
  gap: 0.75rem;
93 +
  width: 100%;
188 94
}
189 95
190 -
.admin-form label {
191 -
	font-size: 12px;
192 -
	color: #888;
193 -
	text-transform: uppercase;
194 -
	letter-spacing: 0.05em;
96 +
.admin-form h3 {
97 +
  font-size: 14px;
98 +
  font-weight: 400;
99 +
  opacity: 0.5;
195 100
}
196 101
197 -
.admin-form input {
198 -
	background: #1a1a1c;
199 -
	color: #ffffff;
200 -
	border: 1px solid #333;
201 -
	padding: 10px;
202 -
	font-family: "Commit Mono", monospace, sans-serif;
203 -
	font-size: 14px;
204 -
	outline: none;
102 +
.admin-notice,
103 +
.hint {
104 +
  font-size: 12px;
105 +
  opacity: 0.5;
106 +
  line-height: 1.4;
205 107
}
206 108
207 -
.admin-form input:focus {
208 -
	border-color: #666;
209 -
}
210 -
211 -
.error-msg {
212 -
	color: #ff6b6b;
213 -
	font-size: 14px;
214 -
}
215 -
216 -
.success-msg {
217 -
	color: #6bff8a;
218 -
	font-size: 14px;
219 -
}
220 -
221 -
.admin-notice {
222 -
	font-size: 14px;
223 -
	color: #888;
224 -
	line-height: 1.4;
225 -
}
109 +
/* Discover panel */
226 110
227 111
.discover-row {
228 -
	display: flex;
229 -
	gap: 0.5rem;
230 -
	width: 100%;
112 +
  display: flex;
113 +
  gap: 0.5rem;
114 +
  width: 100%;
231 115
}
232 116
233 117
.discover-row input {
234 -
	flex: 1;
235 -
	background: #1a1a1c;
236 -
	color: #ffffff;
237 -
	border: 1px solid #333;
238 -
	padding: 10px;
239 -
	font-family: "Commit Mono", monospace, sans-serif;
240 -
	font-size: 14px;
241 -
	outline: none;
242 -
}
243 -
244 -
.discover-row input:focus {
245 -
	border-color: #666;
118 +
  flex: 1;
246 119
}
247 120
248 121
.discover-status {
249 -
	font-size: 12px;
122 +
  font-size: 12px;
250 123
}
251 124
252 125
.discover-results {
253 -
	display: flex;
254 -
	flex-direction: column;
255 -
	gap: 0.25rem;
256 -
	width: 100%;
126 +
  display: flex;
127 +
  flex-direction: column;
128 +
  gap: 0.25rem;
129 +
  width: 100%;
257 130
}
258 131
259 132
.discover-result-item {
260 -
	background: #1a1a1c;
261 -
	color: #888;
262 -
	border: 1px solid #333;
263 -
	padding: 8px 10px;
264 -
	font-size: 12px;
265 -
	text-align: left;
266 -
	cursor: pointer;
267 -
	width: 100%;
268 -
	white-space: nowrap;
269 -
	overflow: hidden;
270 -
	text-overflow: ellipsis;
133 +
  background: #121113;
134 +
  color: #ffffff;
135 +
  border: 1px solid #333;
136 +
  padding: 8px 10px;
137 +
  font-size: 12px;
138 +
  text-align: left;
139 +
  cursor: pointer;
140 +
  width: 100%;
141 +
  white-space: nowrap;
142 +
  overflow: hidden;
143 +
  text-overflow: ellipsis;
144 +
  opacity: 0.7;
145 +
  border-radius: 0;
146 +
  -webkit-appearance: none;
147 +
  appearance: none;
271 148
}
272 149
273 150
.discover-result-item:hover {
274 -
	border-color: #666;
275 -
	color: #ffffff;
151 +
  border-color: #555;
152 +
  opacity: 1;
276 153
}
277 154
278 155
.discover-result-item.active {
279 -
	border-color: #6bff8a;
280 -
	color: #ffffff;
156 +
  border-color: #ffffff;
157 +
  opacity: 1;
281 158
}
282 159
160 +
/* Admin subs */
161 +
283 162
.admin-subs {
284 -
	width: 100%;
285 -
	display: flex;
286 -
	flex-direction: column;
287 -
	gap: 1rem;
163 +
  width: 100%;
164 +
  display: flex;
165 +
  flex-direction: column;
166 +
  gap: 1rem;
167 +
}
168 +
169 +
.admin-subs h3 {
170 +
  font-size: 14px;
171 +
  opacity: 0.5;
172 +
  font-weight: 400;
288 173
}
289 174
290 175
.feed-item form.inline {
291 -
	display: flex;
292 -
	gap: 0.5rem;
293 -
	align-items: center;
176 +
  display: flex;
177 +
  gap: 0.5rem;
178 +
  align-items: center;
294 179
}
295 180
296 181
.feed-item form.inline input {
297 -
	flex: 1;
298 -
	background: #1a1a1c;
299 -
	color: #ffffff;
300 -
	border: 1px solid #333;
301 -
	padding: 10px;
302 -
	font-family: "Commit Mono", monospace, sans-serif;
303 -
	font-size: 14px;
304 -
	outline: none;
182 +
  flex: 1;
305 183
}
306 184
307 -
.feed-item form.inline input:focus {
308 -
	border-color: #666;
309 -
}
185 +
/* Generic .danger on buttons (used in admin) */
310 186
311 -
button.loading {
312 -
	cursor: wait;
187 +
button.danger,
188 +
.btn.danger {
189 +
  opacity: 0.5;
313 190
}
314 191
315 -
.spinner::after {
316 -
	content: "⠋";
317 -
	display: inline-block;
318 -
	animation: braille-spin 0.8s steps(10) infinite;
192 +
button.danger:hover,
193 +
.btn.danger:hover {
194 +
  opacity: 0.3;
319 195
}
320 196
321 -
@keyframes braille-spin {
322 -
	0%   { content: "⠋"; }
323 -
	10%  { content: "⠙"; }
324 -
	20%  { content: "⠹"; }
325 -
	30%  { content: "⠸"; }
326 -
	40%  { content: "⠼"; }
327 -
	50%  { content: "⠴"; }
328 -
	60%  { content: "⠦"; }
329 -
	70%  { content: "⠧"; }
330 -
	80%  { content: "⠇"; }
331 -
	90%  { content: "⠏"; }
197 +
/* Category list (admin) */
198 +
199 +
.category-list {
200 +
  list-style: none;
201 +
  margin-left: 0;
332 202
}
333 203
334 -
.admin-subs h3 {
335 -
	font-size: 14px;
336 -
	color: #888;
337 -
	font-weight: 400;
204 +
.category-list li {
205 +
  display: flex;
206 +
  justify-content: space-between;
207 +
  align-items: center;
208 +
  padding: 0.25rem 0;
338 209
}
339 210
340 -
@font-face {
341 -
	font-family: "Commit Mono";
342 -
	src: url("fonts/CommitMono-400-Regular.otf") format("opentype");
343 -
	font-weight: 400;
344 -
	font-style: normal;
345 -
}
211 +
@media (max-width: 480px) {
212 +
  .feed-meta {
213 +
    flex-direction: column;
214 +
    align-items: flex-start;
215 +
    gap: 0.25rem;
216 +
  }
346 217
347 -
@font-face {
348 -
	font-family: "Commit Mono";
349 -
	src: url("fonts/CommitMono-700-Regular.otf") format("opentype");
350 -
	font-weight: 700;
351 -
	font-style: normal;
218 +
  .feed-title {
219 +
    font-size: 14px;
220 +
  }
352 221
}
apps/jotts/Cargo.toml +1 −0
22 22
tracing-subscriber = { workspace = true }
23 23
andromeda-auth = { workspace = true }
24 24
andromeda-db = { workspace = true, features = ["session", "axum"] }
25 +
andromeda-darkmatter-css = { workspace = true }
25 26
askama = "0.15"
26 27
askama_web = { version = "0.15", features = ["axum-0.8"] }
27 28
pulldown-cmark = "0.12"
apps/jotts/src/server.rs +1 −0
428 428
        // Static assets
429 429
        .route("/static/{*path}", get(serve_static))
430 430
        .merge(api_router)
431 +
        .merge(andromeda_darkmatter_css::router::<Arc<AppState>>())
431 432
        .with_state(state);
432 433
433 434
    let addr = format!("{}:{}", host, port);
apps/jotts/static/styles.css +3 −213
1 -
@font-face {
2 -
  font-family: "Commit Mono";
3 -
  src: url("/static/fonts/CommitMono-400-Regular.otf") format("opentype");
4 -
  font-weight: 400;
5 -
  font-style: normal;
6 -
}
7 -
8 -
@font-face {
9 -
  font-family: "Commit Mono";
10 -
  src: url("/static/fonts/CommitMono-700-Regular.otf") format("opentype");
11 -
  font-weight: 700;
12 -
  font-style: normal;
13 -
}
14 -
15 -
* {
16 -
  padding: 0;
17 -
  margin: 0;
18 -
  box-sizing: border-box;
19 -
  font-family: "Commit Mono", monospace, sans-serif;
20 -
  scrollbar-width: none;
21 -
  -ms-overflow-style: none;
22 -
}
23 -
24 -
html {
25 -
  background: #121113;
26 -
  color: #ffffff;
27 -
  font-size: 14px;
28 -
  line-height: 1.6;
29 -
}
30 -
31 -
html::-webkit-scrollbar {
32 -
  display: none;
33 -
}
34 -
35 -
body {
36 -
  display: flex;
37 -
  flex-direction: column;
38 -
  justify-content: start;
39 -
  align-items: start;
40 -
  gap: 1.5rem;
41 -
  min-height: 100vh;
42 -
  max-width: 700px;
43 -
  margin: auto;
44 -
  padding: 0 1rem;
45 -
}
46 -
47 -
@media (max-width: 480px) {
48 -
  body {
49 -
    padding: 1rem;
50 -
    gap: 1rem;
51 -
  }
52 -
}
53 -
54 -
a {
55 -
  color: #ffffff;
56 -
  text-decoration: none;
57 -
}
58 -
59 -
a:hover {
60 -
  opacity: 0.7;
61 -
}
62 -
63 -
/* Header */
64 -
65 -
.header {
66 -
  display: flex;
67 -
  flex-direction: column;
68 -
  gap: 0.5rem;
69 -
  width: 100%;
70 -
  margin-top: 2rem;
71 -
  border-bottom: 1px solid #333;
72 -
  padding-bottom: 1rem;
73 -
}
74 -
75 -
.logo {
76 -
  font-size: 28px;
77 -
  font-weight: 700;
78 -
  text-decoration: none;
79 -
  text-transform: uppercase;
80 -
}
81 -
82 -
.links {
83 -
  display: flex;
84 -
  align-items: center;
85 -
  gap: 0.75rem;
86 -
  font-size: 12px;
87 -
}
88 -
89 -
/* Main content */
90 -
91 -
main {
92 -
  width: 100%;
93 -
  display: flex;
94 -
  flex-direction: column;
95 -
  gap: 1rem;
96 -
}
97 -
98 -
/* Forms */
99 -
100 -
.form {
101 -
  display: flex;
102 -
  flex-direction: column;
103 -
  gap: 0.5rem;
104 -
  width: 100%;
105 -
}
106 -
107 -
label {
108 -
  font-size: 12px;
109 -
  opacity: 0.7;
110 -
}
111 -
112 -
input, textarea {
113 -
  background: #121113;
114 -
  color: #ffffff;
115 -
  border: 1px solid white;
116 -
  padding: 0.4rem 0.75rem;
117 -
  font-size: 16px;
118 -
  width: 100%;
119 -
  border-radius: 0;
120 -
}
121 -
122 -
textarea {
123 -
  min-height: 400px;
124 -
  resize: vertical;
125 -
}
126 -
127 -
button {
128 -
  background: #121113;
129 -
  color: #ffffff;
130 -
  padding: 0.4rem 0.75rem;
131 -
  border: 1px solid white;
132 -
  cursor: pointer;
133 -
  width: fit-content;
134 -
  font-size: 14px;
135 -
  border-radius: 0;
136 -
}
137 -
138 -
button:hover {
139 -
  opacity: 0.7;
140 -
}
141 -
142 -
/* Error */
143 -
144 -
.error {
145 -
  color: #ffffff;
146 -
  border-left: 2px solid #ffffff;
147 -
  padding-left: 0.5rem;
148 -
  font-size: 13px;
149 -
  opacity: 0.8;
150 -
}
151 -
152 -
/* Note list */
1 +
/* jotts — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
153 4
154 5
.note-list {
155 6
  display: flex;
179 30
  opacity: 0.5;
180 31
}
181 32
182 -
.empty {
183 -
  opacity: 0.5;
184 -
  font-size: 12px;
185 -
}
186 -
187 33
/* Note view */
188 34
189 35
.note-header {
204 50
  font-size: 12px;
205 51
}
206 52
207 -
.inline-form {
208 -
  display: inline;
209 -
}
210 -
211 -
.link-button {
212 -
  background: none;
213 -
  border: none;
214 -
  color: #ffffff;
215 -
  cursor: pointer;
216 -
  font-size: 12px;
217 -
  padding: 0;
218 -
}
219 -
220 -
.link-button:hover {
221 -
  opacity: 0.7;
222 -
}
223 -
224 53
/* Markdown rendered content */
225 54
226 55
.markdown-body {
260 89
  margin-bottom: 0.25rem;
261 90
}
262 91
263 -
.markdown-body code {
264 -
  background: #1e1c1f;
265 -
  padding: 2px 4px;
266 -
  font-size: 13px;
267 -
}
268 -
269 92
.markdown-body pre {
270 -
  background: #1e1c1f;
271 -
  padding: 12px;
272 -
  overflow-x: auto;
273 93
  margin-bottom: 0.75rem;
274 -
  border: 1px solid #333;
275 -
}
276 -
277 -
.markdown-body pre code {
278 -
  background: none;
279 -
  padding: 0;
280 94
}
281 95
282 96
.markdown-body blockquote {
287 101
}
288 102
289 103
.markdown-body table {
290 -
  width: 100%;
291 -
  border-collapse: collapse;
292 104
  margin-bottom: 0.75rem;
293 105
}
294 106
301 113
302 114
.markdown-body th {
303 115
  font-weight: 700;
304 -
  text-transform: uppercase;
305 -
  opacity: 0.5;
306 -
  font-size: 12px;
307 116
}
308 117
309 118
.markdown-body hr {
326 135
}
327 136
328 137
.markdown-body input[type="checkbox"] {
329 -
  -webkit-appearance: none;
330 -
  appearance: none;
331 138
  width: 14px;
332 139
  height: 14px;
333 -
  background: transparent;
334 -
  border: 1px solid white;
335 -
  border-radius: 0;
336 -
  padding: 0;
337 140
  margin-right: 6px;
338 141
  vertical-align: middle;
339 142
  position: relative;
340 143
  top: -1px;
341 -
  cursor: pointer;
342 -
}
343 -
344 -
.markdown-body input[type="checkbox"]:checked::after {
345 -
  content: '✔︎';
346 -
  position: absolute;
347 -
  top: 50%;
348 -
  left: 50%;
349 -
  transform: translate(-50%, -50%);
350 -
  font-size: 12px;
351 -
  color: white;
352 -
  line-height: 1;
353 -
}
354 144
}
apps/jotts/templates/base.html +1 −0
13 13
  <meta property="og:image" content="/static/og.png">
14 14
  <meta property="og:type" content="website">
15 15
  <meta name="theme-color" content="#121113" />
16 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
16 17
  <link rel="stylesheet" href="/static/styles.css">
17 18
</head>
18 19
<body>
apps/jotts/templates/login.html +1 −0
13 13
  <meta property="og:image" content="/static/og.png">
14 14
  <meta property="og:type" content="website">
15 15
  <meta name="theme-color" content="#121113" />
16 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
16 17
  <link rel="stylesheet" href="/static/styles.css">
17 18
</head>
18 19
<body>
apps/og/Cargo.toml +1 −0
17 17
dotenvy = { workspace = true }
18 18
tracing = { workspace = true }
19 19
tracing-subscriber = { workspace = true }
20 +
andromeda-darkmatter-css = { workspace = true }
20 21
askama = "0.13"
21 22
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }
22 23
scraper = "0.22"
apps/og/src/server.rs +2 −1
140 140
    let app = Router::new()
141 141
        .route("/", get(get_index))
142 142
        .route("/check", post(post_check))
143 -
        .route("/static/{*path}", get(static_handler));
143 +
        .route("/static/{*path}", get(static_handler))
144 +
        .merge(andromeda_darkmatter_css::router::<()>());
144 145
145 146
    let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
146 147
    let addr = format!("0.0.0.0:{port}");
apps/og/static/styles.css +6 −120
1 -
@font-face {
2 -
    font-family: "Commit Mono";
3 -
    src: url("/static/fonts/CommitMono-400-Regular.otf") format("opentype");
4 -
    font-weight: 400;
5 -
    font-style: normal;
6 -
}
7 -
8 -
@font-face {
9 -
    font-family: "Commit Mono";
10 -
    src: url("/static/fonts/CommitMono-700-Regular.otf") format("opentype");
11 -
    font-weight: 700;
12 -
    font-style: normal;
13 -
}
14 -
15 -
* {
16 -
    padding: 0;
17 -
    margin: 0;
18 -
    box-sizing: border-box;
19 -
    font-family: "Commit Mono", monospace, sans-serif;
20 -
    scrollbar-width: none;
21 -
    -ms-overflow-style: none;
22 -
}
23 -
24 -
html {
25 -
    background: #121113;
26 -
    color: #ffffff;
27 -
    font-size: 14px;
28 -
    line-height: 1.6;
29 -
}
30 -
31 -
html::-webkit-scrollbar {
32 -
    display: none;
33 -
}
34 -
35 -
body {
36 -
    display: flex;
37 -
    flex-direction: column;
38 -
    justify-content: start;
39 -
    align-items: start;
40 -
    gap: 1.5rem;
41 -
    min-height: 100vh;
42 -
    max-width: 700px;
43 -
    margin: auto;
44 -
    padding: 0 1rem 6rem;
45 -
}
46 -
47 -
a {
48 -
    color: #ffffff;
49 -
    text-decoration: none;
50 -
}
51 -
52 -
a:hover {
53 -
    opacity: 0.7;
54 -
}
55 -
56 -
/* === HEADER === */
57 -
58 -
.header {
59 -
    display: flex;
60 -
    flex-direction: column;
61 -
    gap: 0.5rem;
62 -
    width: 100%;
63 -
    margin-top: 2rem;
64 -
    border-bottom: 1px solid #333;
65 -
    padding-bottom: 1rem;
66 -
}
67 -
68 -
.logo {
69 -
    font-size: 28px;
70 -
    font-weight: 700;
71 -
    text-decoration: none;
72 -
    text-transform: uppercase;
73 -
}
74 -
75 -
/* === INDEX PAGE === */
1 +
/* og — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
76 4
77 5
.index-container {
78 6
    width: 100%;
91 19
92 20
.check-form input {
93 21
    flex: 1;
94 -
    background: #121113;
95 -
    color: #ffffff;
96 -
    border: 1px solid white;
97 -
    padding: 0.4rem 0.75rem;
98 -
    font-size: 14px;
99 -
    border-radius: 0;
100 -
    outline: none;
101 -
    width: 100%;
102 22
}
103 23
104 24
.check-form input::placeholder {
106 26
}
107 27
108 28
.check-form button {
109 -
    background: #121113;
110 -
    color: #ffffff;
111 -
    padding: 0.4rem 0.75rem;
112 -
    border: 1px solid white;
113 -
    cursor: pointer;
114 -
    width: fit-content;
115 29
    flex-shrink: 0;
116 30
    white-space: nowrap;
117 -
    font-size: 14px;
118 31
    font-weight: 700;
119 -
    border-radius: 0;
120 32
}
121 33
122 -
.check-form button:hover {
123 -
    opacity: 0.7;
124 -
}
125 -
126 -
/* === RESULTS PAGE === */
34 +
/* Results page */
127 35
128 36
.results-container {
129 37
    display: flex;
144 52
    word-break: break-all;
145 53
}
146 54
147 -
label, .label {
55 +
.label {
148 56
    display: block;
149 57
    font-size: 12px;
150 58
    opacity: 0.7;
155 63
.results-meta span:last-child {
156 64
    opacity: 0.5;
157 65
    font-size: 12px;
158 -
}
159 -
160 -
/* === ERROR === */
161 -
162 -
.error {
163 -
    border-left: 2px solid #ffffff;
164 -
    padding-left: 0.5rem;
165 -
    font-size: 13px;
166 -
    opacity: 0.8;
167 66
}
168 67
169 68
.error h2 {
172 71
    margin-bottom: 0.25rem;
173 72
}
174 73
175 -
/* === PREVIEWS === */
176 -
177 74
.preview-section {
178 75
    display: flex;
179 76
    flex-direction: column;
196 93
    width: 100%;
197 94
    height: auto;
198 95
}
199 -
200 -
/* === TAGS === */
201 96
202 97
.tag-section {
203 98
    display: flex;
261 156
    font-size: 14px;
262 157
}
263 158
264 -
/* === BACK LINK === */
265 -
266 159
.back-link {
267 160
    display: inline-block;
268 161
    font-size: 12px;
276 169
    opacity: 0.7;
277 170
}
278 171
279 -
/* === RESPONSIVE === */
280 -
281 172
@media (max-width: 480px) {
282 -
    body {
283 -
        padding: 1rem;
284 -
        gap: 1rem;
285 -
    }
286 -
287 -
.tag-item {
173 +
    .tag-item {
288 174
        flex-direction: column;
289 175
        gap: 0.25rem;
290 176
    }
apps/og/templates/base.html +1 −0
14 14
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
15 15
    <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
16 16
    <link rel="manifest" href="/static/site.webmanifest">
17 +
    <link rel="stylesheet" href="/assets/darkmatter.css">
17 18
    <link rel="stylesheet" href="/static/styles.css">
18 19
</head>
19 20
<body>
apps/parcels/Cargo.toml +1 −0
23 23
tracing-subscriber = { workspace = true, features = ["env-filter"] }
24 24
andromeda-auth = { workspace = true }
25 25
andromeda-db = { workspace = true, features = ["session"] }
26 +
andromeda-darkmatter-css = { workspace = true }
26 27
rusqlite = { workspace = true }
27 28
reqwest = { version = "0.12", features = ["json"] }
28 29
askama = { version = "0.12", features = ["with-axum"] }
apps/parcels/src/main.rs +1 −0
372 372
        .route("/packages/{id}/refresh", post(post_refresh_package))
373 373
        .route("/packages/{id}/delete", post(post_delete_package))
374 374
        .nest_service("/static", ServeDir::new("static"))
375 +
        .merge(andromeda_darkmatter_css::router::<Arc<AppState>>())
375 376
        .with_state(state);
376 377
377 378
    let listener = tokio::net::TcpListener::bind(&bind_addr)
apps/parcels/static/styles.css (added) +126 −0
1 +
/* parcels — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
4 +
5 +
.null {
6 +
  opacity: 0.3;
7 +
}
8 +
9 +
.parcel-list {
10 +
  display: flex;
11 +
  flex-direction: column;
12 +
  width: 100%;
13 +
}
14 +
15 +
.parcel-list a {
16 +
  text-decoration: none;
17 +
}
18 +
19 +
.parcel-item {
20 +
  display: flex;
21 +
  flex-direction: column;
22 +
  gap: 0.25rem;
23 +
  padding: 0.75rem 0;
24 +
  border-bottom: 1px solid #333;
25 +
}
26 +
27 +
.parcel-item:last-child {
28 +
  border-bottom: none;
29 +
}
30 +
31 +
.parcel-label {
32 +
  font-size: 16px;
33 +
}
34 +
35 +
.parcel-tracking {
36 +
  font-size: 12px;
37 +
  opacity: 0.5;
38 +
}
39 +
40 +
.parcel-meta {
41 +
  font-size: 12px;
42 +
  opacity: 0.5;
43 +
  font-style: italic;
44 +
}
45 +
46 +
.detail-header {
47 +
  display: flex;
48 +
  justify-content: space-between;
49 +
  align-items: baseline;
50 +
  margin-bottom: 0.25rem;
51 +
}
52 +
53 +
.detail-title {
54 +
  font-size: 16px;
55 +
  font-weight: 700;
56 +
}
57 +
58 +
.detail-tracking {
59 +
  opacity: 0.6;
60 +
  margin-bottom: 1rem;
61 +
}
62 +
63 +
.detail-table {
64 +
  width: auto;
65 +
  margin-bottom: 1.5rem;
66 +
}
67 +
68 +
.detail-table th {
69 +
  padding-right: 2rem;
70 +
}
71 +
72 +
.event-section-header {
73 +
  opacity: 0.5;
74 +
  font-size: 12px;
75 +
  text-transform: uppercase;
76 +
  margin-bottom: 1rem;
77 +
  border-bottom: 1px solid #333;
78 +
  padding-bottom: 0.5rem;
79 +
}
80 +
81 +
.event-item {
82 +
  padding: 0.75rem 0;
83 +
  border-bottom: 1px solid #1e1c1f;
84 +
}
85 +
86 +
.event-timestamp {
87 +
  opacity: 0.5;
88 +
  font-size: 12px;
89 +
  margin-bottom: 0.2rem;
90 +
}
91 +
92 +
.event-type {
93 +
  font-weight: 700;
94 +
}
95 +
96 +
.event-location {
97 +
  opacity: 0.6;
98 +
  font-size: 13px;
99 +
}
100 +
101 +
.empty-note {
102 +
  opacity: 0.4;
103 +
}
104 +
105 +
.login-wrap {
106 +
  max-width: 320px;
107 +
  margin: 4rem auto;
108 +
  width: 100%;
109 +
}
110 +
111 +
.login-wrap h1 {
112 +
  font-size: 24px;
113 +
  margin-bottom: 2rem;
114 +
  font-weight: 700;
115 +
}
116 +
117 +
.form-group {
118 +
  margin-bottom: 1rem;
119 +
}
120 +
121 +
.form-group label {
122 +
  display: block;
123 +
  margin-bottom: 0.25rem;
124 +
  opacity: 0.7;
125 +
  font-size: 12px;
126 +
}
apps/parcels/templates/add.html +11 −6
2 2
{% block title %}PARCELS — add package{% endblock %}
3 3
{% block content %}
4 4
<div class="header">
5 -
  <h1><a href="/" style="text-decoration:none;">PARCELS</a></h1>
5 +
  <a href="/" class="logo">PARCELS</a>
6 +
  <nav class="links">
7 +
    <a href="/">← back</a>
8 +
  </nav>
6 9
</div>
7 10
{% if let Some(err) = error %}
8 -
<div class="error">{{ err }}</div>
11 +
<p class="error">{{ err }}</p>
9 12
{% endif %}
10 -
<form method="POST" action="/packages" style="max-width: 400px;">
11 -
  <div class="form-group">
13 +
<form class="form" method="POST" action="/packages">
14 +
  <div class="form-field">
12 15
    <label for="tracking_number">tracking number</label>
13 16
    <input type="text" id="tracking_number" name="tracking_number" required autofocus
14 17
           placeholder="e.g. 9400111899223397992148">
15 18
  </div>
16 -
  <div class="form-group">
19 +
  <div class="form-field">
17 20
    <label for="label">label</label>
18 21
    <input type="text" id="label" name="label" required placeholder="e.g. Amazon order">
19 22
  </div>
20 -
  <button type="submit">add package</button>
23 +
  <div class="form-actions">
24 +
    <button type="submit">add package</button>
25 +
  </div>
21 26
</form>
22 27
{% endblock %}
apps/parcels/templates/base.html +4 −88
3 3
<head>
4 4
  <meta charset="UTF-8">
5 5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 +
  <meta name="theme-color" content="#121113" />
6 7
  <title>{% block title %}Parcels{% endblock %}</title>
8 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
9 +
  <link rel="stylesheet" href="/static/styles.css">
7 10
  <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
8 11
  <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
9 12
  <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
12 15
  <meta property="og:title" content="PARCELS">
13 16
  <meta property="og:image" content="/static/og.png">
14 17
  <meta property="og:type" content="website">
15 -
  <style>
16 -
    @font-face {
17 -
      font-family: 'CommitMono';
18 -
      src: url('/static/fonts/CommitMono-400-Regular.otf') format('opentype');
19 -
      font-weight: 400;
20 -
    }
21 -
    @font-face {
22 -
      font-family: 'CommitMono';
23 -
      src: url('/static/fonts/CommitMono-700-Regular.otf') format('opentype');
24 -
      font-weight: 700;
25 -
    }
26 -
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27 -
    body {
28 -
      background: #121113;
29 -
      color: #ffffff;
30 -
      font-family: 'CommitMono', monospace;
31 -
      font-size: 14px;
32 -
      line-height: 1.6;
33 -
      min-height: 100vh;
34 -
      max-width: 700px;
35 -
      margin: auto;
36 -
      padding: 0 1rem;
37 -
      display: flex;
38 -
      flex-direction: column;
39 -
      gap: 1.5rem;
40 -
    }
41 -
    .container { padding: 0; }
42 -
    a { color: #ffffff; text-decoration: none; }
43 -
    a:hover { opacity: 0.7; }
44 -
    input, button {
45 -
      font-family: 'CommitMono', monospace;
46 -
      font-size: 14px;
47 -
      background: #121113;
48 -
      color: #ffffff;
49 -
      border: 1px solid #ffffff;
50 -
      padding: 0.4rem 0.75rem;
51 -
      border-radius: 0;
52 -
      cursor: pointer;
53 -
    }
54 -
    button:hover { opacity: 0.7; }
55 -
    input { width: 100%; }
56 -
    .error {
57 -
      color: #ffffff;
58 -
      border-left: 2px solid #ffffff;
59 -
      padding-left: 0.5rem;
60 -
      font-size: 13px;
61 -
      opacity: 0.8;
62 -
    }
63 -
    table { width: 100%; border-collapse: collapse; }
64 -
    th, td {
65 -
      text-align: left;
66 -
      padding: 0.4rem 0.5rem;
67 -
      border-bottom: 1px solid #333;
68 -
      vertical-align: top;
69 -
    }
70 -
    th { opacity: 0.5; font-weight: 400; font-size: 12px; text-transform: uppercase; }
71 -
    .header {
72 -
      display: flex;
73 -
      flex-direction: column;
74 -
      gap: 0.5rem;
75 -
      margin-top: 2rem;
76 -
      border-bottom: 1px solid #333;
77 -
      padding-bottom: 1rem;
78 -
    }
79 -
    .links { display: flex; gap: 0.75rem; font-size: 12px; }
80 -
    .form-group { margin-bottom: 1rem; }
81 -
    .form-group label { display: block; margin-bottom: 0.25rem; opacity: 0.7; font-size: 12px; }
82 -
    .null { opacity: 0.3; }
83 -
    .status-delivered { /* no color accent per design */ }
84 -
    .status-alert { opacity: 0.8; }
85 -
    .actions { display: flex; gap: 0.5rem; }
86 -
    .parcel-list { display: flex; flex-direction: column; width: 100%; }
87 -
    .parcel-list a { text-decoration: none; }
88 -
    .parcel-item {
89 -
      display: flex;
90 -
      flex-direction: column;
91 -
      gap: 0.25rem;
92 -
      padding: 0.75rem 0;
93 -
      border-bottom: 1px solid #333;
94 -
    }
95 -
    .parcel-item:last-child { border-bottom: none; }
96 -
    .parcel-label { font-size: 16px; color: #fff; }
97 -
    .parcel-tracking { font-size: 12px; color: #888; }
98 -
    .parcel-meta { font-size: 12px; color: #888; font-style: italic; }
99 -
  </style>
100 18
</head>
101 19
<body>
102 -
  <div class="container">
103 -
    {% block content %}{% endblock %}
104 -
  </div>
20 +
  {% block content %}{% endblock %}
105 21
</body>
106 22
</html>
apps/parcels/templates/detail.html +17 −20
2 2
{% block title %}PARCELS — {% if let Some(label) = package.label %}{{ label }}{% else %}{{ package.tracking_number }}{% endif %}{% endblock %}
3 3
{% block content %}
4 4
<div class="header">
5 -
  <h1><a href="/" style="text-decoration:none;">PARCELS</a></h1>
6 -
  <nav>
5 +
  <a href="/" class="logo">PARCELS</a>
6 +
  <nav class="links">
7 7
    <a href="/">← back</a>
8 8
  </nav>
9 9
</div>
10 10
{% if let Some(err) = error %}
11 -
<div class="error">{{ err }}</div>
11 +
<p class="error">{{ err }}</p>
12 12
{% endif %}
13 13
14 -
<div style="margin-bottom: 2rem;">
15 -
  <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.25rem;">
14 +
<div>
15 +
  <div class="detail-header">
16 16
    {% if let Some(label) = package.label %}
17 -
    <div style="font-size: 16px; font-weight: 700;">{{ label }}</div>
17 +
    <div class="detail-title">{{ label }}</div>
18 18
    {% endif %}
19 -
    <form method="POST" action="/packages/{{ package.id }}/delete"
19 +
    <form class="inline-form" method="POST" action="/packages/{{ package.id }}/delete"
20 20
          onsubmit="return confirm('Delete this package?')">
21 -
      <button type="submit" style="font-size: 12px; opacity: 0.6; border-color: #555; margin-top: 0.5rem;">delete</button>
21 +
      <button type="submit" class="link-button danger">delete</button>
22 22
    </form>
23 23
  </div>
24 -
  <div style="opacity: 0.6; margin-bottom: 1rem;">{{ package.tracking_number }}</div>
24 +
  <div class="detail-tracking">{{ package.tracking_number }}</div>
25 25
26 -
  <table style="width: auto; margin-bottom: 1.5rem;">
26 +
  <table class="detail-table">
27 27
    <tr>
28 -
      <th style="padding-right: 2rem;">status</th>
28 +
      <th>status</th>
29 29
      <td>{% if let Some(s) = package.status %}{{ s }}{% else %}<span class="null">—</span>{% endif %}</td>
30 30
    </tr>
31 31
    <tr>
41 41
      <td>{% if let Some(d) = package.expected_delivery %}{{ d }}{% else %}<span class="null">—</span>{% endif %}</td>
42 42
    </tr>
43 43
  </table>
44 -
45 44
</div>
46 45
47 46
{% if !events.is_empty() %}
48 47
<div>
49 -
  <div style="opacity: 0.5; font-size: 12px; text-transform: uppercase; margin-bottom: 1rem; border-bottom: 1px solid #333; padding-bottom: 0.5rem;">
50 -
    event history
51 -
  </div>
48 +
  <div class="event-section-header">event history</div>
52 49
  {% for event in events %}
53 -
  <div style="padding: 0.75rem 0; border-bottom: 1px solid #1e1c1f;">
54 -
    <div style="opacity: 0.5; font-size: 12px; margin-bottom: 0.2rem;">
50 +
  <div class="event-item">
51 +
    <div class="event-timestamp">
55 52
      {% if let Some(ts) = event.event_timestamp %}{{ ts }}{% else %}<span class="null">—</span>{% endif %}
56 53
    </div>
57 -
    <div style="font-weight: 700;">
54 +
    <div class="event-type">
58 55
      {% if let Some(et) = event.event_type %}{{ et }}{% else %}<span class="null">—</span>{% endif %}
59 56
    </div>
60 57
    {% if event.event_city.is_some() || event.event_state.is_some() %}
61 -
    <div style="opacity: 0.6; font-size: 13px;">
58 +
    <div class="event-location">
62 59
      {% if let Some(city) = event.event_city %}{{ city }}{% endif %}{% if event.event_city.is_some() && event.event_state.is_some() %}, {% endif %}{% if let Some(state) = event.event_state %}{{ state }}{% endif %}
63 60
    </div>
64 61
    {% endif %}
66 63
  {% endfor %}
67 64
</div>
68 65
{% else %}
69 -
<p style="opacity: 0.4;">no events yet. click refresh to load tracking history.</p>
66 +
<p class="empty">no events yet. click refresh to load tracking history.</p>
70 67
{% endif %}
71 68
{% endblock %}
apps/parcels/templates/index.html +6 −5
2 2
{% block title %}PARCELS{% endblock %}
3 3
{% block content %}
4 4
<div class="header">
5 -
  <h1>PARCELS</h1>
6 -
  <div class="links">
5 +
  <a href="/" class="logo">PARCELS</a>
6 +
  <nav class="links">
7 7
    <a href="/packages/add">add package</a>
8 -
  </div>
8 +
    <a href="/logout">logout</a>
9 +
  </nav>
9 10
</div>
10 11
{% if let Some(err) = error %}
11 -
<div class="error">{{ err }}</div>
12 +
<p class="error">{{ err }}</p>
12 13
{% endif %}
13 14
{% if packages.is_empty() %}
14 -
<p style="opacity: 0.4;">no packages. <a href="/packages/add">add one</a></p>
15 +
<p class="empty">no packages. <a href="/packages/add">add one</a></p>
15 16
{% else %}
16 17
<div class="parcel-list">
17 18
  {% for pkg in packages %}
apps/parcels/templates/login.html +6 −6
1 1
{% extends "base.html" %}
2 2
{% block title %}PARCELS — login{% endblock %}
3 3
{% block content %}
4 -
<div style="max-width: 320px; margin: 4rem auto;">
5 -
  <h1 style="font-size: 24px; margin-bottom: 2rem; font-weight: 700;">PARCELS</h1>
4 +
<div class="login-wrap">
5 +
  <h1>PARCELS</h1>
6 6
  {% if let Some(err) = error %}
7 -
  <div class="error">{{ err }}</div>
7 +
  <p class="error">{{ err }}</p>
8 8
  {% endif %}
9 -
  <form method="POST" action="/login">
10 -
    <div class="form-group">
9 +
  <form class="form" method="POST" action="/login">
10 +
    <div class="form-field">
11 11
      <label for="password">password</label>
12 12
      <input type="password" id="password" name="password" autofocus required>
13 13
    </div>
14 -
    <button type="submit" style="width: 100%; margin-top: 0.5rem;">sign in</button>
14 +
    <button type="submit">sign in</button>
15 15
  </form>
16 16
</div>
17 17
{% endblock %}
apps/posts/Cargo.toml +1 −0
22 22
tracing-subscriber = { workspace = true }
23 23
andromeda-auth = { workspace = true }
24 24
andromeda-db = { workspace = true, features = ["session"] }
25 +
andromeda-darkmatter-css = { workspace = true }
25 26
serde_rusqlite = "0.41"
26 27
askama = "0.15"
27 28
askama_web = { version = "0.15", features = ["axum-0.8"] }
apps/posts/src/server/mod.rs +2 −0
580 580
        .route("/files/{filename}", get(public::serve_uploaded_file))
581 581
        // Static assets
582 582
        .route("/static/{*path}", get(public::serve_static))
583 +
        // Shared Darkmatter CSS assets + /darkmatter gallery
584 +
        .merge(andromeda_darkmatter_css::router::<Arc<AppState>>())
583 585
        // Fallback
584 586
        .fallback(get(public::fallback_handler))
585 587
        .with_state(state)
apps/posts/static/fonts/CommitMono-400-Regular.otf → crates/darkmatter-css/assets/fonts/CommitMono-400-Regular.otf +0 −0

Binary file — no preview.

apps/posts/static/fonts/CommitMono-700-Regular.otf → crates/darkmatter-css/assets/fonts/CommitMono-700-Regular.otf +0 −0

Binary file — no preview.

apps/posts/static/styles.css +34 −535
1 -
@font-face {
2 -
  font-family: "Commit Mono";
3 -
  src: url("/static/fonts/CommitMono-400-Regular.otf") format("opentype");
4 -
  font-weight: 400;
5 -
  font-style: normal;
6 -
}
7 -
8 -
@font-face {
9 -
  font-family: "Commit Mono";
10 -
  src: url("/static/fonts/CommitMono-700-Regular.otf") format("opentype");
11 -
  font-weight: 700;
12 -
  font-style: normal;
13 -
}
14 -
15 -
* {
16 -
  padding: 0;
17 -
  margin: 0;
18 -
  box-sizing: border-box;
19 -
  font-family: "Commit Mono", monospace, sans-serif;
20 -
  scrollbar-width: none;
21 -
  -ms-overflow-style: none;
22 -
}
23 -
24 -
html {
25 -
  background: #121113;
26 -
  color: #ffffff;
27 -
  font-size: 14px;
28 -
  line-height: 1.6;
29 -
}
30 -
31 -
html::-webkit-scrollbar {
32 -
  display: none;
33 -
}
34 -
35 -
body {
36 -
  display: flex;
37 -
  flex-direction: column;
38 -
  justify-content: start;
39 -
  align-items: start;
40 -
  gap: 1.5rem;
41 -
  min-height: 100vh;
42 -
  max-width: 700px;
43 -
  margin: auto;
44 -
  padding: 0 1rem 4rem;
45 -
}
46 -
47 -
@media (max-width: 480px) {
48 -
  body {
49 -
    padding: 1rem;
50 -
    gap: 1rem;
51 -
  }
52 -
}
53 -
54 -
a {
55 -
  color: #ffffff;
56 -
  text-decoration: none;
57 -
}
58 -
59 -
a:hover {
60 -
  opacity: 0.7;
61 -
}
1 +
/* posts — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
62 4
63 -
/* Header */
64 -
65 -
.header {
66 -
  display: flex;
67 -
  flex-direction: column;
68 -
  gap: 0.5rem;
69 -
  width: 100%;
70 -
  margin-top: 2rem;
71 -
  border-bottom: 1px solid #333;
72 -
  padding-bottom: 1rem;
73 -
}
74 -
75 -
.logo {
76 -
  font-size: 28px;
77 -
  font-weight: 700;
78 -
  text-decoration: none;
79 -
  text-transform: uppercase;
80 -
}
81 -
82 -
.links {
83 -
  display: flex;
84 -
  align-items: center;
85 -
  gap: 0.75rem;
86 -
  font-size: 12px;
87 -
}
88 -
89 -
/* Main content */
90 -
91 -
main {
92 -
  width: 100%;
93 -
  display: flex;
94 -
  flex-direction: column;
95 -
  gap: 1rem;
96 -
}
97 -
98 -
/* Forms */
99 -
100 -
.form {
101 -
  display: flex;
102 -
  flex-direction: column;
103 -
  gap: 0.5rem;
104 -
  width: 100%;
105 -
}
106 -
107 -
label {
108 -
  font-size: 12px;
109 -
  opacity: 0.7;
110 -
}
111 -
112 -
input, textarea, select {
113 -
  background: #121113;
114 -
  color: #ffffff;
115 -
  border: 1px solid white;
116 -
  padding: 0.4rem 0.75rem;
117 -
  font-size: 16px;
118 -
  width: 100%;
119 -
  border-radius: 0;
120 -
}
121 -
122 -
textarea {
123 -
  min-height: 400px;
124 -
  resize: vertical;
125 -
}
5 +
/* Textarea variants */
126 6
127 7
textarea.post-content {
128 8
  min-height: 500px;
130 10
131 11
textarea.attributes-textarea {
132 12
  min-height: 80px;
13 +
}
14 +
15 +
.nav-links-input {
16 +
  min-height: 40px;
17 +
  height: 40px;
133 18
}
134 19
135 20
.available-fields {
152 37
  opacity: 0.6;
153 38
}
154 39
155 -
button, .btn {
156 -
  background: #121113;
157 -
  color: #ffffff;
158 -
  padding: 0.4rem 0.75rem;
159 -
  border: 1px solid white;
160 -
  cursor: pointer;
161 -
  width: fit-content;
162 -
  font-size: 14px;
163 -
  border-radius: 0;
164 -
  text-decoration: none;
165 -
  display: inline-block;
166 -
}
167 -
168 -
button:hover, .btn:hover {
169 -
  opacity: 0.7;
170 -
}
171 -
172 -
/* Error / Success */
173 -
174 -
.error {
175 -
  color: #ffffff;
176 -
  border-left: 2px solid #ffffff;
177 -
  padding-left: 0.5rem;
178 -
  font-size: 13px;
179 -
  opacity: 0.8;
180 -
}
181 -
182 -
.success {
183 -
  color: #ffffff;
184 -
  border-left: 2px solid #555;
185 -
  padding-left: 0.5rem;
186 -
  font-size: 13px;
187 -
  opacity: 0.7;
188 -
}
189 -
190 -
.empty {
191 -
  opacity: 0.5;
192 -
  font-size: 12px;
193 -
}
194 -
195 40
/* Post list (public) */
196 41
197 42
.post-list {
219 64
  gap: 0.25rem;
220 65
}
221 66
67 +
.post-item-enhanced .post-item-info {
68 +
  gap: 0.25rem;
69 +
}
70 +
222 71
.post-title {
223 72
  font-size: 16px;
224 73
}
233 82
  opacity: 0.5;
234 83
}
235 84
236 -
/* Tags */
85 +
.post-excerpt {
86 +
  font-size: 12px;
87 +
  opacity: 0.6;
88 +
  line-height: 1.4;
89 +
}
237 90
238 91
.post-tags {
239 92
  display: flex;
240 93
  gap: 0.4rem;
241 94
  flex-wrap: wrap;
242 -
}
243 -
244 -
.tag {
245 -
  font-size: 11px;
246 -
  opacity: 0.5;
247 -
  background: #1e1c1f;
248 -
  padding: 1px 6px;
249 -
  border: 1px solid #333;
250 95
}
251 96
252 97
/* Post header (public single) */
275 120
  letter-spacing: -0.5px;
276 121
}
277 122
278 -
/* Intro */
279 -
280 123
.intro {
281 124
  padding-bottom: 1rem;
282 125
  border-bottom: 1px solid #333;
283 126
}
284 127
285 -
/* Inline form */
286 -
287 -
.inline-form {
288 -
  display: inline;
289 -
}
290 -
291 -
.link-button {
292 -
  background: none;
293 -
  border: none;
294 -
  color: #ffffff;
295 -
  cursor: pointer;
296 -
  font-size: 12px;
297 -
  padding: 0;
298 -
}
299 -
300 -
.link-button:hover {
301 -
  opacity: 0.7;
302 -
}
303 -
304 -
.link-button.danger {
305 -
  opacity: 0.5;
306 -
}
307 -
308 -
.link-button.danger:hover {
309 -
  opacity: 0.3;
310 -
}
311 -
312 -
/* Admin toolbar */
313 -
314 -
.admin-toolbar {
315 -
  display: flex;
316 -
  justify-content: space-between;
317 -
  align-items: center;
318 -
  width: 100%;
319 -
}
320 -
321 -
.admin-toolbar h2 {
322 -
  font-size: 18px;
323 -
  font-weight: 700;
324 -
}
325 -
326 -
/* Admin list */
327 -
328 -
.admin-list {
329 -
  display: flex;
330 -
  flex-direction: column;
331 -
  width: 100%;
332 -
}
333 -
334 -
.admin-list-item {
335 -
  display: flex;
336 -
  justify-content: space-between;
337 -
  align-items: center;
338 -
  padding: 8px 0;
339 -
  border-bottom: 1px solid #333;
340 -
  gap: 1rem;
341 -
}
342 -
343 -
.admin-list-info {
344 -
  display: flex;
345 -
  flex-direction: column;
346 -
  gap: 0.2rem;
347 -
  min-width: 0;
348 -
}
349 -
350 -
.admin-list-title {
351 -
  font-size: 15px;
352 -
  white-space: nowrap;
353 -
  overflow: hidden;
354 -
  text-overflow: ellipsis;
355 -
}
356 -
357 -
.admin-list-meta {
358 -
  display: flex;
359 -
  gap: 0.75rem;
360 -
  align-items: center;
361 -
}
362 -
363 -
.admin-list-date {
364 -
  font-size: 11px;
365 -
  opacity: 0.4;
366 -
}
367 -
368 -
.admin-list-actions {
369 -
  display: flex;
370 -
  gap: 1rem;
371 -
  font-size: 12px;
372 -
  flex-shrink: 0;
373 -
}
374 -
375 -
/* Status badges */
376 -
377 -
.status-badge {
378 -
  font-size: 11px;
379 -
  padding: 1px 6px;
380 -
  border: 1px solid #333;
381 -
}
382 -
383 -
.status-published {
384 -
  opacity: 1;
385 -
  border-color: #555;
386 -
}
387 -
388 -
.status-draft {
389 -
  opacity: 0.4;
390 -
}
391 -
392 -
/* Form layout */
393 -
394 -
.form-row {
395 -
  display: flex;
396 -
  gap: 0.5rem;
397 -
  width: 100%;
398 -
}
399 -
400 -
.form-row .form-field {
401 -
  flex: 1;
402 -
}
403 -
404 -
.form-field {
405 -
  display: flex;
406 -
  flex-direction: column;
407 -
  gap: 0.25rem;
408 -
}
409 -
410 -
.checkbox-field {
411 -
  justify-content: flex-end;
412 -
}
413 -
414 -
.checkbox-field label {
415 -
  display: flex;
416 -
  align-items: center;
417 -
  gap: 0.5rem;
418 -
  font-size: 14px;
419 -
  opacity: 1;
420 -
  cursor: pointer;
421 -
}
422 -
423 -
.checkbox-field input[type="checkbox"] {
424 -
  width: 16px;
425 -
  height: 16px;
426 -
  -webkit-appearance: none;
427 -
  appearance: none;
428 -
  background: transparent;
429 -
  border: 1px solid white;
430 -
  border-radius: 0;
431 -
  cursor: pointer;
432 -
  position: relative;
433 -
}
434 -
435 -
.checkbox-field input[type="checkbox"]:checked::after {
436 -
  content: '✔︎';
437 -
  position: absolute;
438 -
  top: 50%;
439 -
  left: 50%;
440 -
  transform: translate(-50%, -50%);
441 -
  font-size: 12px;
442 -
  color: white;
443 -
  line-height: 1;
444 -
}
445 -
446 -
.form-actions {
447 -
  display: flex;
448 -
  gap: 0.5rem;
449 -
}
450 -
451 -
@media (max-width: 480px) {
452 -
  .form-row {
453 -
    flex-direction: column;
454 -
  }
455 -
456 -
  .admin-list-item {
457 -
    flex-direction: column;
458 -
    align-items: flex-start;
459 -
    gap: 0.5rem;
460 -
  }
461 -
}
462 -
463 128
/* Markdown rendered content */
464 129
465 130
.markdown-body {
499 164
  margin-bottom: 0.25rem;
500 165
}
501 166
502 -
.markdown-body code {
503 -
  background: #1e1c1f;
504 -
  padding: 2px 4px;
505 -
  font-size: 13px;
506 -
}
507 -
508 167
.markdown-body pre {
509 -
  background: #1e1c1f;
510 -
  padding: 12px;
511 -
  overflow-x: auto;
512 168
  margin-bottom: 0.75rem;
513 -
  border: 1px solid #333;
514 -
}
515 -
516 -
.markdown-body pre code {
517 -
  background: none;
518 -
  padding: 0;
519 169
}
520 170
521 171
.markdown-body blockquote {
526 176
}
527 177
528 178
.markdown-body table {
529 -
  width: 100%;
530 -
  border-collapse: collapse;
531 179
  margin-bottom: 0.75rem;
532 180
}
533 181
534 182
.markdown-body th,
535 183
.markdown-body td {
536 184
  border: 1px solid #333;
537 -
  padding: 6px;
538 -
  text-align: left;
539 -
}
540 -
541 -
.markdown-body th {
542 -
  font-weight: 700;
543 -
  text-transform: uppercase;
544 -
  opacity: 0.5;
545 -
  font-size: 12px;
546 185
}
547 186
548 187
.markdown-body hr {
565 204
}
566 205
567 206
.markdown-body input[type="checkbox"] {
568 -
  -webkit-appearance: none;
569 -
  appearance: none;
570 207
  width: 14px;
571 208
  height: 14px;
572 -
  background: transparent;
573 -
  border: 1px solid white;
574 -
  border-radius: 0;
575 -
  padding: 0;
576 209
  margin-right: 6px;
577 210
  vertical-align: middle;
578 211
  position: relative;
579 212
  top: -1px;
580 -
  cursor: pointer;
581 213
}
582 214
583 215
.markdown-body input[type="checkbox"]:checked::after {
584 -
  content: '✔︎';
585 -
  position: absolute;
586 -
  top: 50%;
587 -
  left: 50%;
588 -
  transform: translate(-50%, -50%);
589 216
  font-size: 12px;
590 -
  color: white;
591 -
  line-height: 1;
592 217
}
593 218
594 -
/* File upload input */
595 -
596 -
input[type="file"] {
597 -
  background: #121113;
598 -
  color: #ffffff;
599 -
  border: 1px solid white;
600 -
  padding: 0.4rem 0.75rem;
601 -
  font-size: 14px;
602 -
  width: 100%;
603 -
  cursor: pointer;
604 -
}
605 -
606 -
input[type="file"]::file-selector-button {
607 -
  background: #121113;
608 -
  color: #ffffff;
609 -
  border: 1px solid #555;
610 -
  padding: 0.25rem 0.5rem;
611 -
  cursor: pointer;
612 -
  font-family: "Commit Mono", monospace;
613 -
  font-size: 12px;
614 -
  margin-right: 0.5rem;
615 -
}
219 +
/* Footnotes */
616 220
617 -
.post-excerpt {
221 +
.markdown-body .footnote-definition {
618 222
  font-size: 12px;
619 -
  opacity: 0.6;
620 -
  line-height: 1.4;
621 -
}
622 -
623 -
.post-item-enhanced .post-item-info {
624 -
  gap: 0.25rem;
625 -
}
626 -
627 -
.nav-links-input {
628 -
  min-height: 40px;
629 -
  height: 40px;
630 -
}
631 -
632 -
.switch-row {
223 +
  opacity: 0.7;
224 +
  margin-bottom: 0.5rem;
633 225
  display: flex;
634 -
  align-items: center;
635 226
  gap: 0.5rem;
636 227
}
637 228
638 -
.switch-label {
639 -
  font-size: 14px;
640 -
}
641 -
642 -
.switch {
643 -
  position: relative;
644 -
  display: inline-block;
645 -
  width: 36px;
646 -
  height: 20px;
229 +
.markdown-body .footnote-definition-label {
230 +
  font-size: 11px;
231 +
  opacity: 0.5;
647 232
  flex-shrink: 0;
648 233
}
649 234
650 -
.switch input {
651 -
  opacity: 0;
652 -
  width: 0;
653 -
  height: 0;
235 +
.markdown-body .footnote-definition p {
236 +
  margin-bottom: 0;
654 237
}
655 238
656 -
.switch-slider {
657 -
  position: absolute;
658 -
  cursor: pointer;
659 -
  top: 0;
660 -
  left: 0;
661 -
  right: 0;
662 -
  bottom: 0;
663 -
  background: #333;
664 -
  border-radius: 20px;
665 -
  transition: background 0.2s;
666 -
}
667 -
668 -
.switch-slider::before {
669 -
  content: "";
670 -
  position: absolute;
671 -
  height: 14px;
672 -
  width: 14px;
673 -
  left: 3px;
674 -
  bottom: 3px;
675 -
  background: #888;
676 -
  border-radius: 50%;
677 -
  transition: transform 0.2s, background 0.2s;
678 -
}
679 -
680 -
.switch input:checked + .switch-slider {
681 -
  background: #555;
239 +
.markdown-body sup.footnote-reference a {
240 +
  font-size: 11px;
241 +
  text-decoration: none;
242 +
  opacity: 0.6;
682 243
}
683 244
684 -
.switch input:checked + .switch-slider::before {
685 -
  transform: translateX(16px);
686 -
  background: #ffffff;
245 +
.markdown-body sup.footnote-reference a:hover {
246 +
  opacity: 1;
687 247
}
688 248
689 249
/* File thumbnails */
696 256
  flex-shrink: 0;
697 257
}
698 258
699 -
/* Footer */
700 -
701 -
.footer {
702 -
  width: 100%;
703 -
  border-top: 1px solid #333;
704 -
  padding-top: 1rem;
705 -
  margin-top: auto;
706 -
  display: flex;
707 -
  justify-content: center;
708 -
}
259 +
/* RSS link in footer */
709 260
710 261
.rss-link {
711 262
  display: flex;
718 269
.rss-link:hover {
719 270
  opacity: 0.8;
720 271
}
721 -
722 -
/* Export buttons */
723 -
724 -
a.btn {
725 -
  display: inline-block;
726 -
  background: #121113;
727 -
  color: #ffffff;
728 -
  border: 1px solid white;
729 -
  padding: 0.4rem 0.75rem;
730 -
  font-size: 14px;
731 -
  font-family: "Commit Mono", monospace;
732 -
  text-decoration: none;
733 -
  cursor: pointer;
734 -
}
735 -
736 -
a.btn:hover {
737 -
  opacity: 0.7;
738 -
}
739 -
740 -
/* Footnotes */
741 -
742 -
.markdown-body .footnote-definition {
743 -
  font-size: 12px;
744 -
  opacity: 0.7;
745 -
  margin-bottom: 0.5rem;
746 -
  display: flex;
747 -
  gap: 0.5rem;
748 -
}
749 -
750 -
.markdown-body .footnote-definition-label {
751 -
  font-size: 11px;
752 -
  opacity: 0.5;
753 -
  flex-shrink: 0;
754 -
}
755 -
756 -
.markdown-body .footnote-definition p {
757 -
  margin-bottom: 0;
758 -
}
759 -
760 -
.markdown-body sup.footnote-reference a {
761 -
  font-size: 11px;
762 -
  text-decoration: none;
763 -
  opacity: 0.6;
764 -
}
765 -
766 -
.markdown-body sup.footnote-reference a:hover {
767 -
  opacity: 1;
768 -
}
769 -
770 -
.hidden {
771 -
  display: none;
772 -
}
apps/posts/templates/admin_base.html +1 −0
6 6
  <title>{% block title %}Admin{% endblock %}</title>
7 7
  <link rel="icon" href="/static/favicons/favicon.ico">
8 8
  <meta name="theme-color" content="#121113" />
9 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
9 10
  <link rel="stylesheet" href="/static/styles.css">
10 11
</head>
11 12
<body>
apps/posts/templates/base.html +1 −0
16 16
  {% endif %}
17 17
  <meta name="theme-color" content="#121113" />
18 18
  {% block meta %}{% endblock %}
19 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
19 20
  <link rel="stylesheet" href="/static/styles.css">
20 21
  <link rel="stylesheet" href="/custom-styles.css">
21 22
</head>
apps/posts/templates/login.html +1 −0
6 6
  <title>Login</title>
7 7
  <link rel="icon" href="/static/favicons/favicon.ico">
8 8
  <meta name="theme-color" content="#121113" />
9 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
9 10
  <link rel="stylesheet" href="/static/styles.css">
10 11
</head>
11 12
<body>
apps/shrink/Cargo.toml +1 −0
14 14
tower-http = { workspace = true, features = ["fs"] }
15 15
tracing = { workspace = true }
16 16
tracing-subscriber = { workspace = true }
17 +
andromeda-darkmatter-css = { workspace = true }
17 18
askama = "0.15"
18 19
image = "0.25"
19 20
img-parts = "0.3"
apps/shrink/src/server.rs +2 −1
20 20
        .route("/", get(get_index))
21 21
        .route("/compress", post(post_compress))
22 22
        .layer(DefaultBodyLimit::max(20 * 1024 * 1024))
23 -
        .nest_service("/static", ServeDir::new("static"));
23 +
        .nest_service("/static", ServeDir::new("static"))
24 +
        .merge(andromeda_darkmatter_css::router::<()>());
24 25
25 26
    let addr = format!("{}:{}", host, port);
26 27
    tracing::info!("Listening on {}", addr);
apps/shrink/static/styles.css +14 −125
1 -
@font-face {
2 -
    font-family: "Commit Mono";
3 -
    src: url("/static/fonts/CommitMono-400-Regular.otf") format("opentype");
4 -
    font-weight: 400;
5 -
    font-style: normal;
6 -
}
7 -
8 -
@font-face {
9 -
    font-family: "Commit Mono";
10 -
    src: url("/static/fonts/CommitMono-700-Regular.otf") format("opentype");
11 -
    font-weight: 700;
12 -
    font-style: normal;
13 -
}
14 -
15 -
*, *::before, *::after {
16 -
    margin: 0;
17 -
    padding: 0;
18 -
    box-sizing: border-box;
19 -
}
20 -
21 -
html {
22 -
    background: #121113;
23 -
    color: #ffffff;
24 -
    font-family: "Commit Mono", monospace, sans-serif;
25 -
    font-size: 14px;
26 -
    line-height: 1.6;
27 -
    -webkit-font-smoothing: antialiased;
28 -
}
29 -
30 -
body {
31 -
    max-width: 700px;
32 -
    margin: 0 auto;
33 -
    padding: 0 1rem;
34 -
    display: flex;
35 -
    flex-direction: column;
36 -
    gap: 1.5rem;
37 -
    min-height: 100vh;
38 -
}
39 -
40 -
::-webkit-scrollbar {
41 -
    display: none;
42 -
}
43 -
44 -
header {
45 -
    margin-top: 2rem;
46 -
    padding-bottom: 1rem;
47 -
    border-bottom: 1px solid #333;
48 -
}
49 -
50 -
.logo {
51 -
    font-size: 28px;
52 -
    font-weight: 700;
53 -
    text-transform: uppercase;
54 -
    color: #ffffff;
55 -
    text-decoration: none;
56 -
    letter-spacing: 0.05em;
57 -
}
58 -
59 -
main {
60 -
    display: flex;
61 -
    flex-direction: column;
62 -
    gap: 1.5rem;
63 -
}
1 +
/* shrink — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
64 4
65 5
/* Drop Zone */
66 6
#drop-zone {
141 81
    text-align: right;
142 82
}
143 83
144 -
#quality-note {
145 -
    font-size: 12px;
146 -
    opacity: 0.5;
147 -
    text-transform: uppercase;
148 -
    min-height: 1.2em;
149 -
}
150 -
151 84
/* Number Inputs */
152 85
input[type="number"] {
153 -
    background: #121113;
154 -
    color: #ffffff;
155 -
    border: 1px solid rgba(255, 255, 255, 0.5);
156 -
    border-radius: 0;
157 -
    padding: 0.4rem 0.75rem;
158 -
    font-family: "Commit Mono", monospace, sans-serif;
159 -
    font-size: 14px;
160 86
    width: 90px;
161 -
    outline: none;
162 87
    -moz-appearance: textfield;
163 88
}
164 89
165 90
input[type="number"]::-webkit-inner-spin-button,
166 91
input[type="number"]::-webkit-outer-spin-button {
167 92
    -webkit-appearance: none;
168 -
}
169 -
170 -
input[type="number"]:hover,
171 -
input[type="number"]:focus {
172 -
    border-color: #ffffff;
173 93
}
174 94
175 95
.dimension-sep {
216 136
    opacity: 0.3;
217 137
}
218 138
219 -
/* Select */
220 -
select {
221 -
    background: #121113;
222 -
    color: #ffffff;
223 -
    border: 1px solid rgba(255, 255, 255, 0.5);
224 -
    border-radius: 0;
225 -
    padding: 0.4rem 0.75rem;
226 -
    font-family: "Commit Mono", monospace, sans-serif;
227 -
    font-size: 14px;
228 -
    cursor: pointer;
229 -
    outline: none;
230 -
}
231 -
232 -
select:hover,
233 -
select:focus {
234 -
    border-color: #ffffff;
235 -
}
236 -
237 -
/* Buttons */
238 -
button {
239 -
    background: #121113;
240 -
    color: #ffffff;
241 -
    border: 1px solid #ffffff;
242 -
    border-radius: 0;
243 -
    padding: 0.6rem 1.5rem;
244 -
    font-family: "Commit Mono", monospace, sans-serif;
245 -
    font-size: 14px;
246 -
    text-transform: uppercase;
247 -
    letter-spacing: 0.05em;
248 -
    cursor: pointer;
249 -
    transition: opacity 0.15s;
250 -
}
251 -
252 -
button:hover {
253 -
    opacity: 0.7;
254 -
}
255 -
256 -
button:disabled {
257 -
    opacity: 0.3;
258 -
    cursor: default;
259 -
}
260 -
261 139
/* Results */
262 140
#result-section {
263 141
    display: none;
292 170
    text-decoration: none;
293 171
    align-self: flex-start;
294 172
}
173 +
174 +
button {
175 +
    text-transform: uppercase;
176 +
    letter-spacing: 0.05em;
177 +
    padding: 0.6rem 1.5rem;
178 +
}
179 +
180 +
button:disabled {
181 +
    opacity: 0.3;
182 +
    cursor: default;
183 +
}
apps/shrink/templates/base.html +3 −2
14 14
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
15 15
    <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
16 16
    <link rel="manifest" href="/static/site.webmanifest">
17 +
    <link rel="stylesheet" href="/assets/darkmatter.css">
17 18
    <link rel="stylesheet" href="/static/styles.css">
18 19
</head>
19 20
<body>
20 -
    <header>
21 +
    <div class="header">
21 22
        <a href="/" class="logo">SHRINK</a>
22 -
    </header>
23 +
    </div>
23 24
    <main>
24 25
        {% block content %}{% endblock %}
25 26
    </main>
apps/sipp/Cargo.toml +1 −0
30 30
rusqlite = { workspace = true }
31 31
andromeda-db = { workspace = true, features = ["session"] }
32 32
andromeda-auth = { workspace = true }
33 +
andromeda-darkmatter-css = { workspace = true }
33 34
askama = "0.15.4"
34 35
askama_web = { version = "0.15.1", features = ["axum-0.8"] }
35 36
ratatui = "0.30"
apps/sipp/src/server.rs +1 −0
508 508
        .route("/snippets", post(create_snippet))
509 509
        .merge(api_routes)
510 510
        .route("/static/{*path}", get(serve_static))
511 +
        .merge(andromeda_darkmatter_css::router::<AppState>())
511 512
        .with_state(state);
512 513
513 514
    let addr = format!("{}:{}", host, port);
apps/sipp/static/fonts/CommitMono-400-Italic.otf (deleted) +0 −0

Binary file — no preview.

apps/sipp/static/fonts/CommitMono-400-Regular.otf (deleted) +0 −0

Binary file — no preview.

apps/sipp/static/fonts/CommitMono-700-Italic.otf (deleted) +0 −0

Binary file — no preview.

apps/sipp/static/fonts/CommitMono-700-Regular.otf (deleted) +0 −0

Binary file — no preview.

apps/sipp/static/styles.css +49 −247
1 -
* {
2 -
	padding: 0;
3 -
	margin: 0;
4 -
	box-sizing: border-box;
5 -
	font-family: "Commit Mono", monospace, sans-serif;
6 -
	scrollbar-width: none;
7 -
	-ms-overflow-style: none;
8 -
}
9 -
10 -
html {
11 -
	background: #121113;
12 -
	color: #ffffff;
13 -
	font-size: 14px;
14 -
	line-height: 1.6;
15 -
}
16 -
17 -
a {
18 -
	color: #ffffff;
19 -
	text-decoration: none;
20 -
}
21 -
22 -
a:hover {
23 -
	opacity: 0.7;
24 -
}
25 -
26 -
html::-webkit-scrollbar {
27 -
	display: none;
28 -
}
29 -
30 -
body {
31 -
	display: flex;
32 -
	flex-direction: column;
33 -
	justify-content: start;
34 -
	align-items: start;
35 -
	gap: 1.5rem;
36 -
	min-height: 100vh;
37 -
	max-width: 700px;
38 -
	margin: auto;
39 -
	padding: 0 1rem 4rem;
40 -
}
1 +
/* sipp — app-specific styles.
2 +
 * Shared reset / tokens / components come from /assets/darkmatter.css.
3 +
 */
41 4
42 -
.header {
43 -
	display: flex;
44 -
	flex-direction: column;
45 -
	gap: 0.5rem;
46 -
	margin-top: 2rem;
47 -
}
5 +
/* Logo wraps an h1 in sipp markup. */
48 6
49 -
.logo {
50 -
	text-decoration: none !important;
7 +
.logo h1 {
8 +
  font-size: 28px;
9 +
  font-weight: 700;
10 +
  text-transform: uppercase;
51 11
}
52 12
53 -
.links {
54 -
	display: flex;
55 -
	align-items: center;
56 -
	gap: 0.75rem;
57 -
	font-size: 12px;
58 -
	text-decoration: none;
59 -
}
60 -
61 -
.links .inline-form {
62 -
	display: inline;
63 -
	margin: 0;
64 -
	padding: 0;
65 -
}
66 -
67 -
.links .link-button {
68 -
	font-size: 12px;
69 -
}
13 +
/* Snippet icon links */
70 14
71 15
.icon {
72 -
	display: flex;
73 -
	align-items: center;
74 -
	justify-content: center;
75 -
	color: #878787;
76 -
	width: 24px;
77 -
	height: 24px;
16 +
  display: flex;
17 +
  align-items: center;
18 +
  justify-content: center;
19 +
  color: #878787;
20 +
  width: 24px;
21 +
  height: 24px;
78 22
}
79 23
80 24
.icon svg {
81 -
	width: 24px;
82 -
	height: 24px;
25 +
  width: 24px;
26 +
  height: 24px;
83 27
}
84 28
85 29
.icon svg path {
86 -
	fill: #878787;
30 +
  fill: #878787;
87 31
}
88 32
89 33
.icon:hover svg path {
90 -
	fill: white;
34 +
  fill: #ffffff;
91 35
}
36 +
37 +
/* Create-snippet form */
92 38
93 39
#snippetForm {
94 -
	display: flex;
95 -
	flex-direction: column;
96 -
	gap: 1rem;
97 -
	width: 100%;
40 +
  display: flex;
41 +
  flex-direction: column;
42 +
  gap: 1rem;
43 +
  width: 100%;
98 44
}
99 45
100 -
input, textarea, select {
101 -
	background: #121113;
102 -
	color: #ffffff;
103 -
	border: 1px solid white;
104 -
	padding: 0.4rem 0.75rem;
105 -
	font-size: 16px;
106 -
	width: 100%;
107 -
	border-radius: 0;
46 +
#snippetName {
47 +
  font-size: 14px;
48 +
  opacity: 0.7;
108 49
}
109 50
110 -
textarea {
111 -
	min-height: 400px;
112 -
	resize: vertical;
113 -
}
51 +
/* Highlighted snippet viewer */
114 52
115 53
.code-container {
116 -
	border: 1px solid white;
117 -
	height: 400px;
118 -
	overflow: auto;
54 +
  border: 1px solid #ffffff;
55 +
  height: 400px;
56 +
  overflow: auto;
57 +
  -webkit-overflow-scrolling: touch;
119 58
}
120 59
121 60
.code-container pre {
122 -
	background-color: #121113 !important;
123 -
	padding: 6px;
124 -
	margin: 0;
125 -
	min-height: 100%;
126 -
	font-size: 13px;
127 -
	line-height: 1.4;
128 -
}
129 -
130 -
button, .btn {
131 -
	background: #121113;
132 -
	color: #ffffff;
133 -
	padding: 0.4rem 0.75rem;
134 -
	border: 1px solid white;
135 -
	cursor: pointer;
136 -
	width: fit-content;
137 -
	font-size: 14px;
138 -
	border-radius: 0;
139 -
	text-decoration: none;
140 -
	display: inline-block;
141 -
}
142 -
143 -
button:hover, .btn:hover {
144 -
	opacity: 0.7;
145 -
}
146 -
147 -
@media (max-width: 480px) {
148 -
	body {
149 -
		padding: 1rem;
150 -
		gap: 1rem;
151 -
	}
152 -
}
153 -
154 -
.empty {
155 -
	opacity: 0.5;
156 -
	font-size: 12px;
157 -
}
158 -
159 -
.admin-list {
160 -
	display: flex;
161 -
	flex-direction: column;
162 -
	width: 100%;
163 -
}
164 -
165 -
.admin-list-item {
166 -
	display: flex;
167 -
	justify-content: space-between;
168 -
	align-items: center;
169 -
	padding: 8px 0;
170 -
	border-bottom: 1px solid #333;
171 -
	gap: 1rem;
172 -
}
173 -
174 -
.admin-list-info {
175 -
	display: flex;
176 -
	flex-direction: column;
177 -
	gap: 0.2rem;
178 -
	min-width: 0;
179 -
}
180 -
181 -
.admin-list-title {
182 -
	font-size: 15px;
183 -
	white-space: nowrap;
184 -
	overflow: hidden;
185 -
	text-overflow: ellipsis;
186 -
}
187 -
188 -
.admin-list-meta {
189 -
	display: flex;
190 -
	gap: 0.75rem;
191 -
	align-items: center;
192 -
}
193 -
194 -
.admin-list-date {
195 -
	font-size: 11px;
196 -
	opacity: 0.4;
197 -
}
198 -
199 -
.admin-list-actions {
200 -
	display: flex;
201 -
	gap: 1rem;
202 -
	font-size: 12px;
203 -
	flex-shrink: 0;
204 -
}
205 -
206 -
.inline-form {
207 -
	display: inline;
208 -
	margin: 0;
209 -
	padding: 0;
210 -
}
211 -
212 -
.link-button {
213 -
	background: none;
214 -
	border: none;
215 -
	color: #ffffff;
216 -
	cursor: pointer;
217 -
	font-size: 12px;
218 -
	padding: 0;
219 -
	font-family: inherit;
220 -
}
221 -
222 -
.link-button:hover {
223 -
	opacity: 0.7;
224 -
}
225 -
226 -
.link-button.danger {
227 -
	opacity: 0.5;
228 -
}
229 -
230 -
.link-button.danger:hover {
231 -
	opacity: 0.3;
232 -
}
233 -
234 -
@media (max-width: 480px) {
235 -
	.admin-list-item {
236 -
		flex-direction: column;
237 -
		align-items: flex-start;
238 -
		gap: 0.5rem;
239 -
	}
240 -
}
241 -
242 -
main {
243 -
	width: 100%;
244 -
	display: flex;
245 -
	flex-direction: column;
246 -
	gap: 1rem;
247 -
}
248 -
249 -
.form {
250 -
	display: flex;
251 -
	flex-direction: column;
252 -
	gap: 0.5rem;
253 -
	width: 100%;
61 +
  background-color: #121113 !important;
62 +
  padding: 6px;
63 +
  margin: 0;
64 +
  min-height: 100%;
65 +
  font-size: 13px;
66 +
  line-height: 1.4;
67 +
  border: none;
254 68
}
255 69
256 -
label {
257 -
	font-size: 12px;
258 -
	opacity: 0.7;
259 -
}
70 +
/* Viewer action row */
260 71
261 -
.error {
262 -
	color: #ffffff;
263 -
	border-left: 2px solid #ffffff;
264 -
	padding-left: 0.5rem;
265 -
	font-size: 13px;
266 -
	opacity: 0.8;
72 +
.button-group {
73 +
  display: flex;
74 +
  gap: 0.5rem;
75 +
  flex-wrap: wrap;
267 76
}
268 77
269 -
@font-face {
270 -
	font-family: "Commit Mono";
271 -
	src: url("/static/fonts/CommitMono-400-Regular.otf") format("opentype");
272 -
	font-weight: 400;
273 -
	font-style: normal;
274 -
}
78 +
/* Snippet page header variant (plain <a class="header">) */
275 79
276 -
@font-face {
277 -
	font-family: "Commit Mono";
278 -
	src: url("/static/fonts/CommitMono-700-Regular.otf") format("opentype");
279 -
	font-weight: 700;
280 -
	font-style: normal;
80 +
.nav {
81 +
  width: 100%;
82 +
  margin-top: 2rem;
281 83
}
apps/sipp/templates/admin.html +1 −0
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 6
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
7 8
    <link rel="stylesheet" href="/static/styles.css" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
9 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
apps/sipp/templates/index.html +1 −0
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 6
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
7 8
    <link rel="stylesheet" href="/static/styles.css" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
9 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
apps/sipp/templates/login.html +1 −0
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 6
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
7 8
    <link rel="stylesheet" href="/static/styles.css" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
9 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
apps/sipp/templates/snippet.html +1 −0
3 3
  <head>
4 4
    <meta charset="UTF-8" />
5 5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <link rel="stylesheet" href="/assets/darkmatter.css" />
6 7
    <link rel="stylesheet" href="/static/styles.css" />
7 8
    <meta name="theme-color" content="#121113" />
8 9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
crates/darkmatter-css/Cargo.toml (added) +8 −0
1 +
[package]
2 +
name = "andromeda-darkmatter-css"
3 +
version = "0.1.0"
4 +
edition = "2024"
5 +
6 +
[dependencies]
7 +
axum = { workspace = true }
8 +
rust-embed = { workspace = true }
crates/darkmatter-css/assets/darkmatter.css (added) +623 −0
1 +
/* Darkmatter — canonical CSS for Andromeda apps.
2 +
 * Source of truth for reset, tokens, and shared components.
3 +
 * Docs: /darkmatter
4 +
 */
5 +
6 +
@font-face {
7 +
  font-family: "Commit Mono";
8 +
  src: url("/assets/fonts/CommitMono-400-Regular.otf") format("opentype");
9 +
  font-weight: 400;
10 +
  font-style: normal;
11 +
  font-display: swap;
12 +
}
13 +
14 +
@font-face {
15 +
  font-family: "Commit Mono";
16 +
  src: url("/assets/fonts/CommitMono-700-Regular.otf") format("opentype");
17 +
  font-weight: 700;
18 +
  font-style: normal;
19 +
  font-display: swap;
20 +
}
21 +
22 +
/* ── Reset + webkit hardening ─────────────────────────────────────── */
23 +
24 +
*,
25 +
*::before,
26 +
*::after {
27 +
  padding: 0;
28 +
  margin: 0;
29 +
  box-sizing: border-box;
30 +
  font-family: "Commit Mono", monospace, sans-serif;
31 +
  -webkit-tap-highlight-color: transparent;
32 +
}
33 +
34 +
* {
35 +
  scrollbar-width: none;
36 +
  -ms-overflow-style: none;
37 +
}
38 +
39 +
html {
40 +
  background: #121113;
41 +
  color: #ffffff;
42 +
  font-size: 14px;
43 +
  line-height: 1.6;
44 +
  -webkit-text-size-adjust: 100%;
45 +
  text-size-adjust: 100%;
46 +
}
47 +
48 +
html::-webkit-scrollbar {
49 +
  display: none;
50 +
}
51 +
52 +
body {
53 +
  display: flex;
54 +
  flex-direction: column;
55 +
  justify-content: start;
56 +
  align-items: start;
57 +
  gap: 1.5rem;
58 +
  min-height: 100vh;
59 +
  max-width: 700px;
60 +
  margin: auto;
61 +
  padding: 0 1rem 4rem;
62 +
}
63 +
64 +
@media (max-width: 480px) {
65 +
  body {
66 +
    padding: 1rem;
67 +
    gap: 1rem;
68 +
  }
69 +
}
70 +
71 +
/* ── Links ────────────────────────────────────────────────────────── */
72 +
73 +
a {
74 +
  color: #ffffff;
75 +
  text-decoration: none;
76 +
  touch-action: manipulation;
77 +
}
78 +
79 +
a:hover {
80 +
  opacity: 0.7;
81 +
}
82 +
83 +
/* ── Header / nav ─────────────────────────────────────────────────── */
84 +
85 +
.header {
86 +
  display: flex;
87 +
  flex-direction: column;
88 +
  gap: 0.5rem;
89 +
  width: 100%;
90 +
  margin-top: 2rem;
91 +
  border-bottom: 1px solid #333;
92 +
  padding-bottom: 1rem;
93 +
}
94 +
95 +
.logo {
96 +
  font-size: 28px;
97 +
  font-weight: 700;
98 +
  text-decoration: none;
99 +
  text-transform: uppercase;
100 +
}
101 +
102 +
.links {
103 +
  display: flex;
104 +
  align-items: center;
105 +
  gap: 0.75rem;
106 +
  font-size: 12px;
107 +
}
108 +
109 +
/* ── Main ─────────────────────────────────────────────────────────── */
110 +
111 +
main {
112 +
  width: 100%;
113 +
  display: flex;
114 +
  flex-direction: column;
115 +
  gap: 1rem;
116 +
}
117 +
118 +
/* ── Forms ────────────────────────────────────────────────────────── */
119 +
120 +
.form {
121 +
  display: flex;
122 +
  flex-direction: column;
123 +
  gap: 0.5rem;
124 +
  width: 100%;
125 +
}
126 +
127 +
.form-row {
128 +
  display: flex;
129 +
  gap: 0.5rem;
130 +
  width: 100%;
131 +
}
132 +
133 +
.form-row .form-field {
134 +
  flex: 1;
135 +
}
136 +
137 +
.form-field {
138 +
  display: flex;
139 +
  flex-direction: column;
140 +
  gap: 0.25rem;
141 +
}
142 +
143 +
.form-actions {
144 +
  display: flex;
145 +
  gap: 0.5rem;
146 +
}
147 +
148 +
@media (max-width: 480px) {
149 +
  .form-row {
150 +
    flex-direction: column;
151 +
  }
152 +
}
153 +
154 +
label {
155 +
  font-size: 12px;
156 +
  opacity: 0.7;
157 +
}
158 +
159 +
input,
160 +
textarea,
161 +
select {
162 +
  background: #121113;
163 +
  color: #ffffff;
164 +
  border: 1px solid #ffffff;
165 +
  padding: 0.4rem 0.75rem;
166 +
  font-size: 16px; /* 16px prevents iOS focus zoom */
167 +
  width: 100%;
168 +
  border-radius: 0;
169 +
  -webkit-appearance: none;
170 +
  appearance: none;
171 +
  outline: none;
172 +
}
173 +
174 +
input:focus,
175 +
textarea:focus,
176 +
select:focus {
177 +
  outline: none;
178 +
}
179 +
180 +
textarea {
181 +
  min-height: 400px;
182 +
  resize: vertical;
183 +
}
184 +
185 +
select {
186 +
  background-image: none;
187 +
  padding-right: 0.75rem;
188 +
}
189 +
190 +
input[type="file"] {
191 +
  cursor: pointer;
192 +
  font-size: 14px;
193 +
}
194 +
195 +
input[type="file"]::-webkit-file-upload-button,
196 +
input[type="file"]::file-selector-button {
197 +
  background: #121113;
198 +
  color: #ffffff;
199 +
  border: 1px solid #555;
200 +
  padding: 0.25rem 0.5rem;
201 +
  cursor: pointer;
202 +
  font-family: "Commit Mono", monospace;
203 +
  font-size: 12px;
204 +
  margin-right: 0.5rem;
205 +
  border-radius: 0;
206 +
}
207 +
208 +
input[type="search"]::-webkit-search-decoration,
209 +
input[type="search"]::-webkit-search-cancel-button,
210 +
input[type="search"]::-webkit-search-results-button,
211 +
input[type="search"]::-webkit-search-results-decoration {
212 +
  -webkit-appearance: none;
213 +
}
214 +
215 +
input[type="checkbox"],
216 +
input[type="radio"] {
217 +
  -webkit-appearance: none;
218 +
  appearance: none;
219 +
  width: 16px;
220 +
  height: 16px;
221 +
  background: transparent;
222 +
  border: 1px solid #ffffff;
223 +
  border-radius: 0;
224 +
  padding: 0;
225 +
  cursor: pointer;
226 +
  position: relative;
227 +
  flex-shrink: 0;
228 +
  touch-action: manipulation;
229 +
}
230 +
231 +
input[type="radio"] {
232 +
  border-radius: 50%;
233 +
}
234 +
235 +
input[type="checkbox"]:checked::after {
236 +
  content: '✔︎';
237 +
  position: absolute;
238 +
  top: 50%;
239 +
  left: 50%;
240 +
  transform: translate(-50%, -50%);
241 +
  font-size: 12px;
242 +
  color: #ffffff;
243 +
  line-height: 1;
244 +
}
245 +
246 +
input[type="radio"]:checked::after {
247 +
  content: '';
248 +
  position: absolute;
249 +
  top: 50%;
250 +
  left: 50%;
251 +
  width: 8px;
252 +
  height: 8px;
253 +
  border-radius: 50%;
254 +
  background: #ffffff;
255 +
  transform: translate(-50%, -50%);
256 +
}
257 +
258 +
.checkbox-field {
259 +
  justify-content: flex-end;
260 +
}
261 +
262 +
.checkbox-field label {
263 +
  display: flex;
264 +
  align-items: center;
265 +
  gap: 0.5rem;
266 +
  font-size: 14px;
267 +
  opacity: 1;
268 +
  cursor: pointer;
269 +
}
270 +
271 +
/* Switch */
272 +
273 +
.switch-row {
274 +
  display: flex;
275 +
  align-items: center;
276 +
  gap: 0.5rem;
277 +
}
278 +
279 +
.switch-label {
280 +
  font-size: 14px;
281 +
}
282 +
283 +
.switch {
284 +
  position: relative;
285 +
  display: inline-block;
286 +
  width: 36px;
287 +
  height: 20px;
288 +
  flex-shrink: 0;
289 +
}
290 +
291 +
.switch input {
292 +
  opacity: 0;
293 +
  width: 0;
294 +
  height: 0;
295 +
}
296 +
297 +
.switch-slider {
298 +
  position: absolute;
299 +
  cursor: pointer;
300 +
  top: 0;
301 +
  left: 0;
302 +
  right: 0;
303 +
  bottom: 0;
304 +
  background: #333;
305 +
  border-radius: 20px;
306 +
  transition: background 0.2s;
307 +
}
308 +
309 +
.switch-slider::before {
310 +
  content: "";
311 +
  position: absolute;
312 +
  height: 14px;
313 +
  width: 14px;
314 +
  left: 3px;
315 +
  bottom: 3px;
316 +
  background: #888;
317 +
  border-radius: 50%;
318 +
  transition: transform 0.2s, background 0.2s;
319 +
}
320 +
321 +
.switch input:checked + .switch-slider {
322 +
  background: #555;
323 +
}
324 +
325 +
.switch input:checked + .switch-slider::before {
326 +
  transform: translateX(16px);
327 +
  background: #ffffff;
328 +
}
329 +
330 +
/* ── Buttons ──────────────────────────────────────────────────────── */
331 +
332 +
button,
333 +
.btn {
334 +
  background: #121113;
335 +
  color: #ffffff;
336 +
  padding: 0.2rem 0.75rem;
337 +
  border: 1px solid #ffffff;
338 +
  cursor: pointer;
339 +
  width: fit-content;
340 +
  font-size: 14px;
341 +
  line-height: 1.4;
342 +
  border-radius: 0;
343 +
  -webkit-appearance: none;
344 +
  appearance: none;
345 +
  text-decoration: none;
346 +
  display: inline-block;
347 +
  touch-action: manipulation;
348 +
}
349 +
350 +
button:hover,
351 +
.btn:hover {
352 +
  opacity: 0.7;
353 +
}
354 +
355 +
button.loading {
356 +
  cursor: wait;
357 +
}
358 +
359 +
.link-button {
360 +
  background: none;
361 +
  border: none;
362 +
  color: #ffffff;
363 +
  cursor: pointer;
364 +
  font-size: 12px;
365 +
  padding: 0;
366 +
  font-family: inherit;
367 +
  -webkit-appearance: none;
368 +
  appearance: none;
369 +
}
370 +
371 +
.link-button:hover {
372 +
  opacity: 0.7;
373 +
}
374 +
375 +
.link-button.danger {
376 +
  opacity: 0.5;
377 +
}
378 +
379 +
.link-button.danger:hover {
380 +
  opacity: 0.3;
381 +
}
382 +
383 +
.inline-form {
384 +
  display: inline;
385 +
  margin: 0;
386 +
  padding: 0;
387 +
}
388 +
389 +
/* ── Feedback ─────────────────────────────────────────────────────── */
390 +
391 +
.error {
392 +
  color: #ffffff;
393 +
  border-left: 2px solid #ffffff;
394 +
  padding-left: 0.5rem;
395 +
  font-size: 13px;
396 +
  opacity: 0.8;
397 +
}
398 +
399 +
.success {
400 +
  color: #ffffff;
401 +
  border-left: 2px solid #555;
402 +
  padding-left: 0.5rem;
403 +
  font-size: 13px;
404 +
  opacity: 0.7;
405 +
}
406 +
407 +
.empty {
408 +
  opacity: 0.5;
409 +
  font-size: 12px;
410 +
}
411 +
412 +
/* ── Item list (generic stacked list pattern) ────────────────────── */
413 +
414 +
.item-list {
415 +
  display: flex;
416 +
  flex-direction: column;
417 +
  width: 100%;
418 +
}
419 +
420 +
.item {
421 +
  display: flex;
422 +
  flex-direction: column;
423 +
  gap: 0.25rem;
424 +
  padding: 0.75rem 0;
425 +
  border-bottom: 1px solid #333;
426 +
}
427 +
428 +
.item:hover {
429 +
  opacity: 0.7;
430 +
}
431 +
432 +
.item-title {
433 +
  font-size: 16px;
434 +
}
435 +
436 +
.item-meta {
437 +
  font-size: 12px;
438 +
  opacity: 0.5;
439 +
}
440 +
441 +
/* ── Admin list (horizontal row w/ actions) ──────────────────────── */
442 +
443 +
.admin-list {
444 +
  display: flex;
445 +
  flex-direction: column;
446 +
  width: 100%;
447 +
}
448 +
449 +
.admin-list-item {
450 +
  display: flex;
451 +
  justify-content: space-between;
452 +
  align-items: center;
453 +
  padding: 8px 0;
454 +
  border-bottom: 1px solid #333;
455 +
  gap: 1rem;
456 +
}
457 +
458 +
.admin-list-info {
459 +
  display: flex;
460 +
  flex-direction: column;
461 +
  gap: 0.2rem;
462 +
  min-width: 0;
463 +
}
464 +
465 +
.admin-list-title {
466 +
  font-size: 15px;
467 +
  white-space: nowrap;
468 +
  overflow: hidden;
469 +
  text-overflow: ellipsis;
470 +
}
471 +
472 +
.admin-list-meta {
473 +
  display: flex;
474 +
  gap: 0.75rem;
475 +
  align-items: center;
476 +
}
477 +
478 +
.admin-list-date {
479 +
  font-size: 11px;
480 +
  opacity: 0.4;
481 +
}
482 +
483 +
.admin-list-actions {
484 +
  display: flex;
485 +
  gap: 1rem;
486 +
  font-size: 12px;
487 +
  flex-shrink: 0;
488 +
}
489 +
490 +
.admin-toolbar {
491 +
  display: flex;
492 +
  justify-content: space-between;
493 +
  align-items: center;
494 +
  width: 100%;
495 +
}
496 +
497 +
.admin-toolbar h2 {
498 +
  font-size: 18px;
499 +
  font-weight: 700;
500 +
}
501 +
502 +
@media (max-width: 480px) {
503 +
  .admin-list-item {
504 +
    flex-direction: column;
505 +
    align-items: flex-start;
506 +
    gap: 0.5rem;
507 +
  }
508 +
}
509 +
510 +
/* ── Tags / badges ───────────────────────────────────────────────── */
511 +
512 +
.tag {
513 +
  font-size: 11px;
514 +
  opacity: 0.5;
515 +
  background: #1e1c1f;
516 +
  padding: 1px 6px;
517 +
  border: 1px solid #333;
518 +
}
519 +
520 +
.status-badge {
521 +
  font-size: 11px;
522 +
  padding: 1px 6px;
523 +
  border: 1px solid #333;
524 +
}
525 +
526 +
.status-published {
527 +
  opacity: 1;
528 +
  border-color: #555;
529 +
}
530 +
531 +
.status-draft {
532 +
  opacity: 0.4;
533 +
}
534 +
535 +
/* ── Tables ──────────────────────────────────────────────────────── */
536 +
537 +
table {
538 +
  width: 100%;
539 +
  border-collapse: collapse;
540 +
}
541 +
542 +
th {
543 +
  opacity: 0.5;
544 +
  font-weight: 400;
545 +
  font-size: 12px;
546 +
  text-transform: uppercase;
547 +
  text-align: left;
548 +
  padding: 6px;
549 +
  border-bottom: 1px solid #333;
550 +
}
551 +
552 +
td {
553 +
  padding: 6px;
554 +
  border-bottom: 1px solid #333;
555 +
}
556 +
557 +
/* ── Spinner (braille) ────────────────────────────────────────────── */
558 +
559 +
.spinner {
560 +
  margin-left: 0.6rem;
561 +
}
562 +
563 +
.spinner::after {
564 +
  content: "⠋";
565 +
  display: inline-block;
566 +
  animation: braille-spin 0.8s steps(10) infinite;
567 +
}
568 +
569 +
@keyframes braille-spin {
570 +
  0%   { content: "⠋"; }
571 +
  10%  { content: "⠙"; }
572 +
  20%  { content: "⠹"; }
573 +
  30%  { content: "⠸"; }
574 +
  40%  { content: "⠼"; }
575 +
  50%  { content: "⠴"; }
576 +
  60%  { content: "⠦"; }
577 +
  70%  { content: "⠧"; }
578 +
  80%  { content: "⠇"; }
579 +
  90%  { content: "⠏"; }
580 +
}
581 +
582 +
/* ── Inline code ─────────────────────────────────────────────────── */
583 +
584 +
code {
585 +
  background: #1e1c1f;
586 +
  padding: 2px 4px;
587 +
  font-size: 13px;
588 +
}
589 +
590 +
pre {
591 +
  background: #1e1c1f;
592 +
  padding: 12px;
593 +
  overflow-x: auto;
594 +
  border: 1px solid #333;
595 +
  -webkit-overflow-scrolling: touch;
596 +
}
597 +
598 +
pre code {
599 +
  background: none;
600 +
  padding: 0;
601 +
}
602 +
603 +
/* ── Footer ──────────────────────────────────────────────────────── */
604 +
605 +
.footer {
606 +
  width: 100%;
607 +
  border-top: 1px solid #333;
608 +
  padding-top: 1rem;
609 +
  margin-top: auto;
610 +
  display: flex;
611 +
  justify-content: center;
612 +
}
613 +
614 +
/* ── Utility ─────────────────────────────────────────────────────── */
615 +
616 +
.hidden {
617 +
  display: none;
618 +
}
619 +
620 +
.scroll-x {
621 +
  overflow-x: auto;
622 +
  -webkit-overflow-scrolling: touch;
623 +
}
crates/darkmatter-css/assets/index.html (added) +249 −0
1 +
<!DOCTYPE html>
2 +
<html lang="en">
3 +
<head>
4 +
  <meta charset="UTF-8">
5 +
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 +
  <meta name="theme-color" content="#121113">
7 +
  <title>Darkmatter — Gallery</title>
8 +
  <link rel="stylesheet" href="/assets/darkmatter.css">
9 +
  <style>
10 +
    .section { width: 100%; border-bottom: 1px solid #333; padding-bottom: 2rem; }
11 +
    .section h2 { font-size: 16px; margin-bottom: 0.5rem; }
12 +
    .section p.desc { font-size: 12px; opacity: 0.5; margin-bottom: 0.75rem; }
13 +
    .row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-bottom: 1rem; }
14 +
    .swatch { width: 40px; height: 24px; border: 1px solid #333; }
15 +
    .opacity-scale { display: flex; flex-direction: column; gap: 0.25rem; }
16 +
  </style>
17 +
</head>
18 +
<body>
19 +
  <header class="header">
20 +
    <a href="/darkmatter" class="logo">DARKMATTER</a>
21 +
    <nav class="links">
22 +
      <a href="#reset">reset</a>
23 +
      <a href="#typography">typography</a>
24 +
      <a href="#buttons">buttons</a>
25 +
      <a href="#forms">forms</a>
26 +
      <a href="#feedback">feedback</a>
27 +
      <a href="#lists">lists</a>
28 +
      <a href="#tags">tags</a>
29 +
      <a href="#table">table</a>
30 +
      <a href="#code">code</a>
31 +
    </nav>
32 +
  </header>
33 +
34 +
  <main>
35 +
    <section class="section" id="reset">
36 +
      <h2>Palette</h2>
37 +
      <p class="desc">Background <code>#121113</code>, foreground <code>#ffffff</code>, grays <code>#1e1c1f</code> / <code>#333</code> / <code>#555</code>. No accent colors.</p>
38 +
      <div class="row">
39 +
        <div class="swatch" style="background:#121113"></div>
40 +
        <div class="swatch" style="background:#1e1c1f"></div>
41 +
        <div class="swatch" style="background:#333"></div>
42 +
        <div class="swatch" style="background:#555"></div>
43 +
        <div class="swatch" style="background:#ffffff"></div>
44 +
      </div>
45 +
    </section>
46 +
47 +
    <section class="section" id="typography">
48 +
      <h2>Typography</h2>
49 +
      <p class="desc">Commit Mono. Hierarchy by opacity, not gray hex.</p>
50 +
      <div class="opacity-scale">
51 +
        <div>Primary text — opacity 1.0</div>
52 +
        <div style="opacity: 0.7">Secondary text — opacity 0.7 (labels, blockquotes)</div>
53 +
        <div style="opacity: 0.5">Tertiary text — opacity 0.5 (metadata, dates, table headers)</div>
54 +
        <div style="opacity: 0.3">Muted text — opacity 0.3 (null, placeholder)</div>
55 +
      </div>
56 +
      <div class="row" style="margin-top: 0.75rem">
57 +
        <span style="font-size: 28px; font-weight: 700; text-transform: uppercase">LOGO 28</span>
58 +
        <span style="font-size: 18px">h1 18</span>
59 +
        <span style="font-size: 16px">h2 / title 16</span>
60 +
        <span style="font-size: 14px">body 14</span>
61 +
        <span style="font-size: 12px">meta 12</span>
62 +
        <span style="font-size: 11px">tag 11</span>
63 +
      </div>
64 +
    </section>
65 +
66 +
    <section class="section" id="buttons">
67 +
      <h2>Buttons &amp; links</h2>
68 +
      <p class="desc"><code>button</code>, <code>.btn</code>, <code>.link-button</code>, <code>.link-button.danger</code>, <code>.spinner</code>.</p>
69 +
      <div class="row">
70 +
        <button>Submit</button>
71 +
        <a href="#" class="btn">Link as button</a>
72 +
        <button class="loading">Saving<span class="spinner"></span></button>
73 +
        <button class="link-button">edit</button>
74 +
        <button class="link-button danger">delete</button>
75 +
        <a href="#">plain link</a>
76 +
      </div>
77 +
    </section>
78 +
79 +
    <section class="section" id="forms">
80 +
      <h2>Form controls</h2>
81 +
      <p class="desc">Inputs use 16px font-size to prevent iOS focus zoom. All <code>-webkit-appearance: none</code> to kill default iOS chrome.</p>
82 +
      <form class="form" onsubmit="event.preventDefault()">
83 +
        <div class="form-field">
84 +
          <label for="demo-text">Text input</label>
85 +
          <input id="demo-text" type="text" placeholder="placeholder">
86 +
        </div>
87 +
        <div class="form-field">
88 +
          <label for="demo-search">Search</label>
89 +
          <input id="demo-search" type="search" placeholder="search...">
90 +
        </div>
91 +
        <div class="form-row">
92 +
          <div class="form-field">
93 +
            <label for="demo-a">Split field A</label>
94 +
            <input id="demo-a" type="text">
95 +
          </div>
96 +
          <div class="form-field">
97 +
            <label for="demo-b">Split field B</label>
98 +
            <input id="demo-b" type="text">
99 +
          </div>
100 +
        </div>
101 +
        <div class="form-field">
102 +
          <label for="demo-select">Select</label>
103 +
          <select id="demo-select">
104 +
            <option>one</option>
105 +
            <option>two</option>
106 +
          </select>
107 +
        </div>
108 +
        <div class="form-field">
109 +
          <label for="demo-textarea">Textarea</label>
110 +
          <textarea id="demo-textarea" style="min-height: 120px">multiline input</textarea>
111 +
        </div>
112 +
        <div class="form-field">
113 +
          <label for="demo-file">File upload</label>
114 +
          <input id="demo-file" type="file">
115 +
        </div>
116 +
        <div class="form-field checkbox-field">
117 +
          <label>
118 +
            <input type="checkbox" checked>
119 +
            Checkbox (checked)
120 +
          </label>
121 +
          <label>
122 +
            <input type="checkbox">
123 +
            Checkbox (unchecked)
124 +
          </label>
125 +
        </div>
126 +
        <div class="form-field">
127 +
          <label>
128 +
            <input type="radio" name="demo-radio" checked> Option A
129 +
          </label>
130 +
          <label>
131 +
            <input type="radio" name="demo-radio"> Option B
132 +
          </label>
133 +
        </div>
134 +
        <div class="switch-row">
135 +
          <label class="switch">
136 +
            <input type="checkbox" checked>
137 +
            <span class="switch-slider"></span>
138 +
          </label>
139 +
          <span class="switch-label">Switch toggle</span>
140 +
        </div>
141 +
        <div class="form-actions">
142 +
          <button type="submit">Save</button>
143 +
          <button type="button" class="link-button danger">Cancel</button>
144 +
        </div>
145 +
      </form>
146 +
    </section>
147 +
148 +
    <section class="section" id="feedback">
149 +
      <h2>Feedback</h2>
150 +
      <p class="desc"><code>.error</code>, <code>.success</code>, <code>.empty</code>. No red/green — borders + opacity only.</p>
151 +
      <div class="error">Something went wrong.</div>
152 +
      <div class="success" style="margin-top: 0.5rem">Saved successfully.</div>
153 +
      <div class="empty" style="margin-top: 0.5rem">No items yet.</div>
154 +
    </section>
155 +
156 +
    <section class="section" id="lists">
157 +
      <h2>Item list</h2>
158 +
      <p class="desc"><code>.item-list</code> / <code>.item</code> — stacked with <code>#333</code> bottom divider.</p>
159 +
      <div class="item-list">
160 +
        <a href="#" class="item">
161 +
          <div class="item-title">First entry title</div>
162 +
          <div class="item-meta">apr 18, 2026</div>
163 +
        </a>
164 +
        <a href="#" class="item">
165 +
          <div class="item-title">Second entry title</div>
166 +
          <div class="item-meta">apr 17, 2026</div>
167 +
        </a>
168 +
      </div>
169 +
170 +
      <h2 style="margin-top: 1rem">Admin list</h2>
171 +
      <p class="desc"><code>.admin-list</code> — horizontal row with inline actions.</p>
172 +
      <div class="admin-list">
173 +
        <div class="admin-list-item">
174 +
          <div class="admin-list-info">
175 +
            <div class="admin-list-title">example-item-one</div>
176 +
            <div class="admin-list-meta">
177 +
              <span class="status-badge status-published">published</span>
178 +
              <span class="admin-list-date">2026-04-18</span>
179 +
            </div>
180 +
          </div>
181 +
          <div class="admin-list-actions">
182 +
            <a href="#">edit</a>
183 +
            <button class="link-button danger">delete</button>
184 +
          </div>
185 +
        </div>
186 +
        <div class="admin-list-item">
187 +
          <div class="admin-list-info">
188 +
            <div class="admin-list-title">example-item-two</div>
189 +
            <div class="admin-list-meta">
190 +
              <span class="status-badge status-draft">draft</span>
191 +
              <span class="admin-list-date">2026-04-17</span>
192 +
            </div>
193 +
          </div>
194 +
          <div class="admin-list-actions">
195 +
            <a href="#">edit</a>
196 +
            <button class="link-button danger">delete</button>
197 +
          </div>
198 +
        </div>
199 +
      </div>
200 +
    </section>
201 +
202 +
    <section class="section" id="tags">
203 +
      <h2>Tags</h2>
204 +
      <div class="row">
205 +
        <span class="tag">rust</span>
206 +
        <span class="tag">web</span>
207 +
        <span class="tag">css</span>
208 +
      </div>
209 +
    </section>
210 +
211 +
    <section class="section" id="table">
212 +
      <h2>Table</h2>
213 +
      <table>
214 +
        <thead>
215 +
          <tr>
216 +
            <th>name</th>
217 +
            <th>status</th>
218 +
            <th>date</th>
219 +
          </tr>
220 +
        </thead>
221 +
        <tbody>
222 +
          <tr>
223 +
            <td>first</td>
224 +
            <td>ok</td>
225 +
            <td style="opacity: 0.5">2026-04-18</td>
226 +
          </tr>
227 +
          <tr>
228 +
            <td>second</td>
229 +
            <td>ok</td>
230 +
            <td style="opacity: 0.5">2026-04-17</td>
231 +
          </tr>
232 +
        </tbody>
233 +
      </table>
234 +
    </section>
235 +
236 +
    <section class="section" id="code">
237 +
      <h2>Code</h2>
238 +
      <p>Inline <code>let x = 42;</code> and block:</p>
239 +
      <pre><code>fn main() {
240 +
    println!("hello, darkmatter");
241 +
}</code></pre>
242 +
    </section>
243 +
  </main>
244 +
245 +
  <footer class="footer">
246 +
    <span style="opacity: 0.5; font-size: 12px">darkmatter-css · andromeda workspace</span>
247 +
  </footer>
248 +
</body>
249 +
</html>
crates/darkmatter-css/src/lib.rs (added) +54 −0
1 +
use axum::{
2 +
    extract::Path,
3 +
    http::{HeaderValue, StatusCode, header},
4 +
    response::{IntoResponse, Response},
5 +
    routing::get,
6 +
    Router,
7 +
};
8 +
use rust_embed::Embed;
9 +
10 +
#[derive(Embed)]
11 +
#[folder = "assets/"]
12 +
pub struct Assets;
13 +
14 +
pub fn router<S>() -> Router<S>
15 +
where
16 +
    S: Clone + Send + Sync + 'static,
17 +
{
18 +
    Router::new()
19 +
        .route("/assets/darkmatter.css", get(css))
20 +
        .route("/assets/fonts/{file}", get(font))
21 +
        .route("/darkmatter", get(gallery))
22 +
        .route("/darkmatter/", get(gallery))
23 +
}
24 +
25 +
async fn css() -> Response {
26 +
    serve("darkmatter.css", "text/css; charset=utf-8")
27 +
}
28 +
29 +
async fn gallery() -> Response {
30 +
    serve("index.html", "text/html; charset=utf-8")
31 +
}
32 +
33 +
async fn font(Path(file): Path<String>) -> Response {
34 +
    let mime = match file.rsplit('.').next().unwrap_or("") {
35 +
        "otf" => "font/otf",
36 +
        "ttf" => "font/ttf",
37 +
        "woff" => "font/woff",
38 +
        "woff2" => "font/woff2",
39 +
        _ => "application/octet-stream",
40 +
    };
41 +
    serve(&format!("fonts/{file}"), mime)
42 +
}
43 +
44 +
fn serve(path: &str, mime: &'static str) -> Response {
45 +
    match Assets::get(path) {
46 +
        Some(f) => (
47 +
            StatusCode::OK,
48 +
            [(header::CONTENT_TYPE, HeaderValue::from_static(mime))],
49 +
            f.data.to_vec(),
50 +
        )
51 +
            .into_response(),
52 +
        None => StatusCode::NOT_FOUND.into_response(),
53 +
    }
54 +
}