@commonpub/layer 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/components/ContentCard.vue +1 -1
  2. package/components/ImageUpload.vue +1 -1
  3. package/components/ShareToHubModal.vue +1 -1
  4. package/components/blocks/BlockCodeView.vue +26 -25
  5. package/components/contest/ContestEntries.vue +112 -0
  6. package/components/contest/ContestHero.vue +204 -0
  7. package/components/contest/ContestJudges.vue +51 -0
  8. package/components/contest/ContestPrizes.vue +82 -0
  9. package/components/contest/ContestRules.vue +34 -0
  10. package/components/contest/ContestSidebar.vue +83 -0
  11. package/components/editors/BlogEditor.vue +1 -1
  12. package/components/editors/DocsPageTree.vue +10 -0
  13. package/components/hub/HubHero.vue +1 -1
  14. package/composables/useSanitize.ts +112 -9
  15. package/composables/useTheme.ts +8 -0
  16. package/layouts/default.vue +7 -7
  17. package/middleware/feature-gate.global.ts +24 -0
  18. package/package.json +6 -6
  19. package/pages/[type]/index.vue +4 -3
  20. package/pages/admin/audit.vue +3 -2
  21. package/pages/admin/federation.vue +33 -13
  22. package/pages/admin/index.vue +7 -1
  23. package/pages/admin/reports.vue +152 -36
  24. package/pages/admin/settings.vue +17 -5
  25. package/pages/admin/theme.vue +5 -3
  26. package/pages/auth/forgot-password.vue +35 -35
  27. package/pages/auth/login.vue +6 -5
  28. package/pages/auth/reset-password.vue +44 -32
  29. package/pages/contests/[slug]/edit.vue +238 -56
  30. package/pages/contests/[slug]/index.vue +54 -450
  31. package/pages/contests/[slug]/judge.vue +141 -53
  32. package/pages/contests/[slug]/results.vue +182 -0
  33. package/pages/contests/create.vue +64 -64
  34. package/pages/contests/index.vue +2 -1
  35. package/pages/docs/[siteSlug]/[...pagePath].vue +6 -5
  36. package/pages/docs/[siteSlug]/edit.vue +58 -2
  37. package/pages/docs/[siteSlug]/index.vue +6 -5
  38. package/pages/federated-hubs/[id]/posts/[postId].vue +2 -2
  39. package/pages/hubs/index.vue +3 -2
  40. package/pages/index.vue +25 -7
  41. package/pages/learn/index.vue +1 -1
  42. package/pages/mirror/[id].vue +3 -3
  43. package/pages/notifications.vue +15 -1
  44. package/pages/products/[slug].vue +5 -2
  45. package/pages/settings/notifications.vue +7 -1
  46. package/pages/tags/[slug].vue +3 -2
  47. package/pages/tags/index.vue +3 -2
  48. package/pages/videos/[id].vue +18 -0
  49. package/server/api/admin/content/[id].patch.ts +1 -1
  50. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
  51. package/server/api/admin/federation/refederate.post.ts +7 -3
  52. package/server/api/admin/federation/repair-types.post.ts +2 -45
  53. package/server/api/admin/federation/retry.post.ts +7 -4
  54. package/server/api/admin/reports.get.ts +1 -0
  55. package/server/api/auth/federated/login.post.ts +22 -2
  56. package/server/api/auth/sign-in-username.post.ts +42 -0
  57. package/server/api/content/[id]/products-sync.post.ts +7 -6
  58. package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
  59. package/server/api/contests/[slug]/entries.get.ts +6 -1
  60. package/server/api/contests/[slug]/judge.post.ts +8 -2
  61. package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
  62. package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
  63. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
  64. package/server/api/docs/migrate-content.post.ts +1 -7
  65. package/server/api/federation/hub-follow-status.get.ts +2 -18
  66. package/server/api/federation/hub-follow.post.ts +9 -27
  67. package/server/api/federation/hub-post-like.post.ts +9 -98
  68. package/server/api/federation/hub-post-likes.get.ts +3 -13
  69. package/server/api/notifications/read.post.ts +6 -1
  70. package/server/api/profile/theme.put.ts +23 -0
  71. package/server/api/search/index.get.ts +2 -2
  72. package/server/api/search/trending.get.ts +3 -3
  73. package/server/api/users/index.get.ts +9 -2
  74. package/server/middleware/content-ap.ts +2 -2
  75. package/server/routes/.well-known/webfinger.ts +2 -2
  76. package/theme/base.css +23 -0
  77. package/components/EditorPropertiesPanel.vue +0 -393
  78. package/components/views/BlogView.vue +0 -735
  79. package/server/api/resolve-identity.post.ts +0 -34
@@ -1,735 +0,0 @@
1
- <script setup lang="ts">
2
- import type { ContentViewData } from '../../composables/useEngagement';
3
-
4
- const props = defineProps<{
5
- content: ContentViewData;
6
- federatedId?: string;
7
- }>();
8
-
9
- const contentId = computed(() => props.content?.id);
10
- const contentType = computed(() => props.content?.type ?? 'blog');
11
- const fedId = computed(() => props.federatedId);
12
- const { liked, bookmarked, likeCount, isFederated, toggleLike, toggleBookmark, share, fetchInitialState } = useEngagement({ contentId, contentType, federatedContentId: fedId });
13
-
14
- onMounted(() => {
15
- fetchInitialState(props.content?.likeCount ?? 0);
16
- });
17
-
18
- const authorUrl = computed(() =>
19
- isFederated.value && props.content.author?.profileUrl
20
- ? props.content.author.profileUrl
21
- : `/u/${props.content.author?.username}`,
22
- );
23
-
24
- const config = useRuntimeConfig();
25
- useJsonLd({
26
- type: 'article',
27
- title: props.content.title,
28
- description: props.content.seoDescription ?? props.content.description ?? '',
29
- url: `${config.public.siteUrl}/u/${props.content.author?.username}/${props.content.type}/${props.content.slug}`,
30
- imageUrl: props.content.coverImageUrl ?? undefined,
31
- authorName: props.content.author?.displayName ?? props.content.author?.username ?? '',
32
- authorUrl: `${config.public.siteUrl}/u/${props.content.author?.username}`,
33
- publishedAt: props.content.publishedAt ?? props.content.createdAt,
34
- updatedAt: props.content.updatedAt,
35
- });
36
-
37
- // Series data — only available when content has series metadata
38
- const seriesPart = computed(() => props.content?.seriesPart as number | undefined);
39
- const seriesTitle = computed(() => props.content?.seriesTitle as string | undefined);
40
- const seriesTotalParts = computed(() => (props.content?.seriesTotalParts as number) || 0);
41
- const hasSeries = computed(() => !!seriesTitle.value && seriesTotalParts.value > 0);
42
- </script>
43
-
44
- <template>
45
- <div class="cpub-blog-view">
46
-
47
- <!-- HERO BANNER -->
48
- <div v-if="content.bannerUrl || content.author?.bannerUrl" class="cpub-cover">
49
- <img :src="(content.bannerUrl || content.author?.bannerUrl)!" :alt="content.title" class="cpub-cover-img" />
50
- </div>
51
-
52
- <div class="cpub-blog-wrap">
53
-
54
- <!-- TYPE BADGE -->
55
- <div class="cpub-content-type-badge"><i class="fa-solid fa-pen-nib"></i> Blog Post</div>
56
-
57
- <!-- TITLE -->
58
- <h1 class="cpub-blog-title">{{ content.title }}</h1>
59
-
60
- <!-- AUTHOR ROW -->
61
- <div class="cpub-author-row">
62
- <NuxtLink v-if="content.author" :to="authorUrl" :external="isFederated" :target="isFederated ? '_blank' : undefined" style="text-decoration:none;">
63
- <img v-if="content.author?.avatarUrl" :src="content.author.avatarUrl" :alt="content.author?.displayName ?? content.author?.username ?? ''" class="cpub-av cpub-av-lg" style="object-fit:cover;border:2px solid var(--border);" />
64
- <div v-else class="cpub-av cpub-av-lg">{{ content.author?.displayName?.slice(0, 2).toUpperCase() || 'CP' }}</div>
65
- </NuxtLink>
66
- <div class="cpub-author-info">
67
- <NuxtLink v-if="content.author" :to="authorUrl" :external="isFederated" :target="isFederated ? '_blank' : undefined" class="cpub-author-name">
68
- {{ content.author.displayName || content.author.username }}
69
- </NuxtLink>
70
- <div class="cpub-author-meta">
71
- <span v-if="content.author?.username">@{{ content.author.username }}</span>
72
- <span class="cpub-sep">·</span>
73
- <span>{{ new Date(content.publishedAt || content.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) }}</span>
74
- <span class="cpub-sep">·</span>
75
- <span><i class="fa-regular fa-clock"></i> {{ content.readTime || '5 min read' }}</span>
76
- <template v-if="hasSeries">
77
- <span class="cpub-sep">·</span>
78
- <span class="cpub-tag cpub-tag-pink">{{ seriesTitle }} · Part {{ seriesPart || 1 }} of {{ seriesTotalParts }}</span>
79
- </template>
80
- </div>
81
- </div>
82
- </div>
83
-
84
- <!-- ENGAGEMENT ROW -->
85
- <div class="cpub-engagement-row">
86
- <div class="cpub-eng-stat"><i class="fa-regular fa-eye"></i> {{ content.viewCount?.toLocaleString() || '0' }} views</div>
87
- <div class="cpub-eng-sep"></div>
88
- <button class="cpub-eng-btn" :class="{ liked }" :aria-label="liked ? 'Unlike' : 'Like'" :aria-pressed="liked" @click="toggleLike">
89
- <i class="fa-solid fa-heart"></i> {{ likeCount }}
90
- </button>
91
- <button class="cpub-eng-btn" :class="{ bookmarked }" :aria-label="bookmarked ? 'Remove bookmark' : 'Bookmark'" :aria-pressed="bookmarked" @click="toggleBookmark">
92
- <i class="fa-solid fa-bookmark"></i> Bookmark
93
- </button>
94
- <div class="cpub-eng-spacer"></div>
95
- <button class="cpub-eng-btn" aria-label="Share" @click="share"><i class="fa-solid fa-share-nodes"></i> Share</button>
96
- <button class="cpub-eng-btn" aria-label="More options"><i class="fa-solid fa-ellipsis"></i></button>
97
- </div>
98
-
99
- <!-- COVER PHOTO (in-body) -->
100
- <div v-if="content.coverImageUrl" class="cpub-cover-photo">
101
- <img :src="content.coverImageUrl" :alt="content.title" class="cpub-cover-photo-img" />
102
- </div>
103
-
104
- <!-- BLOG BODY (PROSE) -->
105
- <div class="cpub-prose">
106
- <template v-if="content.content && Array.isArray(content.content) && (content.content as unknown[]).length > 0">
107
- <BlocksBlockContentRenderer :blocks="(content.content as [string, Record<string, unknown>][])" />
108
- </template>
109
- <template v-else>
110
- <p>No content body yet.</p>
111
- </template>
112
- </div>
113
-
114
- <!-- SERIES NAVIGATION -->
115
- <div v-if="hasSeries" class="cpub-series-nav">
116
- <div class="cpub-series-header">
117
- <div class="cpub-series-icon"><i class="fa-solid fa-layer-group"></i></div>
118
- <div>
119
- <div class="cpub-series-label">Series</div>
120
- <div class="cpub-series-title">{{ seriesTitle }}</div>
121
- </div>
122
- <div style="margin-left:auto;">
123
- <span class="cpub-tag cpub-tag-pink">Part {{ seriesPart || 1 }} of {{ seriesTotalParts }}</span>
124
- </div>
125
- </div>
126
- <div class="cpub-series-progress">
127
- <div class="cpub-series-progress-label">
128
- <span>Progress</span>
129
- <span>{{ seriesPart || 1 }} / {{ seriesTotalParts }} published</span>
130
- </div>
131
- <div class="cpub-series-progress-track">
132
- <div class="cpub-series-progress-fill" :style="{ width: ((seriesPart || 1) / seriesTotalParts * 100) + '%' }"></div>
133
- </div>
134
- </div>
135
- <div class="cpub-series-nav-btns">
136
- <NuxtLink v-if="content.seriesPrev" :to="content.seriesPrev.url || '#'" class="cpub-series-nav-btn cpub-prev">
137
- <div class="cpub-series-nav-dir"><i class="fa-solid fa-chevron-left"></i> Previous</div>
138
- <div class="cpub-series-nav-ep">Part {{ (seriesPart || 2) - 1 }}</div>
139
- <div class="cpub-series-nav-post-title">{{ content.seriesPrev.title }}</div>
140
- </NuxtLink>
141
- <div v-else class="cpub-series-nav-btn cpub-prev cpub-disabled">
142
- <div class="cpub-series-nav-dir"><i class="fa-solid fa-chevron-left"></i> Previous</div>
143
- <div class="cpub-series-nav-ep">—</div>
144
- </div>
145
- <NuxtLink v-if="content.seriesNext" :to="content.seriesNext.url || '#'" class="cpub-series-nav-btn cpub-next">
146
- <div class="cpub-series-nav-dir">Next <i class="fa-solid fa-chevron-right"></i></div>
147
- <div class="cpub-series-nav-ep">Part {{ (seriesPart || 1) + 1 }}</div>
148
- <div class="cpub-series-nav-post-title">{{ content.seriesNext.title }}</div>
149
- </NuxtLink>
150
- <div v-else class="cpub-series-nav-btn cpub-next cpub-disabled">
151
- <div class="cpub-series-nav-dir">Next <i class="fa-solid fa-chevron-right"></i></div>
152
- <div class="cpub-series-nav-ep">Coming soon</div>
153
- </div>
154
- </div>
155
- </div>
156
-
157
- <!-- TAGS -->
158
- <div v-if="content.tags?.length" class="cpub-tags-row">
159
- <div class="cpub-tags-label">Tags</div>
160
- <NuxtLink
161
- v-for="(tag, i) in content.tags"
162
- :key="tag.id || tag.name || i"
163
- :to="`/tags/${tag.slug || (tag.name || String(tag)).toLowerCase().replace(/\s+/g, '-')}`"
164
- class="cpub-tag"
165
- :class="{ 'cpub-tag-pink': i === 0 }"
166
- >
167
- {{ tag.name || tag }}
168
- </NuxtLink>
169
- </div>
170
-
171
- <!-- AUTHOR CARD -->
172
- <div v-if="content.author" class="cpub-author-card">
173
- <img v-if="content.author.avatarUrl" :src="content.author.avatarUrl" :alt="content.author.displayName ?? content.author.username ?? ''" class="cpub-av cpub-av-xl" style="object-fit:cover;border:2px solid var(--border);" />
174
- <div v-else class="cpub-av cpub-av-xl">{{ content.author.displayName?.slice(0, 2).toUpperCase() || 'CP' }}</div>
175
- <div class="cpub-author-card-info">
176
- <div class="cpub-author-card-label">Posted by</div>
177
- <div class="cpub-author-card-name">{{ content.author.displayName || content.author.username }}</div>
178
- <div class="cpub-author-card-handle">@{{ content.author.username }}</div>
179
- <div v-if="content.author.bio" class="cpub-author-card-bio">{{ content.author.bio }}</div>
180
- <div class="cpub-author-card-bottom">
181
- <span class="cpub-author-card-stat"><strong>{{ content.author.postCount ?? 0 }}</strong> posts</span>
182
- <span class="cpub-author-card-stat"><strong>{{ content.author.followerCount ?? 0 }}</strong> followers</span>
183
- <div class="cpub-author-card-actions">
184
- <button class="cpub-btn cpub-btn-sm"><i class="fa-solid fa-rss"></i> Follow</button>
185
- </div>
186
- </div>
187
- </div>
188
- </div>
189
-
190
- <!-- ATTACHMENTS -->
191
- <ContentAttachments v-if="content.attachments?.length" :attachments="content.attachments" />
192
-
193
- <!-- COMMENTS -->
194
- <CommentSection :target-type="content.type" :target-id="content.id" :federated-content-id="federatedId" />
195
-
196
- </div>
197
- </div>
198
- </template>
199
-
200
- <style scoped>
201
- /* ── COVER IMAGE ── */
202
- .cpub-cover {
203
- width: 100%;
204
- max-height: 420px;
205
- overflow: hidden;
206
- border-bottom: var(--border-width-default) solid var(--border);
207
- background: var(--surface);
208
- }
209
-
210
- .cpub-cover-img {
211
- width: 100%;
212
- height: 100%;
213
- max-height: 420px;
214
- object-fit: cover;
215
- display: block;
216
- }
217
-
218
- /* ── BLOG WRAP ── */
219
- .cpub-blog-view {
220
- overflow-x: clip;
221
- }
222
-
223
- .cpub-blog-wrap {
224
- max-width: 720px;
225
- margin: 0 auto;
226
- padding: 40px clamp(12px, 4vw, 24px) 80px;
227
- overflow-wrap: break-word;
228
- word-break: break-word;
229
- }
230
-
231
- /* Global overflow protection for content */
232
- .cpub-blog-wrap :deep(img),
233
- .cpub-blog-wrap :deep(video),
234
- .cpub-blog-wrap :deep(iframe) { max-width: 100%; height: auto; }
235
- .cpub-blog-wrap :deep(pre) { overflow-x: auto; -webkit-overflow-scrolling: touch; }
236
-
237
- /* ── TYPE BADGE ── */
238
- .cpub-content-type-badge {
239
- display: inline-flex;
240
- align-items: center;
241
- gap: 5px;
242
- font-size: 10px;
243
- font-family: var(--font-mono);
244
- letter-spacing: 0.1em;
245
- text-transform: uppercase;
246
- color: var(--green);
247
- border: var(--border-width-default) solid var(--border);
248
- background: var(--green-bg);
249
- padding: 3px 10px;
250
- margin-bottom: 16px;
251
- box-shadow: var(--shadow-sm);
252
- }
253
-
254
- /* ── TITLE ── */
255
- .cpub-blog-title {
256
- font-size: 26px;
257
- font-weight: 700;
258
- line-height: 1.25;
259
- color: var(--text);
260
- margin-bottom: 20px;
261
- letter-spacing: -0.01em;
262
- }
263
-
264
- /* ── AVATARS ── */
265
- .cpub-av {
266
- width: 28px;
267
- height: 28px;
268
- border-radius: 50%;
269
- background: var(--surface3);
270
- border: var(--border-width-default) solid var(--border);
271
- display: flex;
272
- align-items: center;
273
- justify-content: center;
274
- font-size: 10px;
275
- font-weight: 600;
276
- color: var(--text-dim);
277
- font-family: var(--font-mono);
278
- flex-shrink: 0;
279
- }
280
-
281
- .cpub-av-lg { width: 44px; height: 44px; font-size: 14px; }
282
- .cpub-av-xl { width: 64px; height: 64px; font-size: 18px; }
283
-
284
- /* ── AUTHOR ROW ── */
285
- .cpub-author-row {
286
- display: flex;
287
- align-items: center;
288
- gap: 12px;
289
- margin-bottom: 20px;
290
- flex-wrap: wrap;
291
- }
292
-
293
- .cpub-author-info {
294
- display: flex;
295
- flex-direction: column;
296
- gap: 2px;
297
- }
298
-
299
- .cpub-author-name {
300
- font-size: 13px;
301
- font-weight: 600;
302
- color: var(--text);
303
- text-decoration: none;
304
- }
305
-
306
- .cpub-author-name:hover { color: var(--accent); }
307
-
308
- .cpub-author-meta {
309
- font-size: 11px;
310
- color: var(--text-faint);
311
- font-family: var(--font-mono);
312
- display: flex;
313
- align-items: center;
314
- gap: 8px;
315
- flex-wrap: wrap;
316
- }
317
-
318
- .cpub-sep { color: var(--border2); }
319
-
320
- /* ── TAGS ── */
321
- .cpub-tag {
322
- display: inline-flex;
323
- align-items: center;
324
- font-size: 10px;
325
- font-family: var(--font-mono);
326
- padding: 2px 8px;
327
- border: var(--border-width-default) solid var(--border2);
328
- color: var(--text-dim);
329
- background: var(--surface2);
330
- text-decoration: none;
331
- }
332
- .cpub-tag:hover { color: var(--accent); border-color: var(--accent-border); }
333
-
334
- .cpub-tag-pink {
335
- border-color: var(--pink-border);
336
- color: var(--pink);
337
- background: var(--pink-bg);
338
- }
339
-
340
- .cpub-tag-teal {
341
- border-color: var(--teal-border);
342
- color: var(--teal);
343
- background: var(--teal-bg);
344
- }
345
-
346
- /* ── COVER PHOTO (in-body) ── */
347
- .cpub-cover-photo {
348
- margin-bottom: 24px;
349
- }
350
- .cpub-cover-photo-img {
351
- width: 100%;
352
- height: auto;
353
- display: block;
354
- border-radius: var(--radius, 0);
355
- }
356
-
357
- /* ── ENGAGEMENT ROW ── */
358
- .cpub-engagement-row {
359
- display: flex;
360
- align-items: center;
361
- gap: 6px;
362
- padding: 14px 0;
363
- border-top: var(--border-width-default) solid var(--border);
364
- border-bottom: var(--border-width-default) solid var(--border);
365
- margin-bottom: 36px;
366
- }
367
-
368
- .cpub-eng-stat {
369
- font-size: 11px;
370
- font-family: var(--font-mono);
371
- color: var(--text-faint);
372
- display: flex;
373
- align-items: center;
374
- gap: 5px;
375
- padding: 5px 10px;
376
- }
377
-
378
- .cpub-eng-btn {
379
- display: flex;
380
- align-items: center;
381
- gap: 5px;
382
- font-size: 11px;
383
- font-family: var(--font-mono);
384
- color: var(--text-dim);
385
- background: var(--surface);
386
- border: var(--border-width-default) solid var(--border);
387
- padding: 5px 12px;
388
- cursor: pointer;
389
- transition: background var(--transition-fast), color var(--transition-fast);
390
- }
391
-
392
- .cpub-eng-btn:hover { background: var(--surface2); color: var(--text); }
393
- .cpub-eng-btn.liked { color: var(--red); border-color: var(--red); background: var(--red-bg); }
394
- .cpub-eng-btn.bookmarked { color: var(--yellow); border-color: var(--yellow); background: var(--yellow-bg); }
395
-
396
- .cpub-eng-sep {
397
- width: 2px;
398
- height: 20px;
399
- background: var(--border);
400
- margin: 0 4px;
401
- }
402
-
403
- .cpub-eng-spacer { margin-left: auto; }
404
-
405
- /* ── PROSE ── */
406
- .cpub-prose {
407
- font-size: 15px;
408
- line-height: 1.9;
409
- color: var(--text-dim);
410
- }
411
-
412
- .cpub-prose :deep(h2) {
413
- font-size: 19px;
414
- font-weight: 700;
415
- color: var(--text);
416
- margin: 40px 0 12px;
417
- letter-spacing: -0.01em;
418
- }
419
-
420
- .cpub-prose :deep(h3) {
421
- font-size: 15px;
422
- font-weight: 600;
423
- color: var(--text);
424
- margin: 28px 0 8px;
425
- }
426
-
427
- .cpub-prose :deep(p) { margin-bottom: 18px; }
428
- .cpub-prose :deep(strong) { color: var(--text); font-weight: 600; }
429
- .cpub-prose :deep(em) { font-style: italic; color: var(--text-dim); }
430
- .cpub-prose :deep(a) { color: var(--accent); text-decoration: none; }
431
- .cpub-prose :deep(a:hover) { text-decoration: underline; }
432
-
433
- .cpub-prose :deep(code) {
434
- font-family: var(--font-mono);
435
- font-size: 12.5px;
436
- color: var(--teal);
437
- background: var(--surface2);
438
- border: var(--border-width-default) solid var(--border2);
439
- padding: 1px 6px;
440
- }
441
-
442
- .cpub-prose :deep(pre code) {
443
- background: none;
444
- border: none;
445
- padding: 0;
446
- color: inherit;
447
- font-size: inherit;
448
- }
449
-
450
- .cpub-prose :deep(ul),
451
- .cpub-prose :deep(ol) {
452
- margin: 0 0 18px 20px;
453
- display: flex;
454
- flex-direction: column;
455
- gap: 7px;
456
- }
457
-
458
- .cpub-prose :deep(li) { color: var(--text-dim); }
459
-
460
- .cpub-prose :deep(blockquote) {
461
- border-left: 4px solid var(--border2);
462
- padding: 14px 20px;
463
- margin: 28px 0;
464
- background: var(--surface);
465
- }
466
-
467
- .cpub-prose :deep(blockquote p) {
468
- color: var(--text-dim);
469
- font-style: italic;
470
- margin: 0;
471
- font-size: 15px;
472
- }
473
-
474
- .cpub-prose :deep(blockquote.reflection) {
475
- border-left: 4px solid var(--purple);
476
- background: var(--purple-bg);
477
- }
478
-
479
- .cpub-prose :deep(blockquote.reflection p) {
480
- color: var(--purple);
481
- }
482
-
483
- .cpub-prose :deep(hr) {
484
- border: none;
485
- border-top: var(--border-width-default) solid var(--border);
486
- margin: 36px 0;
487
- }
488
-
489
- /* ── SERIES NAV ── */
490
- .cpub-series-nav {
491
- background: var(--surface);
492
- border: var(--border-width-default) solid var(--border);
493
- padding: 20px;
494
- margin: 40px 0;
495
- box-shadow: var(--shadow-sm);
496
- }
497
-
498
- .cpub-series-header {
499
- display: flex;
500
- align-items: center;
501
- gap: 8px;
502
- margin-bottom: 14px;
503
- }
504
-
505
- .cpub-series-icon {
506
- width: 28px;
507
- height: 28px;
508
- background: var(--pink-bg);
509
- border: var(--border-width-default) solid var(--pink);
510
- display: flex;
511
- align-items: center;
512
- justify-content: center;
513
- font-size: 12px;
514
- color: var(--pink);
515
- flex-shrink: 0;
516
- }
517
-
518
- .cpub-series-label {
519
- font-size: 10px;
520
- font-family: var(--font-mono);
521
- color: var(--text-faint);
522
- letter-spacing: 0.1em;
523
- text-transform: uppercase;
524
- }
525
-
526
- .cpub-series-title {
527
- font-size: 13px;
528
- font-weight: 600;
529
- color: var(--text);
530
- }
531
-
532
- .cpub-series-progress {
533
- margin-bottom: 16px;
534
- }
535
-
536
- .cpub-series-progress-label {
537
- font-size: 11px;
538
- font-family: var(--font-mono);
539
- color: var(--text-faint);
540
- margin-bottom: 6px;
541
- display: flex;
542
- justify-content: space-between;
543
- }
544
-
545
- .cpub-series-progress-track {
546
- height: 4px;
547
- background: var(--surface3);
548
- overflow: hidden;
549
- border: var(--border-width-default) solid var(--border2);
550
- }
551
-
552
- .cpub-series-progress-fill {
553
- height: 100%;
554
- background: var(--pink);
555
- }
556
-
557
- .cpub-series-nav-btns {
558
- display: grid;
559
- grid-template-columns: 1fr 1fr;
560
- gap: 8px;
561
- }
562
-
563
- .cpub-series-nav-btn {
564
- background: var(--surface);
565
- border: var(--border-width-default) solid var(--border);
566
- padding: 12px 14px;
567
- cursor: pointer;
568
- text-decoration: none;
569
- display: flex;
570
- flex-direction: column;
571
- gap: 4px;
572
- color: inherit;
573
- transition: background var(--transition-fast);
574
- }
575
-
576
- .cpub-series-nav-btn:hover { background: var(--surface2); }
577
- .cpub-series-nav-btn.cpub-next { text-align: right; }
578
- .cpub-series-nav-btn.cpub-disabled { opacity: 0.5; pointer-events: none; }
579
-
580
- .cpub-series-nav-dir {
581
- font-size: 10px;
582
- font-family: var(--font-mono);
583
- color: var(--text-faint);
584
- letter-spacing: 0.08em;
585
- text-transform: uppercase;
586
- display: flex;
587
- align-items: center;
588
- gap: 4px;
589
- }
590
-
591
- .cpub-series-nav-btn.cpub-next .cpub-series-nav-dir { justify-content: flex-end; }
592
-
593
- .cpub-series-nav-ep {
594
- font-size: 10px;
595
- font-family: var(--font-mono);
596
- color: var(--pink);
597
- }
598
-
599
- .cpub-series-nav-post-title {
600
- font-size: 12px;
601
- font-weight: 600;
602
- color: var(--text);
603
- line-height: 1.35;
604
- }
605
-
606
- /* ── TAGS ROW ── */
607
- .cpub-tags-row {
608
- display: flex;
609
- flex-wrap: wrap;
610
- gap: 6px;
611
- margin: 36px 0 28px;
612
- padding-top: 20px;
613
- border-top: var(--border-width-default) solid var(--border);
614
- }
615
-
616
- .cpub-tags-label {
617
- font-size: 10px;
618
- font-family: var(--font-mono);
619
- color: var(--text-faint);
620
- letter-spacing: 0.1em;
621
- text-transform: uppercase;
622
- width: 100%;
623
- margin-bottom: 4px;
624
- }
625
-
626
- /* ── AUTHOR CARD ── */
627
- .cpub-author-card {
628
- background: var(--surface);
629
- border: var(--border-width-default) solid var(--border);
630
- padding: 22px;
631
- display: flex;
632
- gap: 18px;
633
- align-items: flex-start;
634
- margin: 28px 0;
635
- box-shadow: var(--shadow-sm);
636
- }
637
-
638
- .cpub-author-card-info { flex: 1; min-width: 0; }
639
-
640
- .cpub-author-card-label {
641
- font-size: 9px;
642
- font-family: var(--font-mono);
643
- color: var(--text-faint);
644
- letter-spacing: 0.12em;
645
- text-transform: uppercase;
646
- margin-bottom: 6px;
647
- }
648
-
649
- .cpub-author-card-name {
650
- font-size: 15px;
651
- font-weight: 700;
652
- color: var(--text);
653
- margin-bottom: 3px;
654
- }
655
-
656
- .cpub-author-card-handle {
657
- font-size: 11px;
658
- font-family: var(--font-mono);
659
- color: var(--text-faint);
660
- margin-bottom: 10px;
661
- }
662
-
663
- .cpub-author-card-bio {
664
- font-size: 13px;
665
- color: var(--text-dim);
666
- line-height: 1.65;
667
- margin-bottom: 12px;
668
- }
669
-
670
- .cpub-author-card-bottom {
671
- display: flex;
672
- align-items: center;
673
- gap: 10px;
674
- flex-wrap: wrap;
675
- }
676
-
677
- .cpub-author-card-stat {
678
- font-size: 11px;
679
- font-family: var(--font-mono);
680
- color: var(--text-faint);
681
- display: flex;
682
- align-items: center;
683
- gap: 4px;
684
- }
685
-
686
- .cpub-author-card-stat strong {
687
- color: var(--text-dim);
688
- font-weight: 600;
689
- }
690
-
691
- .cpub-author-card-actions {
692
- margin-left: auto;
693
- display: flex;
694
- gap: 8px;
695
- }
696
-
697
- /* ── BUTTONS ── */
698
- .cpub-btn {
699
- font-family: var(--font-sans);
700
- font-size: 12px;
701
- padding: 6px 14px;
702
- border: var(--border-width-default) solid var(--border);
703
- background: var(--surface);
704
- color: var(--text);
705
- cursor: pointer;
706
- display: inline-flex;
707
- align-items: center;
708
- gap: 6px;
709
- transition: background var(--transition-fast);
710
- }
711
-
712
- .cpub-btn:hover { background: var(--surface2); }
713
- .cpub-btn-sm { padding: 4px 10px; font-size: 11px; }
714
-
715
- /* ── RESPONSIVE ── */
716
- @media (max-width: 768px) {
717
- .cpub-blog-wrap { padding-top: 24px; padding-bottom: 48px; }
718
- .cpub-blog-title { font-size: 22px; }
719
- .cpub-engagement-row { flex-wrap: wrap; gap: 6px; }
720
- .cpub-engage-btn { padding: 8px 12px; min-height: 36px; }
721
- .cpub-engage-sep { display: none; }
722
- .cpub-series-nav-btns { grid-template-columns: 1fr; }
723
- .cpub-series-nav-btn { padding: 12px; min-height: 44px; }
724
- .cpub-tag-link { padding: 4px 10px; font-size: 11px; min-height: 28px; display: inline-flex; align-items: center; }
725
- }
726
-
727
- @media (max-width: 480px) {
728
- .cpub-blog-wrap { padding-top: 16px; padding-bottom: 40px; }
729
- .cpub-blog-title { font-size: 20px; }
730
- .cpub-blog-lead { font-size: 13px; }
731
- .cpub-author-card { flex-direction: column; gap: 12px; }
732
- .cpub-engage-btn { font-size: 11px; }
733
- .cpub-hero-cover { max-height: 280px; }
734
- }
735
- </style>