+
diff --git a/app/styles/global.css b/app/styles/global.css
index 25d8103..e8c3a7c 100644
--- a/app/styles/global.css
+++ b/app/styles/global.css
@@ -1,11 +1,25 @@
html {
color-scheme: light;
background: var(--c-paper);
+ /*
+ * Height of the sticky nav, which AppHeader pins to the row it draws. Also
+ * the offset anything sticky below it has to clear -- the table head, the
+ * character on a detail page -- and the room the browser leaves when it
+ * scrolls to a target of its own.
+ */
+ --nav-h: 4rem;
+ scroll-padding-top: var(--nav-h);
}
html.dark {
color-scheme: dark;
}
+@media (min-width: 768px) {
+ html {
+ --nav-h: 4.5rem;
+ }
+}
+
body {
font-family: var(--font-ui);
/* Spaces between Han and Latin/digits are the renderer's job, so the copy
@@ -38,13 +52,8 @@ body {
}
/*
- * The five table columns, pinned to their own region and never following the
- * interface language. scripts/build-fonts.ts generates the @font-face rules
- * into /fonts/kanji.css, chunked by unicode-range so the browser fetches only
- * the chunks holding characters currently on screen.
- */
-/*
- * The five Han columns. The fallback is deliberately a generic family and
+ * The five Han columns, pinned to their own region and never following the
+ * interface language. The fallback is deliberately a generic family and
* never --font-ui: listing a webfont here would make every table character a
* candidate for it, and the browser would pull dozens of that font's
* unicode-range chunks. The lang attribute on each cell drives the system's
diff --git a/app/utils/glyph-group.ts b/app/utils/glyph-group.ts
new file mode 100644
index 0000000..83339be
--- /dev/null
+++ b/app/utils/glyph-group.ts
@@ -0,0 +1,25 @@
+import type { Column } from '~~/shared/types.ts'
+
+/**
+ * Accent for a glyph group. The accents encode grouping and never region
+ * identity, so the same group number always reaches for the same variable
+ * whether it is drawn as a stacked layer, a run under a row, or a segment in
+ * a chip. A caller with only one group has nothing to encode and picks its own
+ * neutral instead.
+ */
+export const groupColor = (group: number) => `var(--c-g${group + 1})`
+
+/**
+ * BCP-47 tag per column. This is accessibility semantics, unrelated to the
+ * interface language; it also drives the system's own CJK fallback when a
+ * subset has not loaded. The kyujitai is Japan's own, so it is tagged as
+ * Japanese too.
+ */
+export const COLUMN_LANG: Record = {
+ cn: 'zh-CN',
+ hk: 'zh-HK',
+ tw: 'zh-TW',
+ jp: 'ja',
+ kr: 'ko',
+ old: 'ja',
+}
diff --git a/uno.config.ts b/uno.config.ts
index 6a8adf8..5928b85 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -7,6 +7,19 @@ import {
export default defineConfig({
theme: {
+ /**
+ * `xs` is the width at which the nav still has room for the wordmark
+ * beside the title -- a large phone rather than a small one. Everything
+ * above it is the preset's own scale.
+ */
+ breakpoints: {
+ xs: '380px',
+ sm: '640px',
+ md: '768px',
+ lg: '1024px',
+ xl: '1280px',
+ '2xl': '1536px',
+ },
colors: {
ink: 'var(--c-ink)',
'ink-soft': 'var(--c-ink-soft)',
@@ -36,8 +49,45 @@ export default defineConfig({
'flex-center': 'flex items-center justify-center',
// Small label, in the register of a spec document
eyebrow: 'font-mono text-[0.6875rem] tracking-[0.04em] text-mute',
+
+ /*
+ * These name a recurring control rather than a one-off run of utilities.
+ * None of them carries `focus-ring`: that class is written by hand in
+ * global.css, and folding it into a shortcut would leave the element with
+ * only the shortcut's name for the hand-written rule to match. Every
+ * focusable element keeps `focus-ring` beside the shortcut.
+ */
+
+ // A square icon-only control, the size of a comfortable touch target
+ 'icon-btn':
+ 'size-8 flex-center rounded-md text-mute transition-colors duration-150 hover:text-ink',
+ // A quiet outlined button
+ 'btn-ghost':
+ 'border border-rule rounded-md text-soft transition-colors duration-150 hover:border-ink/25 hover:text-ink',
+ // Paging, where a disabled button must not answer to hover
+ 'btn-pager':
+ 'border border-rule rounded-md px-3 py-1.5 text-soft transition-colors duration-150 disabled:opacity-35 enabled:hover:border-ink/25 enabled:hover:text-ink',
+ /**
+ * A filter chip. Selection is a filled slab rather than a border, so which
+ * chips are on reads at a glance across the whole bar. Callers add the gap
+ * and type size their own contents need.
+ */
+ chip: 'h-7 flex items-center border rounded-md px-2 transition duration-150',
+ 'chip-on': 'border-$c-ink bg-$c-ink text-$c-paper',
+ 'chip-off': 'border-rule bg-paper text-soft hover:border-ink/30',
+ // An underline in the register of the page's hairlines. Named rule-first:
+ // presetWind3 reads a leading `link-` as the :link variant.
+ 'rule-link':
+ 'underline decoration-rule underline-offset-4 hover:decoration-ink',
},
+ /**
+ * `focus-ring` is a hand-written class (see global.css), but the preset also
+ * reads it as `focus:ring` and was quietly painting a second, blue ring on
+ * every mouse click. Nothing wants that utility here.
+ */
+ blocklist: ['focus-ring'],
+
presets: [
presetWind3(),
presetIcons({