@commonpub/layer 0.8.2 → 0.8.4

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 (76) 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/ArticleEditor.vue +19 -1
  12. package/components/editors/BlogEditor.vue +1 -1
  13. package/components/editors/DocsPageTree.vue +10 -0
  14. package/components/hub/HubHero.vue +1 -1
  15. package/composables/useSanitize.ts +112 -9
  16. package/layouts/default.vue +7 -7
  17. package/middleware/feature-gate.global.ts +24 -0
  18. package/package.json +8 -8
  19. package/pages/[type]/index.vue +4 -3
  20. package/pages/admin/audit.vue +3 -2
  21. package/pages/admin/federation.vue +9 -1
  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/settings/notifications.vue +7 -1
  45. package/pages/tags/[slug].vue +3 -2
  46. package/pages/tags/index.vue +3 -2
  47. package/pages/videos/[id].vue +18 -0
  48. package/server/api/admin/content/[id].patch.ts +1 -1
  49. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
  50. package/server/api/admin/federation/refederate.post.ts +7 -3
  51. package/server/api/admin/federation/repair-types.post.ts +2 -45
  52. package/server/api/admin/federation/retry.post.ts +7 -4
  53. package/server/api/admin/reports.get.ts +1 -0
  54. package/server/api/auth/sign-in-username.post.ts +42 -0
  55. package/server/api/content/[id]/products-sync.post.ts +7 -6
  56. package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
  57. package/server/api/contests/[slug]/entries.get.ts +6 -1
  58. package/server/api/contests/[slug]/judge.post.ts +8 -2
  59. package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
  60. package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
  61. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
  62. package/server/api/docs/migrate-content.post.ts +1 -7
  63. package/server/api/federation/hub-follow-status.get.ts +2 -18
  64. package/server/api/federation/hub-follow.post.ts +9 -27
  65. package/server/api/federation/hub-post-like.post.ts +9 -98
  66. package/server/api/federation/hub-post-likes.get.ts +3 -13
  67. package/server/api/notifications/read.post.ts +6 -1
  68. package/server/api/search/index.get.ts +2 -2
  69. package/server/api/search/trending.get.ts +3 -3
  70. package/server/api/users/index.get.ts +9 -2
  71. package/server/middleware/content-ap.ts +2 -2
  72. package/server/routes/.well-known/webfinger.ts +2 -2
  73. package/theme/base.css +23 -0
  74. package/components/EditorPropertiesPanel.vue +0 -393
  75. package/components/views/BlogView.vue +0 -735
  76. package/server/api/resolve-identity.post.ts +0 -34
@@ -1,8 +1,13 @@
1
1
  <script setup lang="ts">
2
+ import type { Serialized, ContestEntryItem } from '@commonpub/server';
3
+
2
4
  const route = useRoute();
3
5
  const slug = route.params.slug as string;
6
+ const toast = useToast();
7
+ const { isAuthenticated, isAdmin, user } = useAuth();
4
8
 
5
9
  const { data: contest } = useLazyFetch(`/api/contests/${slug}`);
10
+ const { data: apiEntriesData, refresh: refreshEntries } = useLazyFetch<{ items: Serialized<ContestEntryItem>[]; total: number }>(`/api/contests/${slug}/entries`);
6
11
 
7
12
  useSeoMeta({
8
13
  title: () => `${contest.value?.title || 'Contest'} — ${useSiteName()}`,
@@ -10,75 +15,16 @@ useSeoMeta({
10
15
  ogImage: '/og-default.png',
11
16
  });
12
17
 
13
- // Fetch entries from API
14
- const { data: apiEntriesData } = useLazyFetch<{ items: any[]; total: number }>(`/api/contests/${slug}/entries`);
15
-
16
18
  const c = computed(() => contest.value);
17
-
18
- // Countdown timer
19
- const countdown = ref({ days: '00', hours: '00', mins: '00', secs: '00' });
20
- let countdownInterval: ReturnType<typeof setInterval> | null = null;
21
-
22
- function pad(n: number): string { return String(n).padStart(2, '0'); }
23
-
24
- function updateCountdown(): void {
25
- const target = c.value?.endDate ? new Date(c.value.endDate) : new Date();
26
- const now = new Date();
27
- let diff = Math.max(0, Math.floor((target.getTime() - now.getTime()) / 1000));
28
- const days = Math.floor(diff / 86400); diff %= 86400;
29
- const hours = Math.floor(diff / 3600); diff %= 3600;
30
- const mins = Math.floor(diff / 60);
31
- const secs = diff % 60;
32
- countdown.value = { days: pad(days), hours: pad(hours), mins: pad(mins), secs: pad(secs) };
33
- }
34
-
35
- onMounted(() => {
36
- updateCountdown();
37
- countdownInterval = setInterval(updateCountdown, 1000);
38
- });
39
-
40
- onUnmounted(() => {
41
- if (countdownInterval) clearInterval(countdownInterval);
42
- });
43
-
44
- // FAQ accordion
45
- const openFaq = ref<number>(0);
46
- function toggleFaq(i: number): void {
47
- openFaq.value = openFaq.value === i ? -1 : i;
48
- }
49
-
50
- // Vote state
51
- const toast = useToast();
52
- const votedEntries = ref<Set<string>>(new Set());
53
- async function toggleVote(entryId: string): Promise<void> {
54
- if (!isAuthenticated.value) {
55
- toast.error('Log in to vote');
56
- return;
57
- }
58
- try {
59
- await $fetch('/api/social/like', {
60
- method: 'POST',
61
- body: { targetId: entryId, targetType: 'contestEntry' },
62
- });
63
- if (votedEntries.value.has(entryId)) votedEntries.value.delete(entryId);
64
- else votedEntries.value.add(entryId);
65
- } catch {
66
- toast.error('Failed to vote');
67
- }
68
- }
69
-
70
- const entries = computed(() => {
71
- return apiEntriesData.value?.items ?? [];
72
- });
73
-
74
- const entryFilter = ref('all');
75
- const filters = ['all', 'newest'];
19
+ const entries = computed(() => apiEntriesData.value?.items ?? []);
20
+ const isOwner = computed(() => isAdmin.value || !!(user.value?.id && c.value?.createdById === user.value.id));
21
+ const isJudge = computed(() => !!(user.value?.id && ((c.value?.judges ?? []) as string[]).includes(user.value.id)));
76
22
 
77
23
  // Admin contest management
78
- const { isAuthenticated, isAdmin } = useAuth();
79
24
  const transitioning = ref(false);
80
25
 
81
26
  async function transitionStatus(newStatus: string): Promise<void> {
27
+ if (newStatus === 'cancelled' && !confirm('Cancel this contest? This cannot be undone.')) return;
82
28
  transitioning.value = true;
83
29
  try {
84
30
  await $fetch(`/api/contests/${slug}/transition`, {
@@ -106,6 +52,7 @@ const { data: userContent } = useFetch('/api/content', {
106
52
  function copyLink(): void {
107
53
  if (typeof window !== 'undefined' && window.navigator?.clipboard) {
108
54
  window.navigator.clipboard.writeText(window.location.href);
55
+ toast.success('Link copied');
109
56
  }
110
57
  }
111
58
 
@@ -127,102 +74,39 @@ async function submitEntry(): Promise<void> {
127
74
  submitting.value = false;
128
75
  }
129
76
  }
77
+
78
+ async function withdrawEntry(entryId: string): Promise<void> {
79
+ try {
80
+ await $fetch(`/api/contests/${slug}/entries/${entryId}`, { method: 'DELETE' });
81
+ toast.success('Entry withdrawn');
82
+ refreshNuxtData();
83
+ } catch {
84
+ toast.error('Failed to withdraw entry');
85
+ }
86
+ }
130
87
  </script>
131
88
 
132
89
  <template>
133
90
  <div class="cpub-contest">
134
-
135
- <!-- HERO -->
136
- <div class="cpub-hero">
137
- <div class="cpub-hero-pattern">
138
- <div class="cpub-hero-dots"></div>
139
- <div class="cpub-hero-lines"></div>
140
- </div>
141
-
142
- <div class="cpub-hero-inner">
143
- <div class="cpub-hero-eyebrow">
144
- <span class="cpub-contest-badge"><i class="fa fa-trophy" style="margin-right:5px;font-size:8px;"></i>Contest</span>
145
- <span class="cpub-hero-host">
146
- Hosted by
147
- <span class="cpub-av cpub-av-sm" style="background:var(--accent-bg);border-color:var(--accent);color:var(--accent);">CP</span>
148
- <strong style="color:var(--hero-text);">CommonPub</strong>
149
- </span>
150
- </div>
151
-
152
- <div class="cpub-hero-title">{{ c?.title || 'Contest' }}</div>
153
- <div class="cpub-hero-tagline">
154
- {{ c?.description || 'No description available.' }}
155
- </div>
156
-
157
- <div class="cpub-hero-meta">
158
- <span v-if="c?.startDate || c?.endDate" class="cpub-hero-meta-item"><i class="fa fa-calendar"></i> {{ c?.startDate ? new Date(c.startDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '' }}{{ c?.startDate && c?.endDate ? ' — ' : '' }}{{ c?.endDate ? new Date(c.endDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '' }}</span>
159
- <span v-if="c?.startDate || c?.endDate" class="cpub-hero-meta-sep">|</span>
160
- <span class="cpub-hero-meta-item"><i class="fa fa-folder-open"></i> {{ c?.entryCount ?? 0 }} entries</span>
161
- </div>
162
-
163
- <!-- COUNTDOWN -->
164
- <div class="cpub-countdown-section">
165
- <div class="cpub-countdown-label"><i class="fa fa-clock" style="margin-right:4px;color:var(--accent);"></i>{{ c?.status === 'judging' ? 'Judging ends in' : c?.status === 'completed' ? 'Contest ended' : 'Submissions close in' }}</div>
166
- <div class="cpub-countdown-row">
167
- <div class="cpub-countdown-block">
168
- <div class="cpub-countdown-val">{{ countdown.days }}</div>
169
- <div class="cpub-countdown-unit">Days</div>
170
- </div>
171
- <div class="cpub-countdown-sep">:</div>
172
- <div class="cpub-countdown-block">
173
- <div class="cpub-countdown-val">{{ countdown.hours }}</div>
174
- <div class="cpub-countdown-unit">Hours</div>
175
- </div>
176
- <div class="cpub-countdown-sep">:</div>
177
- <div class="cpub-countdown-block">
178
- <div class="cpub-countdown-val">{{ countdown.mins }}</div>
179
- <div class="cpub-countdown-unit">Minutes</div>
180
- </div>
181
- <div class="cpub-countdown-sep">:</div>
182
- <div class="cpub-countdown-block">
183
- <div class="cpub-countdown-val">{{ countdown.secs }}</div>
184
- <div class="cpub-countdown-unit">Seconds</div>
185
- </div>
186
- </div>
187
- </div>
188
-
189
- <div class="cpub-hero-cta">
190
- <button v-if="isAuthenticated && c?.status === 'active'" class="cpub-btn cpub-btn-primary cpub-btn-lg" @click="showSubmitDialog = true"><i class="fa fa-upload"></i> Submit Entry</button>
191
- <button class="cpub-btn cpub-btn-lg cpub-btn-dark"><i class="fa fa-file-lines"></i> View Rules</button>
192
- <button class="cpub-btn cpub-btn-sm cpub-btn-dark" style="margin-left:4px;"><i class="fa fa-bell"></i> Notify Me</button>
193
- </div>
194
-
195
- <!-- Admin controls -->
196
- <div v-if="isAdmin && c" class="cpub-admin-controls">
197
- <span class="cpub-admin-controls-label"><i class="fa-solid fa-shield-halved"></i> Admin</span>
198
- <button v-if="c.status === 'upcoming'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="transitionStatus('active')"><i class="fa-solid fa-play"></i> Activate</button>
199
- <button v-if="c.status === 'active'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="transitionStatus('judging')"><i class="fa-solid fa-gavel"></i> Start Judging</button>
200
- <button v-if="c.status === 'judging'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="transitionStatus('completed')"><i class="fa-solid fa-check"></i> Complete</button>
201
- <span class="cpub-admin-status">Status: <strong>{{ c.status }}</strong></span>
202
- </div>
203
-
204
- <div class="cpub-hero-stats">
205
- <div class="cpub-hero-stat">
206
- <div class="cpub-hero-stat-val">{{ c?.entryCount ?? 0 }}</div>
207
- <div class="cpub-hero-stat-label">Entries</div>
208
- </div>
209
- <div class="cpub-hero-stat">
210
- <div class="cpub-hero-stat-val">{{ c?.status ?? 'draft' }}</div>
211
- <div class="cpub-hero-stat-label">Status</div>
212
- </div>
213
- </div>
214
- </div>
215
- </div>
91
+ <ContestHero
92
+ :contest="c"
93
+ :is-admin="isAdmin"
94
+ :is-authenticated="isAuthenticated"
95
+ :transitioning="transitioning"
96
+ @submit-entry="showSubmitDialog = true"
97
+ @transition="transitionStatus"
98
+ @copy-link="copyLink"
99
+ />
216
100
 
217
101
  <!-- SUBMIT ENTRY DIALOG -->
218
102
  <div v-if="showSubmitDialog" class="cpub-submit-overlay" @click.self="showSubmitDialog = false">
219
103
  <div class="cpub-submit-dialog" role="dialog" aria-label="Submit entry">
220
104
  <div class="cpub-submit-header">
221
- <h2 style="font-size: 14px; font-weight: 700;">Submit Entry</h2>
222
- <button style="background:none;border:none;color:var(--text-faint);cursor:pointer;font-size:14px;" @click="showSubmitDialog = false"><i class="fa-solid fa-times"></i></button>
105
+ <h2>Submit Entry</h2>
106
+ <button class="cpub-submit-close" @click="showSubmitDialog = false"><i class="fa-solid fa-times"></i></button>
223
107
  </div>
224
108
  <div class="cpub-submit-body">
225
- <p style="font-size: 12px; color: var(--text-dim); margin-bottom: 12px;">Select one of your published projects to submit as an entry.</p>
109
+ <p class="cpub-submit-hint">Select one of your published projects to submit as an entry.</p>
226
110
  <select v-model="submitContentId" class="cpub-submit-select">
227
111
  <option value="">Select a project...</option>
228
112
  <option v-for="item in (userContent?.items ?? [])" :key="item.id" :value="item.id">
@@ -242,345 +126,65 @@ async function submitEntry(): Promise<void> {
242
126
  <!-- MAIN CONTENT -->
243
127
  <div class="cpub-contest-main">
244
128
  <div class="cpub-contest-layout">
245
-
246
- <!-- MAIN COLUMN -->
247
129
  <div>
248
-
249
130
  <!-- ABOUT -->
250
- <div style="margin-bottom:20px;">
131
+ <div class="cpub-about-section">
251
132
  <div class="cpub-sec-head">
252
- <h2><i class="fa fa-circle-info" style="color:var(--accent);margin-right:6px;"></i>About This Contest</h2>
133
+ <h2><i class="fa fa-circle-info" style="color: var(--accent);"></i> About This Contest</h2>
253
134
  </div>
254
135
  <div class="cpub-about-card">
255
- <div class="cpub-about-body">
256
- <p>{{ c?.description || 'No description available for this contest.' }}</p>
257
- </div>
258
- </div>
259
- </div>
260
-
261
- <!-- RULES -->
262
- <div v-if="c?.rules" style="margin-bottom:20px;">
263
- <div class="cpub-sec-head">
264
- <h2><i class="fa fa-file-lines" style="color:var(--purple);margin-right:6px;"></i>Rules</h2>
265
- </div>
266
- <div class="cpub-rules-card">
267
- <div class="cpub-about-body" style="white-space: pre-line;">{{ c.rules }}</div>
268
- </div>
269
- </div>
270
-
271
- <!-- ENTRIES -->
272
- <div style="margin-bottom:20px;">
273
- <div class="cpub-sec-head">
274
- <h2><i class="fa fa-box-open" style="color:var(--teal);margin-right:6px;"></i>Submitted Entries</h2>
275
- <span class="cpub-sec-sub">{{ c?.entryCount ?? entries.length }} entries</span>
276
- </div>
277
- <div v-if="entries.length" class="cpub-entry-grid">
278
- <div
279
- v-for="(entry, i) in entries"
280
- :key="entry.id"
281
- class="cpub-entry-card"
282
- >
283
- <div class="cpub-entry-thumb" :class="i % 2 === 0 ? 'cpub-entry-bg-light' : 'cpub-entry-bg-dark'">
284
- <img v-if="entry.contentCoverImageUrl" :src="entry.contentCoverImageUrl" :alt="entry.contentTitle" class="cpub-entry-cover-img" />
285
- <template v-else>
286
- <div class="cpub-entry-grid-pat"></div>
287
- <div class="cpub-entry-icon" style="color: var(--accent)"><i class="fa-solid fa-microchip"></i></div>
288
- </template>
289
- <span v-if="entry.rank" class="cpub-entry-rank" :class="`cpub-rank-${entry.rank}`">#{{ entry.rank }}</span>
290
- </div>
291
- <div class="cpub-entry-body">
292
- <NuxtLink :to="`/u/${entry.authorUsername}/${entry.contentType}/${entry.contentSlug}`" class="cpub-entry-title">{{ entry.contentTitle || `Entry #${i + 1}` }}</NuxtLink>
293
- <div class="cpub-entry-author">
294
- <div class="cpub-entry-av">
295
- <img v-if="entry.authorAvatarUrl" :src="entry.authorAvatarUrl" :alt="entry.authorName || entry.authorUsername" class="cpub-entry-av-img" />
296
- <span v-else>{{ (entry.authorName || entry.authorUsername || '?').charAt(0).toUpperCase() }}</span>
297
- </div>
298
- <NuxtLink v-if="entry.authorUsername" :to="`/u/${entry.authorUsername}`" style="color: var(--text-dim); text-decoration: none;">{{ entry.authorName }}</NuxtLink>
299
- <span class="cpub-entry-meta">{{ new Date(entry.submittedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) }}</span>
300
- </div>
301
- <div class="cpub-entry-footer">
302
- <button
303
- class="cpub-vote-btn"
304
- :class="{ 'cpub-voted': votedEntries.has(entry.id) }"
305
- @click.prevent="toggleVote(entry.id)"
306
- ><i class="fa fa-arrow-up"></i> Vote</button>
307
- <span v-if="entry.score != null" class="cpub-entry-views">Score: {{ entry.score }}</span>
308
- </div>
309
- </div>
310
- </div>
311
- </div>
312
- <div v-else class="cpub-empty-state" style="padding: 32px 0;">
313
- <div class="cpub-empty-state-icon"><i class="fa-solid fa-box-open"></i></div>
314
- <p class="cpub-empty-state-title">No entries yet</p>
315
- <p class="cpub-empty-state-desc">Be the first to submit an entry!</p>
316
- </div>
317
- </div>
318
-
319
- </div>
320
-
321
- <!-- SIDEBAR -->
322
- <div>
323
-
324
- <!-- STATUS -->
325
- <div class="cpub-sb-card">
326
- <div class="cpub-sb-title"><i class="fa-solid fa-circle-info" style="margin-right:5px;"></i>Status</div>
327
- <div style="font-size: 12px; color: var(--text-dim); display: flex; flex-direction: column; gap: 8px;">
328
- <div><strong>Status:</strong> {{ c?.status ?? 'unknown' }}</div>
329
- <div v-if="c?.startDate"><strong>Starts:</strong> {{ new Date(c.startDate).toLocaleDateString() }}</div>
330
- <div v-if="c?.endDate"><strong>Ends:</strong> {{ new Date(c.endDate).toLocaleDateString() }}</div>
331
- <div><strong>Entries:</strong> {{ c?.entryCount ?? 0 }}</div>
136
+ <p>{{ c?.description || 'No description available for this contest.' }}</p>
332
137
  </div>
333
138
  </div>
334
139
 
335
- <!-- SHARE -->
336
- <div class="cpub-sb-card">
337
- <div class="cpub-sb-title"><i class="fa-solid fa-share-nodes" style="margin-right:5px;"></i>Share This Contest</div>
338
- <div style="display:flex;gap:6px;flex-wrap:wrap;">
339
- <button class="cpub-btn cpub-btn-sm" style="flex:1;justify-content:center;" @click="copyLink()"><i class="fa fa-link"></i> Copy Link</button>
340
- </div>
341
- </div>
342
-
343
- <NuxtLink to="/contests" class="cpub-btn" style="width: 100%; text-align: center; display: block; margin-top: 12px;"><i class="fa fa-arrow-left"></i> All Contests</NuxtLink>
140
+ <ContestRules v-if="c?.rules" :rules="c.rules" />
141
+ <ContestPrizes v-if="c?.prizes?.length" :prizes="c.prizes" />
142
+ <ContestJudges v-if="c?.judges?.length" :judge-ids="c.judges" />
143
+ <ContestEntries
144
+ :entries="entries"
145
+ :contest-status="c?.status"
146
+ :current-user-id="user?.id"
147
+ @withdraw="withdrawEntry"
148
+ />
344
149
  </div>
345
150
 
151
+ <ContestSidebar :contest="c" :is-owner="isOwner" :is-judge="isJudge" @copy-link="copyLink" />
346
152
  </div>
347
153
  </div>
348
-
349
154
  </div>
350
155
  </template>
351
156
 
352
157
  <style scoped>
353
- /* Hero uses a dark context — local custom properties for dark-bg values */
354
- .cpub-hero {
355
- --hero-bg: var(--text);
356
- --hero-text: var(--color-text-inverse);
357
- --hero-text-dim: var(--text-faint);
358
- --hero-border: rgba(255, 255, 255, 0.15);
359
- --hero-surface: rgba(255, 255, 255, 0.06);
360
- }
361
-
362
- /* Metallic prize colors — no token equivalents */
363
- .cpub-contest {
364
- --silver: var(--text-faint);
365
- --bronze: #a0724a;
366
- }
367
-
368
158
  /* SUBMIT DIALOG */
369
159
  .cpub-submit-overlay { position: fixed; inset: 0; z-index: 200; background: var(--color-surface-overlay-light); display: flex; align-items: center; justify-content: center; }
370
160
  .cpub-submit-dialog { background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-xl); width: 420px; max-width: 90vw; }
371
161
  .cpub-submit-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: var(--border-width-default) solid var(--border); }
162
+ .cpub-submit-header h2 { font-size: 14px; font-weight: 700; }
163
+ .cpub-submit-close { background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 14px; }
372
164
  .cpub-submit-body { padding: 16px; }
165
+ .cpub-submit-hint { font-size: 12px; color: var(--text-dim); margin-bottom: 12px; }
373
166
  .cpub-submit-select { width: 100%; padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; }
374
167
  .cpub-submit-select:focus { border-color: var(--accent); outline: none; }
375
168
  .cpub-submit-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: var(--border-width-default) solid var(--border); }
376
169
 
377
- /* HERO */
378
- .cpub-hero { position: relative; overflow: hidden; background: var(--hero-bg); padding: 56px 0 48px; }
379
- .cpub-hero-pattern { position: absolute; inset: 0; }
380
- .cpub-hero-dots { position: absolute; inset: 0; background-image: radial-gradient(var(--accent-border) 1.5px, transparent 1.5px); background-size: 28px 28px; opacity: .3; }
381
- .cpub-hero-lines { position: absolute; inset: 0; background-image: linear-gradient(var(--accent-bg) 1px, transparent 1px), linear-gradient(90deg, var(--accent-bg) 1px, transparent 1px); background-size: 56px 56px; }
382
- .cpub-hero-inner { max-width: 1100px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; }
383
- .cpub-hero-eyebrow { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
384
- .cpub-contest-badge { font-size: 9px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; font-family: var(--font-mono); color: var(--accent); background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); padding: 3px 10px; border-radius: var(--radius); }
385
- .cpub-hero-host { font-size: 11px; color: var(--hero-text-dim); font-family: var(--font-mono); display: flex; align-items: center; gap: 6px; }
386
- .cpub-hero-title { font-size: 36px; font-weight: 800; letter-spacing: -.03em; line-height: 1.1; margin-bottom: 10px; color: var(--hero-text); }
387
- .cpub-hero-highlight { color: var(--accent); }
388
- .cpub-hero-tagline { font-size: 14px; color: var(--hero-text-dim); line-height: 1.55; max-width: 580px; margin-bottom: 28px; }
389
- .cpub-hero-meta { display: flex; align-items: center; gap: 20px; font-size: 11px; color: var(--hero-text-dim); font-family: var(--font-mono); margin-bottom: 28px; }
390
- .cpub-hero-meta-item { display: flex; align-items: center; gap: 5px; }
391
- .cpub-hero-meta-sep { color: var(--hero-border); }
392
-
393
- /* COUNTDOWN */
394
- .cpub-countdown-section { margin-bottom: 28px; }
395
- .cpub-countdown-label { font-size: 10px; font-family: var(--font-mono); color: var(--hero-text-dim); letter-spacing: .1em; text-transform: uppercase; margin-bottom: 10px; }
396
- .cpub-countdown-row { display: flex; align-items: center; gap: 8px; }
397
- .cpub-countdown-block { display: flex; flex-direction: column; align-items: center; background: var(--hero-surface); border: var(--border-width-default) solid var(--hero-border); border-radius: var(--radius); padding: 10px 16px; min-width: 60px; box-shadow: 4px 4px 0 var(--hero-surface); }
398
- .cpub-countdown-val { font-size: 26px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); line-height: 1; margin-bottom: 4px; }
399
- .cpub-countdown-unit { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; color: var(--hero-text-dim); font-family: var(--font-mono); }
400
- .cpub-countdown-sep { font-size: 20px; font-weight: 700; color: var(--hero-border); margin-top: -8px; font-family: var(--font-mono); }
401
-
402
- /* HERO CTA & STATS */
403
- .cpub-hero-cta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
404
- .cpub-total-prize { font-size: 12px; color: var(--hero-text-dim); font-family: var(--font-mono); display: flex; align-items: center; gap: 6px; padding-left: 10px; border-left: var(--border-width-default) solid var(--hero-border); }
405
- .cpub-total-prize strong { color: var(--yellow); font-size: 15px; }
406
- .cpub-hero-stats { display: flex; gap: 24px; margin-top: 28px; padding-top: 24px; border-top: var(--border-width-default) solid var(--hero-border); }
407
- .cpub-hero-stat { display: flex; flex-direction: column; }
408
- .cpub-hero-stat-val { font-size: 20px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); }
409
- .cpub-hero-stat-label { font-size: 10px; color: var(--hero-text-dim); text-transform: uppercase; letter-spacing: .1em; font-family: var(--font-mono); }
410
-
411
- /* BUTTONS (page-specific) */
412
- .cpub-btn-lg { padding: 10px 22px; font-size: 13px; }
413
- .cpub-btn-dark { background: var(--hero-surface); color: var(--hero-text); border-color: var(--hero-border); }
414
- .cpub-btn-dark:hover { background: var(--hero-surface); }
415
-
416
- /* AVATARS */
417
- .cpub-av { display: flex; align-items: center; justify-content: center; border-radius: 50%; font-weight: 600; font-family: var(--font-mono); flex-shrink: 0; background: var(--surface3); border: var(--border-width-default) solid var(--border); color: var(--text-dim); }
418
- .cpub-av-sm { width: 24px; height: 24px; font-size: 9px; }
419
-
420
170
  /* LAYOUT */
421
171
  .cpub-contest-main { max-width: 1100px; margin: 0 auto; padding: 32px; }
422
172
  .cpub-contest-layout { display: grid; grid-template-columns: 1fr 300px; gap: 28px; align-items: start; }
423
173
 
424
- /* SECTION HEADERS (page-specific) */
425
- .cpub-sec-sub { font-size: 11px; color: var(--text-faint); margin-left: auto; font-family: var(--font-mono); }
174
+ /* SECTION HEADERS */
175
+ .cpub-sec-head { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
176
+ .cpub-sec-head h2 { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
426
177
 
427
178
  /* ABOUT */
428
- .cpub-about-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; box-shadow: var(--shadow-md); }
429
- .cpub-about-body { font-size: 12px; color: var(--text-dim); line-height: 1.7; }
430
- .cpub-about-body p { margin-bottom: 10px; }
431
- .cpub-about-body p:last-child { margin-bottom: 0; }
432
- .cpub-highlight-box { background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); padding: 12px 14px; margin: 12px 0; font-size: 11px; color: var(--text-dim); }
433
- .cpub-highlight-box strong { color: var(--accent); }
179
+ .cpub-about-section { margin-bottom: 20px; }
180
+ .cpub-about-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow-md); font-size: 12px; color: var(--text-dim); line-height: 1.7; }
181
+ .cpub-about-card p { margin: 0; }
434
182
 
435
- /* PRIZES */
436
- .cpub-prize-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
437
- .cpub-prize-card { border-radius: var(--radius); padding: 20px; position: relative; overflow: hidden; text-align: center; background: var(--surface); border: var(--border-width-default) solid var(--border); }
438
- .cpub-prize-gold { box-shadow: var(--shadow-accent); }
439
- .cpub-prize-silver { box-shadow: var(--shadow-md); }
440
- .cpub-prize-bronze { box-shadow: var(--shadow-md); }
441
- .cpub-prize-rank { font-size: 11px; font-family: var(--font-mono); font-weight: 600; letter-spacing: .08em; margin-bottom: 8px; position: relative; z-index: 1; }
442
- .cpub-prize-rank-gold { color: var(--yellow); }
443
- .cpub-prize-rank-silver { color: var(--silver); }
444
- .cpub-prize-rank-bronze { color: var(--bronze); }
445
- .cpub-prize-icon { font-size: 28px; margin-bottom: 8px; position: relative; z-index: 1; }
446
- .cpub-prize-icon-gold { color: var(--yellow); }
447
- .cpub-prize-icon-silver { color: var(--silver); }
448
- .cpub-prize-icon-bronze { color: var(--bronze); }
449
- .cpub-prize-amount { font-size: 24px; font-weight: 800; font-family: var(--font-mono); margin-bottom: 4px; position: relative; z-index: 1; }
450
- .cpub-prize-amount-gold { color: var(--yellow); }
451
- .cpub-prize-amount-silver { color: var(--silver); }
452
- .cpub-prize-amount-bronze { color: var(--bronze); }
453
- .cpub-prize-label { font-size: 10px; color: var(--text-faint); margin-bottom: 10px; font-family: var(--font-mono); position: relative; z-index: 1; }
454
- .cpub-prize-perks { text-align: left; position: relative; z-index: 1; }
455
- .cpub-prize-perk { font-size: 10px; color: var(--text-dim); display: flex; align-items: center; gap: 5px; margin-bottom: 3px; font-family: var(--font-mono); }
456
- .cpub-prize-perk i { font-size: 8px; color: var(--green); }
457
- .cpub-prize-additional { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 12px; }
458
- .cpub-prize-extra { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 12px; text-align: center; box-shadow: var(--shadow-md); }
459
- .cpub-prize-extra-title { font-size: 11px; font-weight: 600; margin-bottom: 2px; }
460
- .cpub-prize-extra-val { font-size: 14px; font-weight: 700; font-family: var(--font-mono); color: var(--teal); }
461
- .cpub-prize-extra-label { font-size: 9px; color: var(--text-faint); font-family: var(--font-mono); }
462
-
463
- /* RULES */
464
- .cpub-rules-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; box-shadow: var(--shadow-md); }
465
- .cpub-rule-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; font-size: 12px; color: var(--text-dim); line-height: 1.55; }
466
- .cpub-rule-item:last-child { margin-bottom: 0; }
467
- .cpub-rule-icon { font-size: 11px; color: var(--accent); margin-top: 2px; flex-shrink: 0; width: 14px; }
468
-
469
- /* ENTRIES */
470
- .cpub-entries-filter { display: flex; gap: 6px; margin-bottom: 14px; }
471
- .cpub-entry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
472
- .cpub-entry-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; box-shadow: var(--shadow-md); }
473
- .cpub-entry-card:hover { box-shadow: var(--shadow-accent); }
474
- .cpub-entry-thumb { height: 110px; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; }
475
- .cpub-entry-bg-light { background: var(--surface2); }
476
- .cpub-entry-bg-dark { background: var(--surface3); }
477
- .cpub-entry-grid-pat { position: absolute; inset: 0; background-image: linear-gradient(var(--border2) 1px, transparent 1px), linear-gradient(90deg, var(--border2) 1px, transparent 1px); background-size: 20px 20px; opacity: .3; }
478
- .cpub-entry-icon { position: relative; z-index: 1; font-size: 22px; opacity: .65; }
479
- .cpub-entry-rank { position: absolute; top: 8px; left: 8px; z-index: 2; font-size: 10px; font-family: var(--font-mono); font-weight: 700; padding: 2px 7px; border-radius: var(--radius); }
480
- .cpub-rank-1 { background: var(--yellow-bg); color: var(--yellow); border: var(--border-width-default) solid var(--yellow); }
481
- .cpub-rank-2 { background: var(--surface2); color: var(--silver); border: var(--border-width-default) solid var(--silver); }
482
- .cpub-rank-3 { background: var(--surface2); color: var(--bronze); border: var(--border-width-default) solid var(--bronze); }
483
- .cpub-entry-body { padding: 10px 12px; }
484
- .cpub-entry-title { font-size: 12px; font-weight: 600; margin-bottom: 3px; line-height: 1.3; }
485
- .cpub-entry-cover-img { width: 100%; height: 100%; object-fit: cover; }
486
- .cpub-entry-av { width: 18px; height: 18px; border-radius: 50%; background: var(--surface3); border: var(--border-width-default) solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 7px; font-family: var(--font-mono); color: var(--text-faint); flex-shrink: 0; overflow: hidden; }
487
- .cpub-entry-av-img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
488
- .cpub-entry-author { font-size: 10px; color: var(--text-dim); font-family: var(--font-mono); margin-bottom: 6px; display: flex; align-items: center; gap: 5px; }
489
- .cpub-entry-footer { display: flex; align-items: center; gap: 6px; }
490
- .cpub-vote-btn { display: flex; align-items: center; gap: 4px; font-size: 10px; font-family: var(--font-mono); padding: 3px 8px; border-radius: var(--radius); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text-dim); cursor: pointer; }
491
- .cpub-vote-btn:hover { background: var(--surface2); }
492
- .cpub-vote-btn i { font-size: 9px; }
493
- .cpub-voted { background: var(--accent-bg); border-color: var(--accent); color: var(--accent); }
494
- .cpub-entry-views { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); margin-left: auto; display: flex; align-items: center; gap: 3px; }
495
-
496
- /* JUDGES */
497
- .cpub-judges-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
498
- .cpub-judge-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 14px; text-align: center; box-shadow: var(--shadow-md); }
499
- .cpub-judge-av { width: 44px; height: 44px; border-radius: 50%; margin: 0 auto 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; font-family: var(--font-mono); border: var(--border-width-default) solid var(--border); }
500
- .cpub-judge-name { font-size: 11px; font-weight: 600; margin-bottom: 2px; }
501
- .cpub-judge-title { font-size: 10px; color: var(--text-dim); line-height: 1.35; font-family: var(--font-mono); }
502
- .cpub-judge-org { font-size: 10px; color: var(--accent); font-family: var(--font-mono); margin-top: 2px; }
503
-
504
- /* TIMELINE */
505
- .cpub-tl-item { display: flex; gap: 12px; margin-bottom: 14px; position: relative; }
506
- .cpub-tl-item:not(.cpub-tl-last)::before { content: ''; position: absolute; left: 10px; top: 20px; bottom: -14px; width: 2px; background: var(--border); }
507
- .cpub-tl-last { margin-bottom: 0; }
508
- .cpub-tl-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 8px; margin-top: 1px; }
509
- .cpub-tl-done { background: var(--green-bg); border: var(--border-width-default) solid var(--green); color: var(--green); }
510
- .cpub-tl-active { background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); color: var(--accent); }
511
- .cpub-tl-upcoming { background: var(--surface2); border: var(--border-width-default) solid var(--border2); color: var(--text-faint); }
512
- .cpub-tl-info { flex: 1; padding-top: 1px; }
513
- .cpub-tl-name { font-size: 11px; font-weight: 600; margin-bottom: 1px; }
514
- .cpub-tl-name-done { color: var(--green); }
515
- .cpub-tl-name-active { color: var(--text); }
516
- .cpub-tl-name-upcoming { color: var(--text-faint); }
517
- .cpub-tl-date { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); }
518
- .cpub-tl-status { font-size: 9px; font-family: var(--font-mono); padding: 1px 5px; border-radius: var(--radius); }
519
- .cpub-status-done { color: var(--green); background: var(--green-bg); border: var(--border-width-default) solid var(--green); }
520
- .cpub-status-active { color: var(--accent); background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); }
521
-
522
- /* SPONSORS */
523
- .cpub-sponsor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
524
- .cpub-sponsor-card { background: var(--surface2); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; }
525
- .cpub-sponsor-icon { font-size: 16px; margin-bottom: 2px; color: var(--text-dim); }
526
- .cpub-sponsor-name { font-size: 10px; font-weight: 600; font-family: var(--font-mono); color: var(--text); }
527
- .cpub-sponsor-tier { font-size: 8px; font-family: var(--font-mono); color: var(--text-faint); }
528
- .cpub-sponsor-link { font-size: 11px; color: var(--accent); text-decoration: none; font-family: var(--font-mono); }
529
- .cpub-sponsor-link:hover { text-decoration: underline; }
530
-
531
- /* FAQ */
532
- .cpub-faq-wrap { box-shadow: none; padding: 0; border: none; background: transparent; }
533
- .cpub-faq-item { border: var(--border-width-default) solid var(--border); margin-bottom: -2px; overflow: hidden; }
534
- .cpub-faq-item:first-of-type { border-top: var(--border-width-default) solid var(--border); }
535
- .cpub-faq-q { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; cursor: pointer; font-size: 11px; font-weight: 500; gap: 8px; background: var(--surface); }
536
- .cpub-faq-q:hover { background: var(--surface2); color: var(--accent); }
537
- .cpub-faq-q i { font-size: 10px; color: var(--text-faint); flex-shrink: 0; transition: transform .15s; }
538
- .cpub-faq-open .cpub-faq-q i { transform: rotate(180deg); }
539
- .cpub-faq-open .cpub-faq-q { background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border2); }
540
- .cpub-faq-a { font-size: 11px; color: var(--text-dim); line-height: 1.55; padding: 10px 12px; display: none; background: var(--surface); }
541
- .cpub-faq-open .cpub-faq-a { display: block; }
542
-
543
- /* Admin controls */
544
- .cpub-admin-controls {
545
- display: flex; align-items: center; gap: 8px; margin-top: 16px;
546
- padding: 10px 14px; background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border);
547
- }
548
- .cpub-admin-controls-label {
549
- font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
550
- color: var(--accent); margin-right: 4px; font-family: var(--font-mono);
551
- }
552
- .cpub-admin-status {
553
- font-size: 11px; color: var(--text-dim); margin-left: auto; font-family: var(--font-mono);
554
- }
555
- .cpub-admin-status strong { color: var(--accent); text-transform: capitalize; }
556
-
557
- /* ── RESPONSIVE ── */
558
183
  @media (max-width: 768px) {
559
- .cpub-hero { padding: 32px 0 28px; }
560
- .cpub-hero-inner { padding: 0 16px; }
561
- .cpub-hero-title { font-size: 24px; }
562
- .cpub-hero-tagline { font-size: 13px; }
563
- .cpub-hero-meta { flex-wrap: wrap; gap: 10px; }
564
184
  .cpub-contest-main { padding: 20px 16px; }
565
185
  .cpub-contest-layout { grid-template-columns: 1fr; }
566
- .cpub-prize-grid { grid-template-columns: 1fr; }
567
- .cpub-judges-grid { grid-template-columns: 1fr 1fr; }
568
- .cpub-entry-grid { grid-template-columns: 1fr; }
569
- .cpub-countdown-block { padding: 8px 12px; min-width: 48px; }
570
- .cpub-countdown-val { font-size: 20px; }
571
186
  }
572
-
573
187
  @media (max-width: 480px) {
574
- .cpub-hero-title { font-size: 20px; }
575
- .cpub-hero-tagline { font-size: 12px; margin-bottom: 16px; }
576
- .cpub-hero-stats { flex-wrap: wrap; gap: 16px; }
577
- .cpub-hero-stat-val { font-size: 16px; }
578
188
  .cpub-contest-main { padding: 16px 12px; }
579
- .cpub-judges-grid { grid-template-columns: 1fr; }
580
- .cpub-prize-additional { grid-template-columns: 1fr; }
581
- .cpub-sponsor-grid { grid-template-columns: 1fr; }
582
- .cpub-countdown-row { flex-wrap: wrap; justify-content: center; }
583
- .cpub-hero-cta { flex-direction: column; align-items: stretch; }
584
- .cpub-total-prize { border-left: none; padding-left: 0; justify-content: center; }
585
189
  }
586
190
  </style>