Files
remanso/src/components/WelcomeWorld.vue
Julien Calixte a7ea2ce9cf refactor(profile): adopt UserPill and ProfileModal in home and repos
Replaces the duplicated profile-chip button and profile_modal dialog in
WelcomeWorld and RepoList with the shared components, and removes the
now-orphaned profile-chip, hw-modal, hw-ms-label, hw-btn-ghost, and
hw-rule styles. As a side effect the displayed identity now prefers the
ATProto handle over the GitHub username.
2026-05-17 14:15:18 +02:00

2270 lines
56 KiB
Vue

<script lang="ts" setup>
import fontColorContrast from "font-color-contrast"
import { getHex } from "pastel-color"
import { computed, onMounted, ref } from "vue"
import { useRouter } from "vue-router"
import ProfileModal from "@/components/ProfileModal.vue"
import ThemeSwap from "@/components/ThemeSwap.vue"
import UserPill from "@/components/UserPill.vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
import { useForm } from "@/hooks/useForm.hook"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { toShortDid } from "@/modules/atproto/shortDid"
import { useNeedReviewCards } from "@/modules/card/hooks/useNeedReviewCards"
import { useLastVisitedRepos } from "@/modules/history/hooks/useLastVisitedRepos.hook"
import { useFavoriteRepos } from "@/modules/repo/hooks/useFavoriteRepos.hook"
import { slugify } from "@/utils/slugify"
const { username, accessToken } = useGitHubLogin()
const { isLoggedIn: isATProtoLoggedIn, handle } = useATProtoLogin()
const { userInput, repoInput, submit } = useForm()
const { savedFavoriteRepos } = useFavoriteRepos()
const { lastVisitedRepos } = useLastVisitedRepos()
const { cardsToReview } = useNeedReviewCards()
const router = useRouter()
const isGitHubLoggedIn = computed(() => !!accessToken.value)
const isAnyUserLoggedIn = computed(
() => isGitHubLoggedIn.value || isATProtoLoggedIn.value
)
const displayUsername = computed(() => handle.value || username.value || "")
const openProfile = () => {
;(document.getElementById("profile_modal") as HTMLDialogElement)?.showModal()
}
const tileStyle = (seed: string) => {
const bg = getHex(seed)
return { backgroundColor: bg, color: fontColorContrast(bg) }
}
const openRepo = (user: string, repo: string) => {
router.push({ name: "FluxNoteView", params: { user, repo } })
}
const greeting = computed(() => {
const h = new Date().getHours()
const name = displayUsername.value
if (h < 6) return `Still up${name ? ", " + name : ""}?`
if (h < 12) return `Good morning${name ? ", " + name : ""}.`
if (h < 18) return `Good afternoon${name ? ", " + name : ""}.`
return `Good evening${name ? ", " + name : ""}.`
})
const today = computed(() =>
new Date().toLocaleDateString(undefined, {
weekday: "long",
month: "long",
day: "numeric"
})
)
const offsetHighlighted = ref(false)
const flashOffset = () => {
offsetHighlighted.value = true
setTimeout(() => {
offsetHighlighted.value = false
}, 900)
}
const firstHighlighted = ref(false)
const flashFirst = () => {
firstHighlighted.value = true
setTimeout(() => {
firstHighlighted.value = false
}, 900)
}
const zettelRevealed = ref(false)
const toggleZettel = () => {
zettelRevealed.value = !zettelRevealed.value
}
const reviewRepo = computed(() => savedFavoriteRepos.value[0] ?? null)
const showReviewCard = computed(
() => cardsToReview.value.length > 0 && reviewRepo.value !== null
)
const {
notes: publicNotes,
isLoading: publicNotesLoading,
onLoadMore: loadPublicNotes,
getAuthor: getPublicAuthor
} = usePublicNoteList()
const recentPublicNotes = computed(() => publicNotes.value.slice(0, 5))
const sidebarPublicNotes = computed(() => publicNotes.value.slice(0, 3))
const hasPublicNotes = computed(() => recentPublicNotes.value.length > 0)
const publicNotesLoadFailed = computed(
() => !publicNotesLoading.value && publicNotes.value.length === 0
)
const formatPublicDate = (iso: string) =>
new Date(iso).toLocaleDateString(undefined, {
month: "short",
day: "numeric"
})
const publicNoteHandle = (did: string) => {
const handle = getPublicAuthor(did)
return handle ? `@${handle}` : toShortDid(did)
}
const publicNoteRoute = (note: { did: string; rkey: string; title: string }) => ({
name: "PublicNoteView",
params: {
shortDid: toShortDid(note.did),
rkey: note.rkey,
slug: slugify(note.title)
}
})
onMounted(() => {
loadPublicNotes()
})
</script>
<template>
<div class="welcome-world">
<!-- Top Nav -->
<nav class="topnav">
<div class="topnav-left">
<router-link :to="{ name: 'Home' }" class="brand">
<img
src="/favicon.png"
alt="Remanso"
class="brand-mark"
width="28"
height="28"
/>
<span class="brand-word">Remanso</span>
</router-link>
</div>
<div class="topnav-right">
<a href="#about" class="navlink">About</a>
<router-link :to="{ name: 'PublicNoteListView' }" class="navlink"
>Public&nbsp;notes</router-link
>
<router-link
:to="{
name: 'FluxNoteView',
params: { user: 'remanso-space', repo: 'getting-started' }
}"
class="navlink"
>Getting&nbsp;started</router-link
>
<UserPill @click="openProfile" />
</div>
</nav>
<!-- Main content -->
<main>
<!-- =========== LOGGED-IN: Launchpad ============== -->
<template v-if="isAnyUserLoggedIn">
<section class="li-hero">
<div class="li-hero-inner">
<div>
<div class="eyebrow mono">the pool is calm today</div>
<h1 class="li-h1">{{ greeting }}</h1>
<p class="li-sub">
<span class="mono">{{ today }}</span>
<span class="li-dot">·</span>
<router-link
:to="{ name: 'PublicNoteListView' }"
class="li-sub-link"
>Public notes</router-link
>
</p>
</div>
<div class="li-quick">
<div class="li-quick-label mono">jump to a repo</div>
<form class="gh-form compact" @submit.prevent="submit">
<label class="input gh-input-group">
<span class="gh-prefix mono">github/</span>
<input
v-model="userInput"
placeholder="user"
class="gh-input"
spellcheck="false"
autocapitalize="off"
/>
<span class="gh-slash">/</span>
<input
v-model="repoInput"
placeholder="repo"
class="gh-input"
spellcheck="false"
autocapitalize="off"
/>
</label>
<button type="submit" class="hw-btn hw-btn-pink gh-go">
</button>
</form>
</div>
</div>
</section>
<section class="li-main">
<div class="li-grid">
<!-- Left: Repos -->
<div>
<div class="section-label mono" style="margin-top: 0">
§ your repos
</div>
<div
v-if="savedFavoriteRepos.length"
class="favs-grid"
:class="{ 'single-col': savedFavoriteRepos.length === 1 }"
>
<a
v-for="(repo, i) in savedFavoriteRepos"
:key="repo._id"
class="repo-tile"
:class="{
'size-xl': i === 0 && savedFavoriteRepos.length > 1,
'size-lg': i > 0 || savedFavoriteRepos.length === 1,
featured: i === 0 && savedFavoriteRepos.length > 1
}"
:style="tileStyle(`${repo.name}-${username}`)"
href="#"
@click.prevent="openRepo(String(username), repo.name)"
>
<div class="tile-top">
<span class="tile-owner mono">{{ username }}/</span>
<span class="tile-name">{{ repo.name }}</span>
</div>
</a>
<router-link
:to="{ name: 'RepoList' }"
class="add-tile"
title="Manage repos"
>
<span class="plus">+</span>
<span>manage repos</span>
</router-link>
</div>
<div v-else class="empty-repos">
<p>No repos saved yet.</p>
<router-link :to="{ name: 'RepoList' }" class="hw-btn">
Add a repo
</router-link>
<div class="section-label mono" style="margin-top: 1.5rem">
or open one directly
</div>
<form class="gh-form" @submit.prevent="submit">
<label class="input gh-input-group">
<span class="gh-prefix mono">github/</span>
<input
v-model="userInput"
placeholder="user"
class="gh-input"
spellcheck="false"
autocapitalize="off"
/>
<span class="gh-slash">/</span>
<input
v-model="repoInput"
placeholder="repo"
class="gh-input"
spellcheck="false"
autocapitalize="off"
/>
</label>
<button type="submit" class="hw-btn hw-btn-pink gh-go">
Open
</button>
</form>
</div>
</div>
<!-- Right: Sidebar -->
<aside>
<div
v-if="lastVisitedRepos.length"
class="section-label mono"
style="margin-top: 0"
>
§ last visited
</div>
<ul v-if="lastVisitedRepos.length" class="recent-list">
<li
v-for="repo in lastVisitedRepos.slice(0, 5)"
:key="`${repo.user}-${repo.repo}`"
>
<router-link
:to="{
name: 'FluxNoteView',
params: { user: repo.user, repo: repo.repo }
}"
>
<span class="r-path mono"
>{{ repo.user }} / {{ repo.repo }}</span
>
</router-link>
</li>
</ul>
<template v-if="showReviewCard">
<div class="section-label mono">§ due for review</div>
<div class="due-card">
<div class="due-count">{{ cardsToReview.length }}</div>
<div>
<div class="due-t">Cards waiting</div>
<div class="due-d">Spaced repetition</div>
</div>
<router-link
:to="{
name: 'NeedReviewCards',
params: {
user: username,
repo: reviewRepo!.name
}
}"
class="hw-btn hw-btn-pink due-btn"
>
Review
</router-link>
</div>
</template>
<div class="section-label mono">§ drifting in</div>
<ul
v-if="hasPublicNotes"
class="network-side-list"
>
<li
v-for="note in sidebarPublicNotes"
:key="`${note.did}-${note.rkey}`"
>
<router-link
:to="publicNoteRoute(note)"
class="network-side-link"
>
<span class="network-side-title">{{ note.title }}</span>
<span class="mono network-side-meta">
{{ publicNoteHandle(note.did) }}
<span class="li-dot">·</span>
{{ formatPublicDate(note.publishedAt) }}
</span>
</router-link>
</li>
</ul>
<ul
v-else-if="publicNotesLoading"
class="network-side-list"
aria-hidden="true"
>
<li v-for="i in 3" :key="i" class="network-side-skel">
<span class="skel skel-title" />
<span class="skel skel-date" />
</li>
</ul>
<router-link
:to="{ name: 'PublicNoteListView' }"
class="network-side-all"
>
See all public notes
</router-link>
</aside>
</div>
</section>
</template>
<!-- =========== Editorial (always visible) ============== -->
<!-- Hero -->
<section class="hero-ed">
<div class="hero-ed-inner">
<div class="hero-ed-left">
<div class="eyebrow mono">a quiet place for your notes</div>
<h1 class="display">
Remanso is where your<br />
<em>scattered thinking</em><br />
comes to rest.
</h1>
<p class="lede">
Point it at any markdown a GitHub repo of your own, or public
notes from the open ATProto network and Remanso turns it into a
calm, stackable notebook with backlinks where your thinking
finally runs clear.
</p>
<div class="hero-ed-paths">
<!-- CTA 01: GitHub repo -->
<div class="hero-ed-path">
<div class="hep-head">
<span class="hep-n mono">01</span>
<div>
<div class="hep-t">From a GitHub repo</div>
<div class="hep-d">
Point it at your markdown. Public works instantly, private
via OAuth.
</div>
</div>
</div>
<form class="gh-form" @submit.prevent="submit">
<label class="input gh-input-group">
<span class="gh-prefix mono">github/</span>
<input
v-model="userInput"
placeholder="user"
class="gh-input"
spellcheck="false"
autocapitalize="off"
/>
<span class="gh-slash">/</span>
<input
v-model="repoInput"
placeholder="repo"
class="gh-input"
spellcheck="false"
autocapitalize="off"
/>
</label>
<button type="submit" class="hw-btn hw-btn-pink gh-go">
</button>
</form>
</div>
<!-- CTA 02: Public notes (live preview) -->
<div class="hero-ed-path hero-ed-path-network">
<div class="hep-head">
<span class="hep-n mono">02</span>
<div>
<div class="hep-t">From the open network</div>
<div class="hep-d">
Live notes published via ATProto. No account needed
tap one and read.
</div>
</div>
</div>
<div
v-if="hasPublicNotes"
class="network-strip"
role="list"
>
<router-link
v-for="note in recentPublicNotes"
:key="`${note.did}-${note.rkey}`"
:to="publicNoteRoute(note)"
class="network-card"
role="listitem"
>
<span class="mono network-card-handle">{{
publicNoteHandle(note.did)
}}</span>
<span class="network-card-title">{{ note.title }}</span>
<span class="mono network-card-date">{{
formatPublicDate(note.publishedAt)
}}</span>
</router-link>
</div>
<div
v-else-if="publicNotesLoading"
class="network-strip"
aria-hidden="true"
>
<div
v-for="i in 3"
:key="i"
class="network-card network-card--skel"
>
<span class="skel skel-handle" />
<span class="skel skel-title" />
<span class="skel skel-date" />
</div>
</div>
<div v-else-if="publicNotesLoadFailed" class="network-empty">
The pool is quiet right now.
</div>
<router-link
:to="{ name: 'PublicNoteListView' }"
class="network-all"
>
Browse all public notes
</router-link>
</div>
</div>
<div class="hero-ed-sample mono">
new here?
<router-link
:to="{
name: 'FluxNoteView',
params: { user: 'remanso-space', repo: 'getting-started' }
}"
>
try remanso-space/getting-started
</router-link>
</div>
</div>
<div class="hero-ed-right">
<div class="flower-stage">
<img
src="/favicon.png"
alt="Remanso"
class="flower-mark"
width="240"
height="240"
/>
<div class="flower-ripple" />
<div class="flower-ripple r2" />
</div>
</div>
</div>
</section>
<!-- Manifesto -->
<section class="manifesto" id="about">
<div class="manifesto-inner">
<div class="section-label mono">§ 01 what is remanso</div>
<p class="drop-cap manifesto-p">
<em>Remanso</em> is a Portuguese word for the still pool that forms
where a river slows. The current still moves through but for a
moment, it is calm enough to see the bottom. This is what notes
should feel like. A Remanso takes your markdown files and lays them
out as a linked notebook you can read, follow, and add to. No
lock-in. Your notes stay in Git.
</p>
<div class="feature-row">
<div class="feat">
<span class="feat-icon" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 4l-8 4l8 4l8 -4l-8 -4" />
<path d="M4 12l8 4l8 -4" />
<path d="M4 16l8 4l8 -4" />
</svg>
</span>
<h4>Stacked notes</h4>
<p>
Click a link and the next note slides in beside the current one.
Your train of thought stays visible never lose where you came
from.
</p>
</div>
<div class="feat">
<span class="feat-icon" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 14l-4 -4l4 -4" />
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
</svg>
</span>
<h4>Automatic backlinks</h4>
<p>
Every mention becomes a two-way link. See every note that points
back at the one you're reading.
</p>
</div>
<div class="feat">
<span class="feat-icon" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M11 8a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M12 15v-6" />
<path d="M15 11l-2 -2" />
<path d="M11 7l-1.9 -1.9" />
<path
d="M13.446 2.6l7.955 7.954a2.045 2.045 0 0 1 0 2.892l-7.955 7.955a2.045 2.045 0 0 1 -2.892 0l-7.955 -7.955a2.045 2.045 0 0 1 0 -2.892l7.955 -7.955a2.045 2.045 0 0 1 2.892 0"
/>
</svg>
</span>
<h4>Your files, your Git</h4>
<p>
Remanso reads GitHub directly. Edit in your favourite editor.
Ship to Bluesky via ATProto when you're ready to share.
</p>
</div>
</div>
</div>
</section>
<!-- Demo notes -->
<section class="demo" id="getting-started">
<div class="demo-inner">
<div class="section-label mono">§ 02 a note, in full</div>
<h2 class="h2">
This is what a note looks like.<br />
<span class="h2-quiet italic">
Tap a link to stack another beside it.
</span>
</h2>
<div class="demo-notes">
<article
class="note-preview"
:class="{ 'note-preview--flash': firstHighlighted }"
>
<div class="note-header">
<span class="mono note-breadcrumb"
>remanso-space / getting-started</span
>
</div>
<h3 class="note-title">On keeping notes</h3>
<div class="note-body">
<p>
A note should be small enough to hold one idea and
<a class="note-link" href="#" @click.prevent="flashOffset"
>durable enough</a
>
to outlive the day it was written.
</p>
<p>
Luhmann called this the
<a class="note-link" href="#" @click.prevent="toggleZettel"
>zettelkasten</a
>: a slip-box of thoughts you converse with, rather than a
hoard of pages you re-read.
</p>
</div>
<div class="note-backlinks">
<div class="backlinks-h mono"> linked from</div>
<ul>
<li>
<a @click.prevent href="#"
>why links are better than folders</a
>
</li>
<li><a @click.prevent href="#">a reading diary</a></li>
</ul>
</div>
</article>
<div class="note-stack">
<article
class="note-preview note-preview-offset"
:class="{ 'note-preview--flash': offsetHighlighted }"
>
<div class="note-header">
<span class="mono note-breadcrumb"
>remanso-space / getting-started</span
>
</div>
<h3 class="note-title">Durable enough</h3>
<div class="note-body">
<p>
A durable note survives its own context. You should be able
to pick it up six months from now and still know what it
means.
</p>
<p>
Rule of thumb: write the title as the claim, and the body as
the argument.
</p>
</div>
<div class="note-backlinks">
<div class="backlinks-h mono"> linked from</div>
<ul>
<li>
<a @click.prevent="flashFirst" href="#"
>on keeping notes</a
>
</li>
</ul>
</div>
</article>
<article
class="note-preview note-preview-zettel"
:class="{ 'is-revealed': zettelRevealed }"
:aria-hidden="!zettelRevealed"
>
<div class="note-header">
<span class="mono note-breadcrumb"
>remanso-space / getting-started</span
>
</div>
<h3 class="note-title">Zettelkasten</h3>
<div class="note-body">
<p>
A <em>slip-box</em> of atomic notes wired together by links
instead of filed away in folders. Niklas Luhmann kept ninety
thousand of them in a wooden cabinet and wrote with them,
not just about them.
</p>
<p>
Each <em>Zettel</em> earns its keep by being linked to. Open
one, follow a thread, end up somewhere you didn't plan to
go.
</p>
</div>
<div class="note-backlinks">
<div class="backlinks-h mono">↖ linked from</div>
<ul>
<li><a @click.prevent href="#">on keeping notes</a></li>
</ul>
</div>
</article>
</div>
</div>
</div>
</section>
<!-- Zettelkasten primer -->
<section class="zk" id="zk">
<div class="zk-inner">
<div class="section-label mono">§ 03 — zettelkasten, in a minute</div>
<ol class="zk-steps">
<li>
<span class="zk-n mono">01</span>
<div>
<h4>One idea per note.</h4>
<p>Keep them small. A note that does one thing gets reused.</p>
</div>
</li>
<li>
<span class="zk-n mono">02</span>
<div>
<h4>Link, don't nest.</h4>
<p>
Folders calcify. Links compound. Every
<code>[link](note.md)</code> becomes a door to a new idea.
</p>
</div>
</li>
<li>
<span class="zk-n mono">03</span>
<div>
<h4>Let the web emerge.</h4>
<p>
Don't plan the structure. Write, link, re-read. Structure
emerges from what you return to.
</p>
</div>
</li>
</ol>
</div>
</section>
</main>
<!-- ── Footer ─────────────────────────────────────────── -->
<footer class="page-footer">
<div class="footer-inner">
<div class="footer-brand">
<img
src="/favicon.png"
alt="Remanso"
width="32"
height="32"
class="footer-mark"
/>
<div>
<div class="footer-title">Remanso</div>
<div class="footer-sub mono">a quiet pool for your notes</div>
</div>
</div>
<div class="footer-cols">
<div>
<div class="footer-h">Product</div>
<router-link
:to="{
name: 'FluxNoteView',
params: { user: 'remanso-space', repo: 'getting-started' }
}"
class="footer-link"
>Getting started</router-link
>
<router-link
:to="{ name: 'PublicNoteListView' }"
class="footer-link"
>Public notes</router-link
>
<router-link :to="{ name: 'RepoList' }" class="footer-link"
>Your repos</router-link
>
</div>
<div>
<div class="footer-h">Learn</div>
<a href="#about" class="footer-link">What is Remanso?</a>
<a href="#zk" class="footer-link">Zettelkasten</a>
<a
href="https://atproto.com/"
target="_blank"
rel="noreferrer"
class="footer-link"
>atproto</a
>
</div>
</div>
</div>
<div class="footer-fine">
<theme-swap />
<span>
made with <span style="color: var(--hw-pink-deep)"></span> by
<a
href="https://apoena.dev"
target="_blank"
rel="noreferrer"
class="footer-link"
>apoena.dev </a
>
</span>
</div>
</footer>
<ProfileModal />
</div>
</template>
<style lang="scss" scoped>
/* ── CSS variables ─────────────────────────────────────────── */
.welcome-world {
/* Derived from DaisyUI base tokens — adapts to any theme automatically.
base-100 is the lightest surface; base-200/300 are progressively deeper.
Paper uses base-200 so cards (base-100) appear slightly elevated above it. */
--hw-surface: var(--color-base-100);
--hw-paper: var(--color-base-200);
--hw-paper-warm: var(--color-base-300);
--hw-ink: var(--color-base-content);
--hw-ink-soft: color-mix(
in oklch,
var(--color-base-content) 65%,
var(--color-base-200)
);
--hw-ink-faint: color-mix(
in oklch,
var(--color-base-content) 38%,
var(--color-base-200)
);
--hw-rule: color-mix(
in oklch,
var(--color-base-content) 12%,
var(--color-base-200)
);
/* Brand accent — intentionally fixed, not derived from the active theme */
--hw-pink: #e36598;
--hw-pink-deep: color-mix(in oklch, #e36598 75%, var(--color-base-content));
--hw-pink-wash: color-mix(in oklch, #e36598 12%, var(--color-base-200));
--hw-pink-wash-2: color-mix(in oklch, #e36598 22%, var(--color-base-200));
--hw-leaf: #6b8e4e;
--hw-serif: "Libertinus Serif", "Iowan Old Style", "Palatino", serif;
--hw-mono: "Courier Prime", "IBM Plex Mono", ui-monospace, monospace;
font-family: var(--hw-serif);
background: var(--hw-paper);
color: var(--hw-ink);
font-size: 18px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100dvh;
display: flex;
flex-direction: column;
position: relative;
}
.welcome-world::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(
circle at 20% 10%,
rgba(227, 101, 152, 0.04),
transparent 40%
),
radial-gradient(
circle at 90% 80%,
rgba(107, 142, 78, 0.035),
transparent 45%
);
}
main {
position: relative;
flex: 1;
}
/* ── Shared button system ───────────────────────────────────── */
.hw-btn {
font-family: var(--hw-serif);
font-size: 16px;
padding: 0.55rem 1rem;
border-radius: 2px;
border: 1px solid var(--hw-ink);
background: transparent;
color: var(--hw-ink);
cursor: pointer;
transition:
background 0.15s,
color 0.15s,
transform 0.1s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
&:hover {
background: var(--hw-ink);
color: var(--hw-paper);
}
&:active {
transform: translateY(1px);
}
}
.hw-btn-primary {
background: var(--hw-ink);
color: var(--hw-paper);
&:hover {
background: var(--hw-pink-deep);
border-color: var(--hw-pink-deep);
}
}
.hw-btn-pink {
background: var(--hw-pink);
border-color: var(--hw-pink);
color: #fff;
&:hover {
background: var(--hw-pink-deep);
border-color: var(--hw-pink-deep);
}
}
/* ── Utility classes ────────────────────────────────────────── */
.mono {
font-family: var(--hw-mono);
}
.italic {
font-style: italic;
}
.drop-cap::first-letter {
font-family: var(--hw-serif);
float: left;
font-size: 4.6em;
line-height: 0.85;
font-weight: 600;
margin: 0.1em 0.1em 0 0;
color: var(--hw-pink-deep);
}
/* ── Top nav ────────────────────────────────────────────────── */
.topnav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 2rem;
border-bottom: 1px solid var(--hw-rule);
background: var(--hw-paper);
position: sticky;
top: 0;
z-index: 20;
backdrop-filter: saturate(1.1) blur(6px);
}
.brand {
display: flex;
align-items: center;
gap: 0.55rem;
text-decoration: none;
color: var(--hw-ink);
}
.brand-mark {
display: block;
object-fit: contain;
view-transition-name: remanso-logo;
}
.brand-word {
font-size: 1.25rem;
letter-spacing: 0.02em;
font-weight: 600;
}
.topnav-right {
display: flex;
align-items: center;
gap: 1.25rem;
}
.navlink {
text-decoration: none;
color: var(--hw-ink-soft);
font-size: 0.95rem;
transition: color 0.15s;
&:hover {
color: var(--hw-pink-deep);
}
}
/* ── GitHub form ────────────────────────────────────────────── */
.gh-form {
display: flex;
align-items: stretch;
&.compact {
.gh-input-group {
padding: 0.15rem 0.35rem 0.15rem 0.75rem;
}
.gh-go {
padding: 0.3rem 0.7rem;
font-size: 0.85rem;
}
}
}
.gh-input-group {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.35rem 0.45rem 0.35rem 0.9rem;
background: var(--hw-surface);
border: 1px solid var(--hw-rule);
border-right: none;
border-radius: 999px 0 0 999px;
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.02),
0 6px 18px -10px rgba(201, 74, 125, 0.15);
cursor: text;
transition:
border-color 0.15s,
box-shadow 0.15s;
height: auto;
outline: none;
&:focus-within {
border-color: var(--hw-pink);
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.02),
0 10px 28px -10px rgba(201, 74, 125, 0.25);
outline: none;
}
}
.gh-prefix {
color: var(--hw-ink-faint);
font-size: 0.85rem;
user-select: none;
}
.gh-slash {
color: var(--hw-ink-faint);
font-family: var(--hw-mono);
}
.gh-input {
flex: 1;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
font-family: var(--hw-mono);
font-size: 0.95rem;
color: var(--hw-ink);
padding: 0;
&::placeholder {
color: var(--hw-ink-faint);
font-style: italic;
}
}
.gh-go {
flex-shrink: 0;
padding: 0.4rem 0.9rem;
font-size: 0.9rem;
border-radius: 0 999px 999px 0;
align-self: stretch;
display: flex;
align-items: center;
}
/* ── Hero (logged out) ──────────────────────────────────────── */
.hero-ed {
padding: 4.5rem 2rem 3rem;
}
.hero-ed-inner {
max-width: 1180px;
margin: 0 auto;
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 4rem;
align-items: center;
}
.eyebrow {
font-size: 0.78rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--hw-pink-deep);
margin-bottom: 1.5rem;
}
.display {
font-family: var(--hw-serif);
font-size: clamp(2.5rem, 5.5vw, 4.6rem);
line-height: 1.02;
letter-spacing: -0.01em;
margin: 0 0 1.75rem;
font-weight: 600;
text-wrap: balance;
em {
font-style: italic;
font-weight: 400;
color: var(--hw-pink-deep);
}
}
.lede {
font-size: 1.18rem;
line-height: 1.55;
color: var(--hw-ink-soft);
max-width: 36ch;
margin: 0 0 2rem;
text-wrap: pretty;
}
.hero-ed-paths {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 0.5rem;
}
.hero-ed-path {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 2.25rem 2rem;
background: var(--hw-surface);
border: 1px solid var(--hw-rule);
border-radius: 6px;
transition:
border-color 0.15s,
box-shadow 0.2s,
transform 0.15s;
&:hover {
border-color: var(--hw-pink-wash-2);
box-shadow: 0 18px 30px -22px rgba(201, 74, 125, 0.18);
transform: translateY(-1px);
}
}
.hero-ed-path-network {
gap: 1.25rem;
&:hover {
transform: none;
border-color: var(--hw-rule);
box-shadow: none;
}
}
.network-strip {
display: flex;
gap: 0.85rem;
overflow-x: auto;
padding: 0.25rem 0.25rem 0.75rem;
margin: 0 -0.25rem;
scroll-snap-type: x proximity;
scrollbar-width: thin;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: var(--hw-pink-wash-2);
border-radius: 999px;
}
}
.network-card {
flex: 0 0 220px;
scroll-snap-align: start;
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 0.9rem 1rem 0.85rem;
background: var(--hw-paper);
border: 1px solid var(--hw-rule);
border-radius: 4px;
text-decoration: none;
color: var(--hw-ink);
position: relative;
min-height: 130px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
transition:
transform 0.15s ease,
border-color 0.15s ease,
box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
border-color: var(--hw-pink-wash-2);
box-shadow: 0 14px 24px -18px rgba(201, 74, 125, 0.35);
.network-card-title {
color: var(--hw-pink-deep);
}
}
}
.network-card-handle {
font-size: 0.72rem;
color: var(--hw-ink-faint);
letter-spacing: 0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.network-card-title {
font-family: var(--hw-serif);
font-size: 1rem;
font-weight: 600;
line-height: 1.3;
color: var(--hw-ink);
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color 0.15s ease;
}
.network-card-date {
font-size: 0.7rem;
color: var(--hw-ink-faint);
letter-spacing: 0.04em;
text-transform: lowercase;
margin-top: auto;
}
.network-card--skel {
pointer-events: none;
}
.skel {
display: block;
border-radius: 3px;
background: linear-gradient(
90deg,
var(--hw-pink-wash) 0%,
var(--hw-pink-wash-2) 50%,
var(--hw-pink-wash) 100%
);
background-size: 200% 100%;
animation: skel-shimmer 1.4s ease-in-out infinite;
}
.skel-handle {
height: 0.6rem;
width: 60%;
}
.skel-title {
height: 0.95rem;
width: 90%;
margin-top: 0.1rem;
}
.skel-date {
height: 0.55rem;
width: 30%;
margin-top: auto;
}
@keyframes skel-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.network-empty {
font-size: 0.9rem;
color: var(--hw-ink-faint);
font-style: italic;
padding: 0.5rem 0;
}
.network-all {
align-self: flex-start;
font-family: var(--hw-mono);
font-size: 0.82rem;
letter-spacing: 0.04em;
color: var(--hw-pink-deep);
text-decoration: none;
border-bottom: 1px dashed var(--hw-pink-wash-2);
padding-bottom: 1px;
&:hover {
color: var(--hw-pink-deep);
border-bottom-color: var(--hw-pink);
}
}
.hep-head {
display: flex;
gap: 0.85rem;
align-items: flex-start;
}
.hep-n {
font-size: 0.85rem;
letter-spacing: 0.12em;
color: var(--hw-pink-deep);
padding-top: 0.35rem;
}
.hep-t {
font-family: var(--hw-serif);
font-size: 1.45rem;
font-weight: 600;
line-height: 1.2;
margin-bottom: 0.4rem;
}
.hep-d {
font-size: 1.05rem;
color: var(--hw-ink-soft);
line-height: 1.55;
text-wrap: pretty;
}
.hep-btn {
align-self: flex-start;
padding: 0.65rem 1.25rem;
font-size: 1.05rem;
}
.hero-ed-sample {
margin-top: 1.1rem;
font-size: 0.8rem;
color: var(--hw-ink-faint);
a {
color: var(--hw-ink-soft);
text-decoration: underline;
text-decoration-color: var(--hw-rule);
&:hover {
color: var(--hw-pink-deep);
}
}
}
.hero-ed-left {
min-width: 0;
}
.hero-ed-right {
display: flex;
justify-content: center;
}
.flower-stage {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.flower-mark {
position: relative;
z-index: 2;
display: block;
object-fit: contain;
}
.flower-ripple {
position: absolute;
inset: 10% 10%;
border: 4px solid var(--hw-pink-wash-2);
border-radius: 50%;
animation: ripple 4.5s ease-out infinite;
&.r2 {
animation-delay: 1.8s;
}
}
@keyframes ripple {
0% {
transform: scale(0.6);
opacity: 0;
}
30% {
opacity: 0.6;
}
100% {
transform: scale(1.35);
opacity: 0;
}
}
/* ── Manifesto ──────────────────────────────────────────────── */
.manifesto {
padding: 2rem 2rem 3rem;
border-top: 1px solid var(--hw-rule);
}
.manifesto-inner {
max-width: 760px;
margin: 0 auto;
}
.section-label {
font-size: 0.72rem;
letter-spacing: 0.18em;
color: var(--hw-ink-faint);
text-transform: lowercase;
margin: 2.75rem 0 1.5rem;
}
.manifesto-p {
font-size: 1.25rem;
line-height: 1.6;
color: var(--hw-ink);
text-wrap: pretty;
margin: 0;
em {
font-style: italic;
color: var(--hw-pink-deep);
}
}
.feature-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2.25rem;
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--hw-rule);
}
.feat {
h4 {
font-size: 1.05rem;
margin: 0.6rem 0 0.35rem;
font-weight: 600;
}
p {
font-size: 0.95rem;
color: var(--hw-ink-soft);
line-height: 1.55;
margin: 0;
text-wrap: pretty;
}
}
.feat-icon {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--hw-pink-deep);
border: 1px solid var(--hw-pink);
border-radius: 50%;
svg {
width: 16px;
height: 16px;
}
}
/* ── Demo notes ─────────────────────────────────────────────── */
.demo {
padding: 2rem 2rem 4rem;
background: linear-gradient(
180deg,
transparent,
color-mix(in srgb, var(--hw-pink-wash) 35%, transparent) 40%,
transparent
);
}
.demo-inner {
max-width: 1080px;
margin: 0 auto;
}
.h2 {
font-family: var(--hw-serif);
font-size: clamp(1.8rem, 3.2vw, 2.6rem);
line-height: 1.15;
font-weight: 600;
margin: 0 0 2.5rem;
letter-spacing: -0.005em;
}
.h2-quiet {
font-weight: 400;
color: var(--hw-ink-soft);
}
.demo-notes {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
}
.note-preview {
background: var(--hw-surface);
border: 1px solid var(--hw-rule);
border-radius: 4px;
padding: 1.5rem 1.75rem 1.25rem;
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.02),
0 20px 40px -25px rgba(0, 0, 0, 0.15);
max-width: 420px;
font-size: 0.95rem;
line-height: 1.6;
}
.note-preview-offset {
margin-top: 2.5rem;
transform: rotate(0.4deg);
transition:
box-shadow 0.15s ease,
outline-color 0.15s ease;
outline: 2px solid transparent;
outline-offset: 3px;
}
.note-preview--flash {
animation: note-flash 0.9s ease forwards;
}
.note-stack {
position: relative;
display: flex;
align-items: flex-start;
}
.note-preview-zettel {
position: absolute;
inset: 0;
margin-top: 2.5rem;
transform: rotate(-0.6deg) translate(0, 0) scale(0.97);
opacity: 0;
pointer-events: none;
z-index: -1;
transition:
transform 0.4s cubic-bezier(0.2, 0.7, 0.2, 1),
opacity 0.3s ease;
&.is-revealed {
transform: rotate(-2deg) translate(28px, 28px) scale(0.97);
opacity: 1;
pointer-events: auto;
}
}
@media (max-width: 640px) {
.note-preview-zettel {
&.is-revealed {
transform: rotate(-1.5deg) translate(12px, 22px) scale(0.96);
}
}
}
@keyframes note-flash {
0% {
box-shadow:
0 0 0 3px var(--hw-pink-wash-2),
0 1px 0 rgba(0, 0, 0, 0.02),
0 20px 40px -25px rgba(0, 0, 0, 0.15);
}
25% {
box-shadow:
0 0 0 6px var(--hw-pink-wash-2),
0 1px 0 rgba(0, 0, 0, 0.02),
0 20px 40px -25px rgba(0, 0, 0, 0.15);
}
100% {
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.02),
0 20px 40px -25px rgba(0, 0, 0, 0.15);
}
}
.note-header {
margin-bottom: 0.5rem;
}
.note-breadcrumb {
font-size: 0.72rem;
color: var(--hw-ink-faint);
letter-spacing: 0.02em;
}
.note-title {
font-family: var(--hw-serif);
font-size: 1.4rem;
margin: 0.3rem 0 0.8rem;
font-weight: 600;
line-height: 1.2;
}
.note-body {
color: var(--hw-ink-soft);
p {
margin: 0 0 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
.note-link {
color: var(--hw-pink-deep);
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 3px;
}
.note-backlinks {
margin-top: 1.2rem;
padding-top: 0.9rem;
border-top: 1px dashed var(--hw-rule);
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
padding: 0.15rem 0;
}
a {
color: var(--hw-ink-soft);
text-decoration: none;
font-size: 0.9rem;
font-style: italic;
&:hover {
color: var(--hw-pink-deep);
}
}
}
.backlinks-h {
font-size: 0.7rem;
color: var(--hw-ink-faint);
letter-spacing: 0.1em;
text-transform: lowercase;
margin-bottom: 0.4rem;
}
/* ── Zettelkasten section ───────────────────────────────────── */
.zk {
padding: 2rem 2rem 3rem;
border-top: 1px solid var(--hw-rule);
}
.zk-inner {
max-width: 760px;
margin: 0 auto;
}
.zk-steps {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 1.75rem;
li {
display: grid;
grid-template-columns: auto 1fr;
gap: 1.5rem;
padding: 1rem 0;
border-bottom: 1px dashed var(--hw-rule);
&:last-child {
border-bottom: 0;
}
}
h4 {
font-family: var(--hw-serif);
font-size: 1.3rem;
margin: 0 0 0.35rem;
font-weight: 600;
}
p {
margin: 0;
color: var(--hw-ink-soft);
font-size: 1rem;
line-height: 1.55;
}
code {
font-family: var(--hw-mono);
background: var(--hw-pink-wash);
color: var(--hw-pink-deep);
padding: 0.05em 0.35em;
border-radius: 3px;
font-size: 0.9em;
}
}
.zk-n {
font-size: 0.95rem;
color: var(--hw-pink-deep);
letter-spacing: 0.1em;
padding-top: 0.25rem;
}
img {
box-shadow: none;
}
/* ── Logged-in hero ─────────────────────────────────────────── */
.li-hero {
padding: 3rem 2rem 1.5rem;
}
.li-hero-inner {
max-width: 1180px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 2rem;
flex-wrap: wrap;
}
.li-h1 {
font-family: var(--hw-serif);
font-size: clamp(2rem, 4vw, 3.2rem);
font-weight: 600;
margin: 0.5rem 0 0.75rem;
letter-spacing: -0.005em;
}
.li-sub {
margin: 0;
color: var(--hw-ink-soft);
font-size: 0.95rem;
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
align-items: center;
}
.li-dot {
color: var(--hw-ink-faint);
}
.li-sub-link {
color: var(--hw-ink-soft);
text-decoration: underline;
text-decoration-color: var(--hw-rule);
&:hover {
color: var(--hw-pink-deep);
}
}
.li-quick {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.4rem;
}
.li-quick-label {
font-size: 0.72rem;
letter-spacing: 0.14em;
color: var(--hw-ink-faint);
text-transform: lowercase;
}
/* ── Logged-in main ─────────────────────────────────────────── */
.li-main {
padding: 1rem 2rem 3rem;
}
.li-grid {
max-width: 1180px;
margin: 0 auto;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 3rem;
}
.favs-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
> :first-child.featured {
grid-column: span 2;
grid-row: span 2;
}
&.single-col {
grid-template-columns: 1fr 1fr;
}
}
.repo-tile {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1rem 1.1rem;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.04);
transition:
transform 0.15s ease,
box-shadow 0.2s ease;
min-height: 110px;
position: relative;
overflow: hidden;
text-decoration: none;
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.15);
}
&.size-xl {
min-height: 160px;
.tile-name {
font-size: 1.8rem;
}
}
}
.tile-top {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.tile-owner {
font-size: 0.75rem;
opacity: 0.65;
}
.tile-name {
font-size: 1.3rem;
font-weight: 600;
line-height: 1.1;
}
.add-tile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.3rem;
border: 1px dashed var(--hw-rule);
border-radius: 6px;
color: var(--hw-ink-faint);
text-decoration: none;
min-height: 110px;
font-size: 0.9rem;
transition: all 0.15s;
font-family: var(--hw-serif);
&:hover {
border-color: var(--hw-pink);
color: var(--hw-pink-deep);
background: var(--hw-pink-wash);
}
}
.plus {
font-size: 1.8rem;
font-weight: 400;
line-height: 1;
}
.empty-repos {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
p {
color: var(--hw-ink-soft);
margin: 0;
font-size: 1rem;
}
}
/* ── Sidebar lists ──────────────────────────────────────────── */
.recent-list {
list-style: none;
padding: 0;
margin: 0;
li {
border-bottom: 1px dashed var(--hw-rule);
&:last-child {
border-bottom: 0;
}
}
a {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.75rem;
padding: 0.7rem 0;
text-decoration: none;
color: var(--hw-ink);
&:hover .r-path {
color: var(--hw-pink-deep);
}
}
}
.r-path {
font-size: 0.82rem;
color: var(--hw-ink-soft);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.due-card {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.9rem;
align-items: center;
padding: 1rem 1.1rem;
background: var(--hw-pink-wash);
border: 1px solid var(--hw-pink-wash-2);
border-radius: 6px;
margin-bottom: 1rem;
}
.due-count {
font-family: var(--hw-serif);
font-size: 2.4rem;
font-weight: 600;
color: var(--hw-pink-deep);
line-height: 1;
}
.due-t {
font-weight: 600;
font-size: 0.95rem;
}
.due-d {
font-size: 0.85rem;
color: var(--hw-ink-soft);
}
.due-btn {
font-size: 0.85rem;
padding: 0.35rem 0.8rem;
}
.network-side-list {
list-style: none;
padding: 0;
margin: 0 0 0.75rem;
li {
border-bottom: 1px dashed var(--hw-rule);
&:last-child {
border-bottom: 0;
}
}
}
.network-side-link {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding: 0.65rem 0;
text-decoration: none;
color: var(--hw-ink);
transition: color 0.15s ease;
&:hover {
.network-side-title {
color: var(--hw-pink-deep);
}
}
}
.network-side-title {
font-family: var(--hw-serif);
font-size: 0.95rem;
font-weight: 600;
line-height: 1.3;
color: var(--hw-ink);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color 0.15s ease;
}
.network-side-meta {
font-size: 0.72rem;
color: var(--hw-ink-faint);
display: flex;
gap: 0.4rem;
align-items: center;
letter-spacing: 0.02em;
}
.network-side-skel {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 0.65rem 0;
}
.network-side-all {
display: inline-block;
margin-top: 0.25rem;
font-family: var(--hw-mono);
font-size: 0.78rem;
letter-spacing: 0.04em;
color: var(--hw-pink-deep);
text-decoration: none;
border-bottom: 1px dashed var(--hw-pink-wash-2);
padding-bottom: 1px;
&:hover {
border-bottom-color: var(--hw-pink);
}
}
/* ── Footer ─────────────────────────────────────────────────── */
.page-footer {
border-top: 1px solid var(--hw-rule);
padding: 3.5rem 2rem 2rem;
margin-top: 5rem;
background: linear-gradient(180deg, transparent, var(--hw-paper-warm));
position: relative;
}
.footer-inner {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 2fr;
gap: 3rem;
}
.footer-brand {
display: flex;
gap: 0.75rem;
align-items: center;
}
.footer-mark {
display: block;
object-fit: contain;
}
.footer-title {
font-size: 1.1rem;
font-weight: 600;
}
.footer-sub {
font-size: 0.8rem;
color: var(--hw-ink-faint);
letter-spacing: 0.03em;
}
.footer-cols {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.footer-h {
font-family: var(--hw-mono);
text-transform: uppercase;
font-size: 0.72rem;
letter-spacing: 0.15em;
color: var(--hw-ink-faint);
margin-bottom: 0.75rem;
}
.footer-link {
display: block;
text-decoration: none;
color: var(--hw-ink-soft);
padding: 0.25rem 0;
font-size: 0.95rem;
&:hover {
color: var(--hw-pink-deep);
}
}
.footer-fine {
max-width: 1100px;
margin: 2.5rem auto 0;
padding-top: 1.5rem;
border-top: 1px dashed var(--hw-rule);
display: flex;
gap: 0.75rem;
align-items: center;
justify-content: center;
font-family: var(--hw-mono);
font-size: 0.8rem;
color: var(--hw-ink-faint);
}
.footer-dot {
color: var(--hw-ink-faint);
}
/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 900px) {
.hero-ed-inner {
grid-template-columns: 1fr;
gap: 2rem;
}
.hero-ed-right {
order: -1;
}
.flower-mark {
width: 160px;
height: 160px;
}
.feature-row {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.li-grid {
grid-template-columns: 1fr;
}
.favs-grid {
grid-template-columns: repeat(2, 1fr);
> :first-child.featured {
grid-column: span 2;
grid-row: auto;
}
}
.footer-inner {
grid-template-columns: 1fr;
gap: 2rem;
}
.footer-cols {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 640px) {
.topnav {
padding: 1rem 1.25rem;
}
.navlink {
display: none;
}
.hero-ed {
padding: 3rem 1.25rem 2rem;
}
.li-hero {
padding: 2rem 1.25rem 1rem;
}
.li-hero-inner {
flex-direction: column;
align-items: flex-start;
}
.li-quick {
align-items: flex-start;
width: 100%;
}
.li-main,
.manifesto,
.demo,
.zk,
.page-footer {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
}
@media (max-width: 520px) {
.footer-cols {
grid-template-columns: 1fr 1fr;
}
.favs-grid {
grid-template-columns: 1fr;
> :first-child.featured {
grid-column: span 1;
}
}
}
</style>