A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1---
2import Base from '../layouts/Base.astro';
3import Logo from '../components/Logo.astro';
4import Footer from '../components/Footer.astro';
5import AccountMenu from '../components/AccountMenu.tsx';
6import { PHASES, DEFAULT_PHASE } from '../lib/landing/time-of-day';
7
8const fallback = PHASES[ DEFAULT_PHASE ];
9---
10
11<Base
12 title="SkyPress: a writing studio for the open social web"
13 description="A standalone, long-form writing studio for the AT Protocol. Write in blocks and publish to the open social web, under your own account."
14 phase={DEFAULT_PHASE}
15>
16 <Fragment slot="head">
17 <script is:inline>
18 // Set the sky phase before first paint so there is no flash.
19 // Boundaries mirror src/lib/landing/time-of-day.ts (kept in sync by its test).
20 ( function () {
21 var h = new Date().getHours();
22 var p =
23 h >= 21 || h < 5 ? 'night'
24 : h < 7 ? 'dawn'
25 : h < 10 ? 'morning'
26 : h < 16 ? 'midday'
27 : h < 19 ? 'golden'
28 : 'dusk';
29 document.documentElement.dataset.phase = p;
30 } )();
31 </script>
32 </Fragment>
33
34 <div class="page">
35 <!-- The sky backs the masthead + hero together. Wrapping them lets the sky fill the
36 zone exactly (inset: 0) instead of guessing a fixed height — on a narrow phone the
37 wrapping title can push the hero past any fixed height, dropping the trailing copy
38 onto the bare dark page background as unreadable dark-on-dark text. -->
39 <div class="skyzone">
40 <div class="sky" aria-hidden="true">
41 <div class="stars"></div>
42 <span class="shootingstar"></span>
43 <div class="bloom"></div>
44 <div class="halo"></div>
45 <div class="horizon"></div>
46 </div>
47
48 <header class="masthead">
49 <Logo />
50 <div class="masthead__right">
51 <a class="btn btn--ghost masthead-write" href="/write">Write</a>
52 <AccountMenu client:only="react" />
53 </div>
54 </header>
55
56 <main class="hero">
57 <p class="eyebrow" id="greet">{fallback.greeting}</p>
58 <h1 class="hero__title" id="headline" set:html={fallback.headlineHtml} />
59 <p class="hero__lede" id="lede">{fallback.lede}</p>
60 <div class="hero__cta">
61 <a class="btn btn--primary hero__start" href="/write">Start writing →</a>
62 </div>
63 <p class="hero__free">Free & open-source. Your words live in your account, not ours.</p>
64 </main>
65 </div>
66
67 <section class="showcase">
68 <p class="showcase__label">See it in action</p>
69 <div class="showcase__strip">
70 <figure class="shot">
71 <div class="shot__frame"><img src="/screenshots/editor.png" alt="The SkyPress block editor" loading="lazy" /></div>
72 <figcaption>The editor</figcaption>
73 </figure>
74 <figure class="shot">
75 <div class="shot__frame"><img src="/screenshots/published-article.png" alt="A published SkyPress article" loading="lazy" /></div>
76 <figcaption>A published piece</figcaption>
77 </figure>
78 <figure class="shot">
79 <div class="shot__frame"><img src="/screenshots/author-page.png" alt="An author's publications page" loading="lazy" /></div>
80 <figcaption>All your publications</figcaption>
81 </figure>
82 </div>
83 </section>
84
85 <Footer />
86 </div>
87</Base>
88
89<script>
90 import { PHASES, phaseForHour } from '../lib/landing/time-of-day';
91
92 const now = new Date();
93 const phase = PHASES[ phaseForHour( now.getHours() ) ];
94 const greet = document.getElementById( 'greet' );
95 const headline = document.getElementById( 'headline' );
96 const lede = document.getElementById( 'lede' );
97 const hh = String( now.getHours() ).padStart( 2, '0' );
98 const mm = String( now.getMinutes() ).padStart( 2, '0' );
99 if ( greet ) greet.textContent = `${ phase.greeting } · ${ hh }:${ mm }`;
100 if ( headline ) headline.innerHTML = phase.headlineHtml;
101 if ( lede ) lede.textContent = phase.lede;
102</script>
103
104<style>
105 .page {
106 position: relative;
107 min-height: 100vh;
108 display: flex;
109 flex-direction: column;
110 overflow: hidden;
111 }
112
113 /* The sky zone holds the masthead + hero; the sky fills it exactly so the backdrop always
114 reaches the bottom of the hero, however tall the (wrapping) title makes it. flex: 1 lets
115 it grow to push the showcase down when the content is short. */
116 .skyzone {
117 position: relative;
118 flex: 1;
119 display: flex;
120 flex-direction: column;
121 }
122
123 /* ===== Atmospheric sky (hero backdrop) — varies by [data-phase] ===== */
124 .sky {
125 position: absolute;
126 inset: 0;
127 z-index: 0;
128 pointer-events: none;
129 }
130 .sky .stars,
131 .sky .bloom,
132 .sky .halo {
133 position: absolute;
134 inset: 0;
135 transition: opacity 0.6s ease;
136 }
137 .bloom {
138 background: radial-gradient(
139 ellipse 46% 42% at 50% 78%,
140 rgba(255, 238, 200, 0.55),
141 rgba(255, 200, 120, 0.3) 40%,
142 transparent 72%
143 );
144 }
145 .halo {
146 background: radial-gradient(circle at 50% 62%, transparent 0 70px, rgba(255, 236, 200, 0.85) 71px 73px, transparent 74px);
147 filter: drop-shadow(0 0 26px rgba(255, 200, 120, 0.45));
148 }
149 .horizon {
150 position: absolute;
151 left: 0;
152 right: 0;
153 top: 70%;
154 height: 1px;
155 background: linear-gradient(90deg, transparent, rgba(232, 146, 12, 0.7), transparent);
156 }
157 .stars {
158 opacity: 0;
159 background-image:
160 radial-gradient(1.5px 1.5px at 20% 18%, #fff, transparent),
161 radial-gradient(1.5px 1.5px at 67% 12%, #fff, transparent),
162 radial-gradient(1px 1px at 43% 30%, #fff, transparent),
163 radial-gradient(1px 1px at 82% 24%, #fff, transparent),
164 radial-gradient(1.5px 1.5px at 12% 38%, #fff, transparent),
165 radial-gradient(1px 1px at 90% 40%, #fff, transparent),
166 radial-gradient(1px 1px at 33% 9%, #fff, transparent);
167 }
168
169 /* Playful: a rare shooting star, dark phases only (silenced under reduced motion below). */
170 .shootingstar {
171 position: absolute;
172 top: 12%;
173 left: -8%;
174 width: 90px;
175 height: 2px;
176 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.9));
177 border-radius: 2px;
178 transform: rotate(16deg);
179 opacity: 0;
180 }
181 :global([data-phase='night']) .shootingstar,
182 :global([data-phase='dusk']) .shootingstar,
183 :global([data-phase='dawn']) .shootingstar {
184 animation: shoot 12s ease-in 4s infinite;
185 }
186 @keyframes shoot {
187 0% { left: -8%; top: 12%; opacity: 0; }
188 2% { opacity: 1; }
189 9% { left: 78%; top: 40%; opacity: 0; }
190 100% { left: 78%; top: 40%; opacity: 0; }
191 }
192
193 /* Per-phase sky gradient + star visibility + hero text colour */
194 :global([data-phase='night']) .sky { background: linear-gradient(180deg, #080611, #140f28 50%, #231a38); }
195 :global([data-phase='night']) .stars { opacity: 1; }
196 /* Night is the bare starfield — no sun glow, halo ring, or lit horizon. */
197 :global([data-phase='night']) .bloom,
198 :global([data-phase='night']) .halo,
199 :global([data-phase='night']) .horizon { opacity: 0; }
200 :global([data-phase='dawn']) .sky { background: linear-gradient(180deg, #231533, #7a2f63 42%, #d7613a 78%, #f4a14a); }
201 :global([data-phase='dawn']) .stars { opacity: 0.5; }
202 :global([data-phase='morning']) .sky { background: linear-gradient(180deg, #3a2752, #c0567f 34%, #f5934a 70%, #ffd98a); }
203 :global([data-phase='midday']) .sky { background: linear-gradient(180deg, #e9b977, #f7dca5 45%, #fdf0d6); }
204 :global([data-phase='golden']) .sky { background: linear-gradient(180deg, #4a2150, #b5417a 30%, #ef7d3a 64%, #f9c25a 92%, #ffe6ab); }
205 :global([data-phase='dusk']) .sky { background: linear-gradient(180deg, #140f2a, #4a2150 38%, #93324f 70%, #c75a3b); }
206 :global([data-phase='dusk']) .stars { opacity: 0.5; }
207
208 /* The whole sky zone (masthead + hero) is coloured by the phase, independent of the
209 OS theme: light text on dark skies, ink on the pale midday sky. */
210 :global([data-phase='night']) .masthead, :global([data-phase='night']) .hero,
211 :global([data-phase='dawn']) .masthead, :global([data-phase='dawn']) .hero,
212 :global([data-phase='morning']) .masthead, :global([data-phase='morning']) .hero,
213 :global([data-phase='golden']) .masthead, :global([data-phase='golden']) .hero,
214 :global([data-phase='dusk']) .masthead, :global([data-phase='dusk']) .hero {
215 --sky-ink: #fbf7ef;
216 --sky-soft: rgba(251, 247, 239, 0.94);
217 --sky-line: rgba(251, 247, 239, 0.55);
218 --sky-chip: rgba(255, 255, 255, 0.12);
219 --sky-shadow: 0 1px 14px rgba(20, 10, 4, 0.4);
220 }
221 :global([data-phase='midday']) .masthead, :global([data-phase='midday']) .hero {
222 --sky-ink: #241a10;
223 --sky-soft: rgba(36, 26, 16, 0.9);
224 --sky-line: rgba(36, 26, 16, 0.42);
225 --sky-chip: rgba(0, 0, 0, 0.05);
226 --sky-shadow: 0 1px 12px rgba(255, 248, 234, 0.6);
227 }
228
229 /* Logo wordmark (in the Logo component) follows the sky, not the OS theme. */
230 .masthead :global(.logo),
231 .masthead :global(.logo__word) {
232 color: var(--sky-ink);
233 }
234
235 /* Ghost buttons sitting on the sky get a frosted chip so they read at every phase. */
236 .masthead .btn--ghost,
237 .hero .btn--ghost {
238 background: var(--sky-chip);
239 color: var(--sky-ink);
240 border-color: var(--sky-line);
241 backdrop-filter: blur(8px);
242 -webkit-backdrop-filter: blur(8px);
243 }
244 .masthead .btn--ghost:hover,
245 .hero .btn--ghost:hover {
246 color: var(--sky-ink);
247 border-color: var(--sky-ink);
248 }
249
250 /* Signed-in account menu — frosted sky-chip trigger + dropdown. AccountMenu is
251 a `client:only` React island, so Astro's scoped styles never reach its DOM;
252 target it with `:global()`. The `.masthead` prefix stays scoped so it still
253 inherits the per-phase `--sky-*` custom properties. */
254 .masthead :global(.account-menu) {
255 position: relative;
256 }
257 .masthead :global(.account-menu__trigger) {
258 display: inline-flex;
259 align-items: center;
260 gap: 0.55rem;
261 padding: 0.32rem 0.7rem 0.32rem 0.4rem;
262 border-radius: var(--radius-sm);
263 background: var(--sky-chip);
264 border: 1px solid var(--sky-line);
265 color: var(--sky-ink);
266 font: inherit;
267 cursor: pointer;
268 text-shadow: var(--sky-shadow);
269 backdrop-filter: blur(8px);
270 -webkit-backdrop-filter: blur(8px);
271 }
272 .masthead :global(.account-menu__trigger:hover) {
273 border-color: var(--sky-ink);
274 }
275 .masthead :global(.account-menu__avatar) {
276 width: 30px;
277 height: 30px;
278 border-radius: 50%;
279 object-fit: cover;
280 flex: none;
281 }
282 .masthead :global(.account-menu__avatar--fallback) {
283 display: inline-flex;
284 align-items: center;
285 justify-content: center;
286 background: var(--sun-tint);
287 color: var(--sun);
288 font-weight: 700;
289 font-size: 0.85rem;
290 }
291 .masthead :global(.account-menu__who) {
292 display: flex;
293 flex-direction: column;
294 line-height: 1.1;
295 text-align: left;
296 }
297 .masthead :global(.account-menu__name) {
298 font-weight: 680;
299 font-size: 0.86rem;
300 }
301 .masthead :global(.account-menu__handle) {
302 font-size: 0.72rem;
303 opacity: 0.8;
304 }
305 .masthead :global(.account-menu__dropdown) {
306 position: absolute;
307 top: calc(100% + 0.4rem);
308 right: 0;
309 min-width: 11rem;
310 display: flex;
311 flex-direction: column;
312 padding: 0.3rem;
313 border-radius: var(--radius-sm);
314 background: var(--paper-raised);
315 border: 1px solid var(--line-strong);
316 box-shadow: var(--shadow);
317 z-index: 5;
318 animation: account-menu-in 0.14s ease;
319 }
320 /* Transparent bridge across the visual gap below the trigger, so moving the
321 cursor from the trigger into the dropdown never leaves `.account-menu` (which
322 would fire mouseleave and close the menu before the pointer arrives). */
323 .masthead :global(.account-menu__dropdown)::before {
324 content: '';
325 position: absolute;
326 left: 0;
327 right: 0;
328 top: -0.4rem;
329 height: 0.4rem;
330 }
331 .masthead :global(.account-menu__item) {
332 padding: 0.5rem 0.7rem;
333 border-radius: calc(var(--radius-sm) - 2px);
334 color: var(--ink);
335 font-size: 0.88rem;
336 text-decoration: none;
337 }
338 .masthead :global(.account-menu__item:hover) {
339 background: var(--line);
340 color: var(--ink);
341 }
342 @keyframes account-menu-in {
343 from {
344 opacity: 0;
345 transform: translateY(-4px);
346 }
347 }
348
349 .masthead {
350 position: relative;
351 /* Above .hero / .showcase (both z-index: 2). The account menu dropdown is confined to
352 the masthead's stacking context, so unless the masthead outranks the later siblings it
353 overlaps, their content paints over the open dropdown (it looks see-through) and
354 intercepts its clicks — most visible on mobile, where the dropdown reaches the hero. */
355 z-index: 3;
356 display: flex;
357 align-items: center;
358 justify-content: space-between;
359 padding: 1.5rem clamp(1.25rem, 5vw, 4rem);
360 }
361 .masthead__right {
362 display: flex;
363 align-items: center;
364 gap: 0.75rem;
365 }
366 /* AccountMenu sets data-signed-in once a session restores; hide the static link then. */
367 .masthead__right[data-signed-in] .masthead-write {
368 display: none;
369 }
370 .hero {
371 position: relative;
372 z-index: 2;
373 flex: 1;
374 width: 100%;
375 max-width: 46rem;
376 margin: 0 auto;
377 padding: clamp(2rem, 9vh, 5rem) clamp(1.25rem, 5vw, 4rem) clamp(3rem, 8vh, 5rem);
378 display: flex;
379 flex-direction: column;
380 align-items: center;
381 text-align: center;
382 }
383 .hero .eyebrow { color: var(--sun); }
384 .hero__title {
385 font-size: clamp(2.7rem, 8vw, 5rem);
386 font-weight: 760;
387 letter-spacing: -0.035em;
388 line-height: 0.98;
389 margin: 0.75rem 0 0;
390 max-width: 14ch;
391 color: var(--sky-ink);
392 text-shadow: var(--sky-shadow);
393 }
394 .hero__title :global(em) {
395 font-style: italic;
396 color: var(--sun);
397 }
398 .hero__lede {
399 font-size: clamp(1.1rem, 2.2vw, 1.35rem);
400 line-height: 1.5;
401 color: var(--sky-soft);
402 max-width: 36ch;
403 margin: 1.25rem 0 0;
404 text-shadow: var(--sky-shadow);
405 }
406 /* ===== See it in action (themed surface below the sky) ===== */
407 .showcase {
408 position: relative;
409 z-index: 2;
410 background: var(--paper);
411 border-top: 1px solid var(--line);
412 padding: clamp(2.5rem, 6vw, 4rem) clamp(1.25rem, 5vw, 4rem);
413 }
414 .showcase__label {
415 max-width: 64rem;
416 margin: 0 auto 1.25rem;
417 font-family: var(--font-mono);
418 font-size: 0.72rem;
419 letter-spacing: 0.08em;
420 text-transform: uppercase;
421 color: var(--sun);
422 }
423 .showcase__strip {
424 max-width: 64rem;
425 margin: 0 auto;
426 display: grid;
427 gap: 1.25rem;
428 grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
429 }
430 .shot { margin: 0; }
431 .shot__frame {
432 border: 1px solid var(--line-strong);
433 border-radius: var(--radius);
434 overflow: hidden;
435 background: var(--paper-raised);
436 box-shadow: var(--shadow);
437 aspect-ratio: 16 / 10;
438 }
439 .shot__frame img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }
440 .shot figcaption {
441 margin-top: 0.6rem;
442 font-size: 0.9rem;
443 color: var(--ink-soft);
444 }
445
446 /* ===== Primary CTA (lives on the sky) ===== */
447 .hero__cta { width: 100%; max-width: 26rem; margin: 2rem auto 0; }
448 /* Primary "Start writing" action — the writing-first front door, and the home page's only
449 CTA (sign-in happens via Publish on /write). Larger than the default .btn and lifted off
450 the sky with a soft shadow so it reads at every phase. */
451 .hero__start {
452 font-size: 1.05rem;
453 padding: 0.8rem 1.6rem;
454 box-shadow: 0 6px 22px rgba(20, 10, 4, 0.28);
455 }
456 .hero__free {
457 color: var(--sky-soft);
458 font-size: 0.9rem;
459 margin-top: 1rem;
460 text-shadow: var(--sky-shadow);
461 }
462 @media (prefers-reduced-motion: reduce) {
463 .shootingstar { animation: none; opacity: 0; }
464 .sky .stars, .sky .bloom, .sky .halo { transition: none; }
465 .masthead :global(.account-menu__dropdown) {
466 animation: none;
467 }
468 }
469</style>