docs.g1t.sh and status.g1t.sh follow your system's light or dark setting, and each has an Auto, Light and Dark switch that this browser remembers: the docs' in the header (at the foot of the menu on a phone), with code, search, the API reference and its explorer in both themes; the status page's in its header, chosen before anything is drawn; the workspaces guide says where.
11 files+346−450/11 viewed
| 29 | 29 | // Each tab under the header shows its own part of the sidebar. | |
| 30 | 30 | routeMiddleware: './src/route-data.ts', | |
| 31 | 31 | expressiveCode: { | |
| 32 | − | themes: ['github-dark-default'], | |
| 32 | + | // Dark first, as the base; the light one where the page is light. | |
| 33 | + | // Starlight switches between them by <html data-theme>. | |
| 34 | + | themes: ['github-dark-default', 'github-light-default'], | |
| 35 | + | useDarkModeMediaQuery: false, | |
| 33 | 36 | // Plain panels with a copy button, without window chrome. | |
| 34 | 37 | defaultProps: { frame: 'none' }, | |
| 35 | 38 | styleOverrides: { | |
| ⋯ | |||
| 49 | 52 | }, | |
| 50 | 53 | logo: { src: '@g1t/theme/mark.svg', alt: '' }, | |
| 51 | 54 | favicon: '/favicon.svg', | |
| 52 | − | customCss: ['@g1t/theme/fonts.css', '@g1t/theme/tokens.css', './src/styles/g1t.css'], | |
| 55 | + | customCss: [ | |
| 56 | + | '@g1t/theme/fonts.css', | |
| 57 | + | '@g1t/theme/tokens.css', | |
| 58 | + | './src/styles/light.css', | |
| 59 | + | './src/styles/g1t.css', | |
| 60 | + | ], | |
| 53 | 61 | editLink: { | |
| 54 | 62 | baseUrl: 'https://g1t.sh/flagon-io/g1t/blob/main/apps/docs/', | |
| 55 | 63 | }, | |
| ⋯ | |||
| 60 | 68 | { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '512x512', href: '/icon-512.png' } }, | |
| 61 | 69 | { tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' } }, | |
| 62 | 70 | { tag: 'link', attrs: { rel: 'manifest', href: '/site.webmanifest' } }, | |
| 63 | − | { tag: 'meta', attrs: { name: 'theme-color', content: '#0f0f11' } }, | |
| 71 | + | // The browser's own bars, in the page's colour for the system's setting. | |
| 72 | + | { tag: 'meta', attrs: { name: 'theme-color', content: '#0f0f11', media: '(prefers-color-scheme: dark)' } }, | |
| 73 | + | { tag: 'meta', attrs: { name: 'theme-color', content: '#fbfbfa', media: '(prefers-color-scheme: light)' } }, | |
| 64 | 74 | ], | |
| 65 | 75 | sidebar: [ | |
| 66 | 76 | { | |
| 7 | 7 | // and when it does not. Here the row is three columns, the outer two always | |
| 8 | 8 | // the same width, so the search sits at the header's centre on every page. | |
| 9 | 9 | import Search from '@astrojs/starlight/components/Search.astro'; | |
| 10 | + | // Auto, Light or Dark, kept in this browser; Auto follows the system. | |
| 11 | + | // On tablets and phones it is at the foot of the menu instead. | |
| 12 | + | import ThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro'; | |
| 10 | 13 | ||
| 11 | 14 | import { TABS, tabFor } from '../tabs'; | |
| 12 | 15 | import SiteTitle from './SiteTitle.astro'; | |
| ⋯ | |||
| 19 | 22 | <div class="g1t-header-top"> | |
| 20 | 23 | <div class="g1t-header-title"><SiteTitle /></div> | |
| 21 | 24 | <div class="g1t-header-search print:hidden"><Search /></div> | |
| 22 | − | <div class="g1t-header-links print:hidden"><SocialIcons /></div> | |
| 25 | + | <div class="g1t-header-links print:hidden"> | |
| 26 | + | <SocialIcons /> | |
| 27 | + | <div class="g1t-header-theme"><ThemeSelect /></div> | |
| 28 | + | </div> | |
| 23 | 29 | </div> | |
| 24 | 30 | <nav class="g1t-tabs" aria-label="Sections"> | |
| 25 | 31 | { | |
| ⋯ | |||
| 70 | 76 | } | |
| 71 | 77 | .g1t-header-links { | |
| 72 | 78 | display: flex; | |
| 79 | + | align-items: center; | |
| 73 | 80 | justify-content: flex-end; | |
| 81 | + | gap: 0.75rem; | |
| 74 | 82 | min-width: 0; | |
| 75 | 83 | } | |
| 84 | + | .g1t-header-theme { | |
| 85 | + | display: flex; | |
| 86 | + | align-items: center; | |
| 87 | + | padding-inline-start: 0.75rem; | |
| 88 | + | border-inline-start: 1px solid var(--g1t-line); | |
| 89 | + | height: 1.25rem; | |
| 90 | + | } | |
| 91 | + | /* Desktop: the links and the theme select together are wider. On a | |
| 92 | + | narrow desktop the select shows only its icon (styles/g1t.css), so the | |
| 93 | + | search keeps its room. */ | |
| 94 | + | @media (min-width: 50rem) { | |
| 95 | + | .g1t-header-top { | |
| 96 | + | --g1t-header-side: 17.5rem; | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | @media (min-width: 64rem) { | |
| 100 | + | .g1t-header-top { | |
| 101 | + | --g1t-header-side: 21rem; | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | /* Tablets and phones: the theme select is at the foot of the menu. */ | |
| 105 | + | @media (max-width: 49.99rem) { | |
| 106 | + | .g1t-header-theme { | |
| 107 | + | display: none; | |
| 108 | + | } | |
| 109 | + | } | |
| 76 | 110 | /* Tablets: the sidebar is a menu, and its button (fixed in place by | |
| 77 | 111 | Starlight) takes the row's end. The right column makes room for it | |
| 78 | 112 | inside itself, so the two columns stay equal and the search centred. */ | |
| 168 | 168 | background: var(--g1t-raised); | |
| 169 | 169 | box-shadow: | |
| 170 | 170 | inset 0 0 0 1px var(--g1t-line-strong), | |
| 171 | − | 0 12px 32px rgb(0 0 0 / 0.45); | |
| 171 | + | var(--g1t-docs-menu-shadow); | |
| 172 | 172 | } | |
| 173 | 173 | /* Its own spacing: the page's prose rules stop at not-content, and | |
| 174 | 174 | nothing inside keeps a margin of its own. */ |
| 45 | 45 | font-weight: 500; | |
| 46 | 46 | } | |
| 47 | 47 | nav.g1t-nav .g1t-nav-cta:hover { | |
| 48 | − | background: #fff; | |
| 48 | + | background: var(--g1t-docs-fg-hover); | |
| 49 | 49 | color: var(--g1t-bg); | |
| 50 | 50 | } | |
| 51 | 51 | nav.g1t-nav .g1t-nav-icon { |
| 589 | 589 | Product screenshots on g1t.sh's home page, and the g1t mark on agents' | |
| 590 | 590 | avatars, stay dark in either theme. | |
| 591 | 591 | ||
| 592 | + | These docs and [status.g1t.sh](https://status.g1t.sh/) also follow your | |
| 593 | + | system's setting, each with its own switch for this browser: here, the | |
| 594 | + | **Auto**, **Light** and **Dark** menu at the end of the header (at the foot | |
| 595 | + | of the menu on a phone); on the status page, the three icons at the top. | |
| 596 | + | ||
| 592 | 597 | ### Apps | |
| 593 | 598 | ||
| 594 | 599 | Every other app in the workspace is a page you can pin to your dock: |
| 4 | 4 | // layout. The reference itself is the Starlight pages under /reference/api/. | |
| 5 | 5 | import '@g1t/theme/fonts.css'; | |
| 6 | 6 | import '@g1t/theme/tokens.css'; | |
| 7 | + | import '../../styles/light.css'; | |
| 7 | 8 | import sans from '@g1t/theme/fonts/hanken-grotesk-latin.woff2?url'; | |
| 8 | 9 | ||
| 9 | 10 | // The wordmark: G1T in pixel capitals, the 1 in lavender, as in | |
| ⋯ | |||
| 15 | 16 | rows.flatMap((row, y) => [...row].flatMap((c, x) => (c === '#' ? [{ x: dx + x, y, one }] : []))); | |
| 16 | 17 | const word = [...pixels(G, 0, false), ...pixels(ONE, 6, true), ...pixels(T, 10, false)]; | |
| 17 | 18 | ||
| 19 | + | // Light or dark as the rest of the docs: the head's script below sets | |
| 20 | + | // forceDarkModeState from the reader's choice before the explorer loads. | |
| 18 | 21 | const configuration = { | |
| 19 | 22 | theme: 'none', | |
| 20 | 23 | darkMode: true, | |
| ⋯ | |||
| 41 | 44 | <link rel="manifest" href="/site.webmanifest" /> | |
| 42 | 45 | <link rel="canonical" href="https://docs.g1t.sh/api/reference/" /> | |
| 43 | 46 | <link rel="preload" href={sans} as="font" type="font/woff2" crossorigin /> | |
| 47 | + | <meta name="theme-color" content="#0f0f11" media="(prefers-color-scheme: dark)" /> | |
| 48 | + | <meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme: light)" /> | |
| 49 | + | {/* The theme chosen in the docs' theme select (kept in this browser by | |
| 50 | + | Starlight), or the system's on Auto; set before the first paint. */} | |
| 51 | + | <script is:inline> | |
| 52 | + | (() => { | |
| 53 | + | let stored = ''; | |
| 54 | + | try { | |
| 55 | + | stored = localStorage.getItem('starlight-theme') || ''; | |
| 56 | + | } catch {} | |
| 57 | + | const theme = | |
| 58 | + | stored === 'light' || stored === 'dark' | |
| 59 | + | ? stored | |
| 60 | + | : matchMedia('(prefers-color-scheme: light)').matches | |
| 61 | + | ? 'light' | |
| 62 | + | : 'dark'; | |
| 63 | + | document.documentElement.dataset.theme = theme; | |
| 64 | + | window.g1tTheme = theme; | |
| 65 | + | })(); | |
| 66 | + | </script> | |
| 44 | 67 | </head> | |
| 45 | 68 | <body> | |
| 46 | 69 | <header class="bar"> | |
| ⋯ | |||
| 71 | 94 | id="api-reference" | |
| 72 | 95 | data-url="https://api.g1t.sh/openapi.json" | |
| 73 | 96 | data-configuration={JSON.stringify(configuration)}></script> | |
| 97 | + | <script is:inline> | |
| 98 | + | (() => { | |
| 99 | + | const el = document.getElementById('api-reference'); | |
| 100 | + | const config = JSON.parse(el.dataset.configuration); | |
| 101 | + | config.darkMode = window.g1tTheme !== 'light'; | |
| 102 | + | config.forceDarkModeState = window.g1tTheme === 'light' ? 'light' : 'dark'; | |
| 103 | + | el.dataset.configuration = JSON.stringify(config); | |
| 104 | + | })(); | |
| 105 | + | </script> | |
| 74 | 106 | <script is:inline src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script> | |
| 75 | 107 | </body> | |
| 76 | 108 | </html> | |
| ⋯ | |||
| 88 | 120 | --scalar-font: var(--g1t-font-sans); | |
| 89 | 121 | --scalar-font-code: var(--g1t-font-mono); | |
| 90 | 122 | } | |
| 91 | − | .dark-mode { | |
| 123 | + | .dark-mode, | |
| 124 | + | .light-mode { | |
| 92 | 125 | --scalar-background-1: var(--g1t-bg); | |
| 93 | 126 | --scalar-background-2: var(--g1t-surface); | |
| 94 | 127 | --scalar-background-3: var(--g1t-raised); | |
| ⋯ | |||
| 96 | 129 | --scalar-color-1: var(--g1t-fg); | |
| 97 | 130 | --scalar-color-2: var(--g1t-muted); | |
| 98 | 131 | --scalar-color-3: var(--g1t-faint); | |
| 99 | − | --scalar-color-accent: #cfc6ff; | |
| 132 | + | --scalar-color-accent: var(--g1t-docs-link); | |
| 100 | 133 | --scalar-background-accent: color-mix(in srgb, var(--g1t-accent) 12%, transparent); | |
| 101 | 134 | --scalar-button-1: var(--g1t-fg); | |
| 102 | 135 | --scalar-button-1-color: var(--g1t-bg); | |
| 103 | − | --scalar-button-1-hover: #ffffff; | |
| 136 | + | --scalar-button-1-hover: var(--g1t-docs-fg-hover); | |
| 104 | 137 | --scalar-color-green: var(--g1t-success); | |
| 105 | 138 | --scalar-color-blue: var(--g1t-info); | |
| 106 | 139 | --scalar-color-red: var(--g1t-danger); | |
| 1 | 1 | /* | |
| 2 | − | * g1t's docs: Starlight in g1t's own design. A very dark gray base, | |
| 3 | − | * lavender as the accent for links and the current page, quiet | |
| 4 | − | * typography, and one theme. | |
| 2 | + | * g1t's docs: Starlight in g1t's own design. A very dark gray base (or a | |
| 3 | + | * near-white one, in light: styles/light.css), lavender as the accent for | |
| 4 | + | * links and the current page, and quiet typography. Starlight's colours | |
| 5 | + | * are mapped onto the g1t tokens once, here, for both themes. | |
| 5 | 6 | */ | |
| 6 | − | :root, | |
| 7 | − | :root[data-theme='light'] { | |
| 7 | + | :root { | |
| 8 | 8 | --sl-font: var(--g1t-font-sans); | |
| 9 | 9 | --sl-font-mono: var(--g1t-font-mono); | |
| 10 | 10 | ||
| 11 | 11 | /* Lavender is the docs' accent: links, the current page, highlights. */ | |
| 12 | 12 | --sl-color-accent-low: color-mix(in srgb, var(--g1t-accent) 14%, var(--g1t-bg)); | |
| 13 | 13 | --sl-color-accent: var(--g1t-accent); | |
| 14 | − | --sl-color-accent-high: #d9d1ff; | |
| 14 | + | --sl-color-accent-high: var(--g1t-docs-accent-high); | |
| 15 | 15 | ||
| 16 | 16 | --sl-color-white: var(--g1t-fg); | |
| 17 | 17 | --sl-color-gray-1: var(--g1t-fg-soft); | |
| ⋯ | |||
| 30 | 30 | --sl-color-hairline-light: var(--g1t-line); | |
| 31 | 31 | --sl-color-hairline-shade: var(--g1t-line); | |
| 32 | 32 | --sl-color-text: var(--g1t-fg-soft); | |
| 33 | − | --sl-color-text-accent: #cfc6ff; | |
| 33 | + | --sl-color-text-accent: var(--g1t-docs-link); | |
| 34 | 34 | --sl-color-text-invert: var(--g1t-bg); | |
| 35 | 35 | --sl-color-bg-inline-code: var(--g1t-raised); | |
| 36 | 36 | ||
| ⋯ | |||
| 43 | 43 | --sl-sidebar-width: 17rem; | |
| 44 | 44 | /* Big enough to tap. */ | |
| 45 | 45 | --sl-menu-button-size: 2.5rem; | |
| 46 | + | } | |
| 46 | 47 | ||
| 47 | − | color-scheme: dark; | |
| 48 | + | /* Starlight's theme select (Auto, Light, Dark): in the header's links, and | |
| 49 | + | at the foot of the phone menu. Drawn like the links beside it. */ | |
| 50 | + | starlight-theme-select label { | |
| 51 | + | color: var(--g1t-muted); | |
| 52 | + | font-size: 0.85rem; | |
| 53 | + | } | |
| 54 | + | starlight-theme-select label:hover { | |
| 55 | + | color: var(--g1t-fg); | |
| 56 | + | } | |
| 57 | + | starlight-theme-select select { | |
| 58 | + | border-radius: 0.4rem; | |
| 48 | 59 | } | |
| 49 | − | ||
| 50 | − | /* There is one theme, so the switcher has nothing to switch. */ | |
| 51 | − | starlight-theme-select { | |
| 52 | − | display: none; | |
| 60 | + | starlight-theme-select option { | |
| 61 | + | background: var(--g1t-surface); | |
| 62 | + | color: var(--g1t-fg); | |
| 53 | 63 | } | |
| 54 | − | /* Starlight's divider between the links and the theme and language | |
| 55 | − | pickers; with neither shown, it is a line before nothing. */ | |
| 56 | − | .social-icons::after { | |
| 57 | − | display: none; | |
| 64 | + | /* A narrow desktop: in the header, only the select's icon and caret; the | |
| 65 | + | choice is still its accessible name and in the list it opens. */ | |
| 66 | + | @media (min-width: 50rem) and (max-width: 63.99rem) { | |
| 67 | + | .g1t-header-theme starlight-theme-select select { | |
| 68 | + | width: 3.625rem; | |
| 69 | + | color: transparent; | |
| 70 | + | } | |
| 58 | 71 | } | |
| 59 | 72 | ||
| 60 | 73 | /* --- Header --------------------------------------------------------------- */ | |
| ⋯ | |||
| 134 | 147 | color: var(--g1t-fg); | |
| 135 | 148 | } | |
| 136 | 149 | ||
| 150 | + | /* Search, in light: the dialog is a white panel, so its results sit on a | |
| 151 | + | step darker rather than on a white just off the panel's. */ | |
| 152 | + | :root[data-theme='light'] #starlight__search { | |
| 153 | + | --pagefind-ui-background: var(--g1t-surface); | |
| 154 | + | } | |
| 155 | + | :root[data-theme='light'] | |
| 156 | + | #starlight__search | |
| 157 | + | :is(.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)), .pagefind-ui__result-nested):not(:hover) { | |
| 158 | + | background-color: var(--g1t-raised); | |
| 159 | + | } | |
| 160 | + | ||
| 137 | 161 | /* --- Sidebar -------------------------------------------------------------- */ | |
| 138 | 162 | ||
| 139 | 163 | .sidebar-content { | |
| ⋯ | |||
| 230 | 254 | color: var(--g1t-fg-soft); | |
| 231 | 255 | } | |
| 232 | 256 | .sl-markdown-content a:not(.card, .sl-link-button, .sl-link-card) { | |
| 233 | − | color: #cfc6ff; | |
| 257 | + | color: var(--g1t-docs-link); | |
| 234 | 258 | text-decoration-color: color-mix(in srgb, var(--g1t-accent) 45%, transparent); | |
| 235 | 259 | text-underline-offset: 0.2em; | |
| 236 | 260 | } | |
| ⋯ | |||
| 338 | 362 | color: var(--g1t-fg); | |
| 339 | 363 | } | |
| 340 | 364 | .right-sidebar-panel a[aria-current='true'] { | |
| 341 | − | color: #cfc6ff; | |
| 365 | + | color: var(--g1t-docs-link); | |
| 342 | 366 | } | |
| 343 | 367 | ||
| 344 | 368 | /* --- Cards and buttons ---------------------------------------------------- */ | |
| ⋯ | |||
| 365 | 389 | color: var(--g1t-bg); | |
| 366 | 390 | } | |
| 367 | 391 | .sl-link-button.primary:hover { | |
| 368 | − | background: #fff; | |
| 392 | + | background: var(--g1t-docs-fg-hover); | |
| 369 | 393 | } | |
| 370 | 394 | .sl-link-button.minimal { | |
| 371 | 395 | color: var(--g1t-fg-soft); | |
| ⋯ | |||
| 472 | 496 | border: 0 !important; | |
| 473 | 497 | border-radius: 0.3rem; | |
| 474 | 498 | padding: 0.05rem 0.4rem; | |
| 475 | − | background: color-mix(in srgb, var(--method) 14%, transparent) !important; | |
| 499 | + | background: color-mix(in srgb, var(--method) var(--g1t-docs-tag-tint), transparent) !important; | |
| 476 | 500 | color: var(--method) !important; | |
| 477 | 501 | font-family: var(--g1t-font-mono); | |
| 478 | 502 | font-size: 0.7rem; | |
| 1 | + | /* | |
| 2 | + | * The docs' two themes. Dark is g1t's own very dark gray; light swaps the | |
| 3 | + | * shared --g1t-* tokens for their light values (--g1t-light-* in | |
| 4 | + | * packages/theme/tokens.css, the same palette as the app's Light | |
| 5 | + | * appearance), so every rule that reads a token follows without a light | |
| 6 | + | * rule of its own. | |
| 7 | + | * | |
| 8 | + | * Starlight sets data-theme on <html> before the first paint: the reader's | |
| 9 | + | * choice in its theme select (Auto, Light or Dark, kept in this browser), | |
| 10 | + | * and on Auto their system's setting. The API explorer | |
| 11 | + | * (src/pages/api/reference.astro) sets it the same way. | |
| 12 | + | * | |
| 13 | + | * The few colours the docs use that have no shared token are here as | |
| 14 | + | * --g1t-docs-*, dark first. | |
| 15 | + | */ | |
| 16 | + | :root { | |
| 17 | + | /* Link text: a paler lavender than the accent on dark. */ | |
| 18 | + | --g1t-docs-link: #cfc6ff; | |
| 19 | + | /* Text on a faint accent tint. */ | |
| 20 | + | --g1t-docs-accent-high: #d9d1ff; | |
| 21 | + | /* A primary button's hover: brighter than fg on dark, softer on light. */ | |
| 22 | + | --g1t-docs-fg-hover: #ffffff; | |
| 23 | + | /* The shadow under a menu that floats over the page. */ | |
| 24 | + | --g1t-docs-menu-shadow: 0 12px 32px rgb(0 0 0 / 0.45); | |
| 25 | + | /* How much of an API method's colour tints its tag. */ | |
| 26 | + | --g1t-docs-tag-tint: 14%; | |
| 27 | + | color-scheme: dark; | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | :root[data-theme='light'] { | |
| 31 | + | --g1t-bg: var(--g1t-light-bg); | |
| 32 | + | --g1t-surface: var(--g1t-light-surface); | |
| 33 | + | --g1t-raised: var(--g1t-light-raised); | |
| 34 | + | --g1t-line: var(--g1t-light-line); | |
| 35 | + | --g1t-line-strong: var(--g1t-light-line-strong); | |
| 36 | + | ||
| 37 | + | --g1t-fg: var(--g1t-light-fg); | |
| 38 | + | --g1t-fg-soft: var(--g1t-light-fg-soft); | |
| 39 | + | --g1t-muted: var(--g1t-light-muted); | |
| 40 | + | --g1t-faint: var(--g1t-light-faint); | |
| 41 | + | ||
| 42 | + | --g1t-accent: var(--g1t-light-accent); | |
| 43 | + | --g1t-accent-hover: var(--g1t-light-accent-hover); | |
| 44 | + | --g1t-accent-dim: var(--g1t-light-accent-dim); | |
| 45 | + | --g1t-accent-low: var(--g1t-light-accent-low); | |
| 46 | + | ||
| 47 | + | --g1t-success: var(--g1t-light-success); | |
| 48 | + | --g1t-success-dim: var(--g1t-light-success-dim); | |
| 49 | + | --g1t-success-low: var(--g1t-light-success-low); | |
| 50 | + | ||
| 51 | + | --g1t-info: var(--g1t-light-info); | |
| 52 | + | --g1t-merged: var(--g1t-light-merged); | |
| 53 | + | --g1t-warn: var(--g1t-light-warn); | |
| 54 | + | --g1t-danger: var(--g1t-light-danger); | |
| 55 | + | ||
| 56 | + | /* The accent itself as link text: 5.5:1 on the page. */ | |
| 57 | + | --g1t-docs-link: var(--g1t-light-accent); | |
| 58 | + | --g1t-docs-accent-high: #4b36a6; | |
| 59 | + | --g1t-docs-fg-hover: #3a3a41; | |
| 60 | + | --g1t-docs-menu-shadow: 0 12px 32px rgb(24 24 27 / 0.12); | |
| 61 | + | /* Lighter, so each method's text keeps 4.5:1 on its tag. */ | |
| 62 | + | --g1t-docs-tag-tint: 9%; | |
| 63 | + | color-scheme: light; | |
| 64 | + | } |
| 15 | 15 | * GET / the page | |
| 16 | 16 | * GET /status.json the same as JSON (snake_case, CORS open) | |
| 17 | 17 | * GET /badge.svg a small badge | |
| 18 | + | * GET /theme.js, /status.js the page's scripts: its theme before paint; hover detail and the theme switch | |
| 18 | 19 | * GET /incidents/<id> an incident's updates and postmortem | |
| 19 | 20 | * GET /maintenance/<id> a maintenance window's updates | |
| 20 | 21 | * GET /history the last 12 months, by month | |
| ⋯ | |||
| 105 | 106 | import { | |
| 106 | 107 | FAVICON, | |
| 107 | 108 | SCRIPT, | |
| 109 | + | THEME_SCRIPT, | |
| 108 | 110 | type PageOptions, | |
| 109 | 111 | renderBadge, | |
| 110 | 112 | renderHistory, | |
| ⋯ | |||
| 663 | 665 | case "/subscribe/confirm": | |
| 664 | 666 | case "/unsubscribe": | |
| 665 | 667 | return (await subscriptions(request, env, ctx, url, options))!; | |
| 668 | + | case "/theme.js": | |
| 669 | + | return new Response(THEME_SCRIPT, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": fresh(3600), ...COMMON } }); | |
| 666 | 670 | case "/status.js": | |
| 667 | 671 | return new Response(SCRIPT, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": fresh(3600), ...COMMON } }); | |
| 668 | 672 | case "/favicon.svg": | |
| 5 | 5 | ||
| 6 | 6 | import { components } from "./components.ts"; | |
| 7 | 7 | import { type DayRow, buildPage } from "./model.ts"; | |
| 8 | − | import { byMonth, dayDetail, escape, prose, renderBadge, renderHistory, renderIncident, renderMaintenance, renderMessage, renderPage, renderSubscribe } from "./render.ts"; | |
| 8 | + | import { | |
| 9 | + | THEME_KEY, | |
| 10 | + | THEME_SCRIPT, | |
| 11 | + | byMonth, | |
| 12 | + | dayDetail, | |
| 13 | + | escape, | |
| 14 | + | prose, | |
| 15 | + | renderBadge, | |
| 16 | + | renderHistory, | |
| 17 | + | renderIncident, | |
| 18 | + | renderMaintenance, | |
| 19 | + | renderMessage, | |
| 20 | + | renderPage, | |
| 21 | + | renderSubscribe, | |
| 22 | + | } from "./render.ts"; | |
| 9 | 23 | ||
| 10 | 24 | const NOW = new Date("2026-10-05T12:00:00Z"); | |
| 11 | 25 | const OPTIONS = { siteUrl: "https://g1t.sh", supportUrl: "https://g1t.sh/support", ogImage: "", selfUrl: "https://status.g1t.sh/", now: NOW, email: true }; | |
| ⋯ | |||
| 89 | 103 | assert.ok(html.includes('href="/incidents/abc"'), "the title links to its page"); | |
| 90 | 104 | }); | |
| 91 | 105 | ||
| 106 | + | test("every page follows the system's light or dark setting, with a switch kept in this browser", () => { | |
| 107 | + | for (const html of [ | |
| 108 | + | renderPage(page({ site: "up" }), OPTIONS), | |
| 109 | + | renderHistory([], [], OPTIONS), | |
| 110 | + | renderMessage(OPTIONS, { title: "Not found", text: "Nothing here." }), | |
| 111 | + | ]) { | |
| 112 | + | const head = html.slice(0, html.indexOf("</head>")); | |
| 113 | + | // The choice is applied by a script that blocks in the head, before anything is drawn. | |
| 114 | + | assert.match(head, /<script src="\/theme\.js"><\/script>/); | |
| 115 | + | assert.match(head, /<meta name="color-scheme" content="dark light">/); | |
| 116 | + | assert.match(head, /theme-color" content="#fbfbfa" media="\(prefers-color-scheme: light\)"/); | |
| 117 | + | // Dark is the base; light on the system's say-so unless Dark was chosen, or when Light was. | |
| 118 | + | assert.match(head, /:root\{--bg:#0f0f11;/); | |
| 119 | + | assert.match(head, /@media \(prefers-color-scheme:light\)\{:root:not\(\[data-theme=dark\]\)\{--bg:#fbfbfa;/); | |
| 120 | + | assert.match(head, /:root\[data-theme=light\]\{--bg:#fbfbfa;/); | |
| 121 | + | for (const choice of ["auto", "light", "dark"]) assert.ok(html.includes(`data-theme-choice="${choice}"`), choice); | |
| 122 | + | } | |
| 123 | + | assert.ok(THEME_SCRIPT.includes(JSON.stringify(THEME_KEY))); | |
| 124 | + | assert.ok(!/<script>/.test(renderPage(page({ site: "up" }), OPTIONS)), "no inline script: the page's policy allows only its own files"); | |
| 125 | + | }); | |
| 126 | + | ||
| 127 | + | test("the theme script picks the stored choice, else auto", () => { | |
| 128 | + | for (const [stored, expected] of [["light", "light"], ["dark", "dark"], [null, "auto"], ["purple", "auto"]] as const) { | |
| 129 | + | const root = { dataset: {} as Record<string, string> }; | |
| 130 | + | const run = new Function("localStorage", "document", THEME_SCRIPT); | |
| 131 | + | run({ getItem: (key: string) => (key === THEME_KEY ? stored : null) }, { documentElement: root }); | |
| 132 | + | assert.equal(root.dataset.theme, expected, String(stored)); | |
| 133 | + | } | |
| 134 | + | // A browser that refuses storage still gets a page. | |
| 135 | + | const root = { dataset: {} as Record<string, string> }; | |
| 136 | + | new Function("localStorage", "document", THEME_SCRIPT)({ getItem: () => { throw new Error("denied"); } }, { documentElement: root }); | |
| 137 | + | assert.equal(root.dataset.theme, "auto"); | |
| 138 | + | }); | |
| 139 | + | ||
| 92 | 140 | test("a stale page says the checks stopped", () => { | |
| 93 | 141 | const model = page({ api: "up" }); | |
| 94 | 142 | const html = renderPage({ ...model, stale: true }, OPTIONS); | |
| 2 | 2 | * The pages at status.g1t.sh, drawn on the server as plain HTML: no | |
| 3 | 3 | * framework, a small stylesheet inline, and a few lines of script (served | |
| 4 | 4 | * at /status.js) that only add hover and keyboard detail to the bars and | |
| 5 | − | * say times in the browser's own zone. Without it, times are already in | |
| 5 | + | * say times in the browser's own zone, and the Auto, Light and Dark switch | |
| 6 | + | * (with /theme.js, which draws the reader's choice before the first | |
| 7 | + | * paint; on Auto, and with no script, the page follows the system's light | |
| 8 | + | * or dark setting). Without it, times are already in | |
| 6 | 9 | * the reader's zone as Cloudflare places them (PageOptions `zone`), with | |
| 7 | 10 | * the zone's abbreviation; `datetime` attributes and feeds stay in UTC. Every page works without it: | |
| 8 | 11 | * subscribing, confirming and leaving are plain forms. No Workers | |
| ⋯ | |||
| 119 | 122 | (row, y) => [...row].flatMap((c, x) => (c === "#" ? [`<rect x="${x * 2 + 3}" y="${y * 2 + 1}" width="2" height="2"/>`] : [])), | |
| 120 | 123 | ).join("")}</svg>`; | |
| 121 | 124 | ||
| 125 | + | /** Where this browser keeps the reader's theme: "light", "dark", or nothing for Auto. */ | |
| 126 | + | export const THEME_KEY = "g1t-status-theme"; | |
| 127 | + | ||
| 128 | + | /** | |
| 129 | + | * The page's theme before its first paint (served at /theme.js, loaded | |
| 130 | + | * without defer in the head): the reader's choice from this browser, or | |
| 131 | + | * "auto", which the stylesheet resolves by the system's setting. Without | |
| 132 | + | * scripts the page follows the system and the switch stays hidden. | |
| 133 | + | */ | |
| 134 | + | export const THEME_SCRIPT = `(() => { | |
| 135 | + | let theme = ""; | |
| 136 | + | try { theme = localStorage.getItem(${JSON.stringify(THEME_KEY)}) || ""; } catch {} | |
| 137 | + | document.documentElement.dataset.theme = theme === "light" || theme === "dark" ? theme : "auto"; | |
| 138 | + | })(); | |
| 139 | + | `; | |
| 140 | + | ||
| 141 | + | const ICON = (body: string) => | |
| 142 | + | `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`; | |
| 143 | + | ||
| 144 | + | const THEMES = [ | |
| 145 | + | { value: "auto", label: "Auto: follow the system", icon: ICON('<rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8M12 17v4"/>') }, | |
| 146 | + | { | |
| 147 | + | value: "light", | |
| 148 | + | label: "Light", | |
| 149 | + | icon: ICON('<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2m-7.07-17.07 1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>'), | |
| 150 | + | }, | |
| 151 | + | { value: "dark", label: "Dark", icon: ICON('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>') }, | |
| 152 | + | ]; | |
| 153 | + | ||
| 154 | + | /** Auto, Light and Dark, in the header; status.js keeps the choice in this browser. */ | |
| 155 | + | const THEME_SWITCH = `<div class="theme" role="group" aria-label="Theme">${THEMES.map( | |
| 156 | + | (t) => `<button type="button" data-theme-choice="${t.value}" aria-label="${t.label}" aria-pressed="false">${t.icon}</button>`, | |
| 157 | + | ).join("")}</div>`; | |
| 158 | + | ||
| 122 | 159 | function bar(component: StatusComponent, days: DayBar[]): string { | |
| 123 | 160 | if (component.check_state === "unmonitored" && component.uptime_90d == null) { | |
| 124 | 161 | return `<p class="unchecked">${escape(component.checks)}</p>`; | |
| ⋯ | |||
| 252 | 289 | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 253 | 290 | <title>${escape(page.title)}</title> | |
| 254 | 291 | <meta name="description" content="${escape(page.description)}"> | |
| 255 | − | <meta name="theme-color" content="#0f0f11"> | |
| 256 | − | <meta name="color-scheme" content="dark"> | |
| 292 | + | <meta name="theme-color" content="#0f0f11" media="(prefers-color-scheme: dark)"> | |
| 293 | + | <meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme: light)"> | |
| 294 | + | <meta name="color-scheme" content="dark light"> | |
| 295 | + | <script src="/theme.js"></script> | |
| 257 | 296 | <link rel="icon" href="/favicon.svg" type="image/svg+xml"> | |
| 258 | 297 | <link rel="canonical" href="${escape(options.selfUrl)}"> | |
| 259 | 298 | <link rel="alternate" type="application/json" href="/status.json"> | |
| ⋯ | |||
| 274 | 313 | <div class="page"> | |
| 275 | 314 | <header class="top"> | |
| 276 | 315 | <a class="brand" href="/" aria-label="g1t status">${LOGO}<span>Status</span></a> | |
| 316 | + | <div class="top-end"> | |
| 317 | + | ${THEME_SWITCH} | |
| 277 | 318 | <a class="out" href="${escape(site)}/">${escape(siteHost)} <span aria-hidden="true">→</span></a> | |
| 319 | + | </div> | |
| 278 | 320 | </header> | |
| 279 | 321 | <main> | |
| 280 | 322 | ${page.body} | |
| ⋯ | |||
| 530 | 572 | } | |
| 531 | 573 | times(); | |
| 532 | 574 | setInterval(times, 30000); | |
| 575 | + | // The theme switch: Auto, Light or Dark, kept in this browser (theme.js applies it before paint). | |
| 576 | + | const choices = [...document.querySelectorAll("[data-theme-choice]")]; | |
| 577 | + | function pressed() { | |
| 578 | + | const now = document.documentElement.dataset.theme || "auto"; | |
| 579 | + | for (const b of choices) b.setAttribute("aria-pressed", String(b.dataset.themeChoice === now)); | |
| 580 | + | } | |
| 581 | + | for (const b of choices) { | |
| 582 | + | b.addEventListener("click", () => { | |
| 583 | + | const theme = b.dataset.themeChoice; | |
| 584 | + | document.documentElement.dataset.theme = theme; | |
| 585 | + | try { | |
| 586 | + | if (theme === "auto") localStorage.removeItem(${JSON.stringify(THEME_KEY)}); | |
| 587 | + | else localStorage.setItem(${JSON.stringify(THEME_KEY)}, theme); | |
| 588 | + | } catch {} | |
| 589 | + | pressed(); | |
| 590 | + | }); | |
| 591 | + | } | |
| 592 | + | pressed(); | |
| 533 | 593 | for (const bar of document.querySelectorAll(".bar")) { | |
| 534 | 594 | const detail = bar.parentElement.querySelector(".detail"); | |
| 535 | 595 | const days = [...bar.children]; | |
| ⋯ | |||
| 565 | 625 | })(); | |
| 566 | 626 | `; | |
| 567 | 627 | ||
| 628 | + | /** | |
| 629 | + | * The light palette: g1t's light tokens (--g1t-light-* in | |
| 630 | + | * packages/theme/tokens.css), every text colour at least 4.5:1 on the | |
| 631 | + | * page and on a panel. The day bars take softer fills than the state | |
| 632 | + | * words, which are text. | |
| 633 | + | */ | |
| 634 | + | const LIGHT = `--bg:#fbfbfa;--surface:#ffffff;--raised:#f1f1ef;--line:#e4e4e1;--line-strong:#d2d2ce;--fg:#18181b;--soft:#2e2e33;--muted:#5c5c66;--faint:#686871; | |
| 635 | + | --up:#0d7a52;--warn:#a84f00;--down:#c42a43;--lav:#6b52c8;--lav-hover:#5b42b8;--info:#2160cc; | |
| 636 | + | --bar-up:#3a9d77;--bar-warn:#e08a3c;--bar-down:#d9475d;color-scheme:light`; | |
| 637 | + | ||
| 568 | 638 | const STYLE = ` | |
| 569 | 639 | @font-face{font-family:"Hanken Grotesk";font-weight:400 700;font-display:swap;src:url(/fonts/hanken-grotesk.woff2) format("woff2")} | |
| 570 | 640 | @font-face{font-family:"Bricolage Grotesque";font-weight:500 700;font-display:swap;src:url(/fonts/bricolage-grotesque.woff2) format("woff2")} | |
| 571 | 641 | @font-face{font-family:"IBM Plex Mono";font-weight:400;font-display:swap;src:url(/fonts/ibm-plex-mono.woff2) format("woff2")} | |
| 572 | 642 | :root{--bg:#0f0f11;--surface:#161618;--raised:#1e1e21;--line:#28282c;--line-strong:#38383e;--fg:#ededef;--soft:#dcdce0;--muted:#a0a0a8;--faint:#86868e; | |
| 573 | − | --up:#86efc4;--warn:#ffbd8c;--down:#ff8394;--lav:#b6a8ff;--info:#8ec5ff; | |
| 643 | + | --up:#86efc4;--warn:#ffbd8c;--down:#ff8394;--lav:#b6a8ff;--lav-hover:#c8bdff;--info:#8ec5ff; | |
| 644 | + | --bar-up:var(--up);--bar-warn:var(--warn);--bar-down:var(--down); | |
| 574 | 645 | --sans:"Hanken Grotesk",ui-sans-serif,system-ui,sans-serif;--display:"Bricolage Grotesque",ui-sans-serif,system-ui,sans-serif;--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color-scheme:dark} | |
| 646 | + | @media (prefers-color-scheme:light){:root:not([data-theme=dark]){${LIGHT}}} | |
| 647 | + | :root[data-theme=light]{${LIGHT}} | |
| 575 | 648 | *{box-sizing:border-box} | |
| 576 | 649 | html{-webkit-text-size-adjust:100%} | |
| 577 | 650 | body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 var(--sans);-webkit-font-smoothing:antialiased} | |
| ⋯ | |||
| 584 | 657 | .logo{height:22px;width:auto;display:block}.logo rect{fill:var(--fg)}.logo rect.one{fill:var(--lav)} | |
| 585 | 658 | .out{font-size:13px;color:var(--muted);text-decoration:none}.out:hover{color:var(--fg)} | |
| 586 | 659 | .banner{display:flex;gap:14px;align-items:flex-start;margin-top:32px;padding:20px 22px;border:1px solid var(--line);border-radius:14px;background:var(--surface)} | |
| 587 | − | .banner.up{border-color:rgba(134,239,196,.28);background:linear-gradient(0deg,rgba(134,239,196,.04),rgba(134,239,196,.04)),var(--surface)} | |
| 588 | − | .banner.degraded{border-color:rgba(255,189,140,.4);background:linear-gradient(0deg,rgba(255,189,140,.06),rgba(255,189,140,.06)),var(--surface)} | |
| 589 | − | .banner.down{border-color:rgba(255,131,148,.45);background:linear-gradient(0deg,rgba(255,131,148,.07),rgba(255,131,148,.07)),var(--surface)} | |
| 590 | − | .banner.maintenance{border-color:rgba(142,197,255,.4);background:linear-gradient(0deg,rgba(142,197,255,.06),rgba(142,197,255,.06)),var(--surface)} | |
| 660 | + | .banner.up{border-color:color-mix(in srgb,var(--up) 28%,transparent);background:linear-gradient(0deg,color-mix(in srgb,var(--up) 4%,transparent),color-mix(in srgb,var(--up) 4%,transparent)),var(--surface)} | |
| 661 | + | .banner.degraded{border-color:color-mix(in srgb,var(--warn) 40%,transparent);background:linear-gradient(0deg,color-mix(in srgb,var(--warn) 6%,transparent),color-mix(in srgb,var(--warn) 6%,transparent)),var(--surface)} | |
| 662 | + | .banner.down{border-color:color-mix(in srgb,var(--down) 45%,transparent);background:linear-gradient(0deg,color-mix(in srgb,var(--down) 7%,transparent),color-mix(in srgb,var(--down) 7%,transparent)),var(--surface)} | |
| 663 | + | .banner.maintenance{border-color:color-mix(in srgb,var(--info) 40%,transparent);background:linear-gradient(0deg,color-mix(in srgb,var(--info) 6%,transparent),color-mix(in srgb,var(--info) 6%,transparent)),var(--surface)} | |
| 591 | 664 | .banner h1{margin:0;font:600 26px/1.2 var(--display);letter-spacing:-.02em} | |
| 592 | 665 | .banner p{margin:6px 0 0;color:var(--muted);font-size:14px} | |
| 593 | 666 | .dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--faint);flex:none} | |
| 594 | − | .dot.big{width:12px;height:12px;margin-top:9px;box-shadow:0 0 0 4px rgba(134,239,196,.12)} | |
| 667 | + | .dot.big{width:12px;height:12px;margin-top:9px;box-shadow:0 0 0 4px color-mix(in srgb,var(--up) 12%,transparent)} | |
| 595 | 668 | .dot.up{background:var(--up)}.dot.degraded,.dot.partial{background:var(--warn)}.dot.down{background:var(--down)}.dot.maintenance{background:var(--info)}.dot.unmonitored,.dot.unknown{background:var(--faint)} | |
| 596 | − | .dot.big.degraded{box-shadow:0 0 0 4px rgba(255,189,140,.14)}.dot.big.down{box-shadow:0 0 0 4px rgba(255,131,148,.16)}.dot.big.maintenance{box-shadow:0 0 0 4px rgba(142,197,255,.14)}.dot.big.unknown{box-shadow:none} | |
| 669 | + | .dot.big.degraded{box-shadow:0 0 0 4px color-mix(in srgb,var(--warn) 14%,transparent)}.dot.big.down{box-shadow:0 0 0 4px color-mix(in srgb,var(--down) 16%,transparent)}.dot.big.maintenance{box-shadow:0 0 0 4px color-mix(in srgb,var(--info) 14%,transparent)}.dot.big.unknown{box-shadow:none} | |
| 597 | 670 | .block{margin-top:40px} | |
| 598 | 671 | .block h2,.month h2{margin:0 0 12px;font:600 13px/1.4 var(--sans);color:var(--muted);letter-spacing:.02em} | |
| 599 | 672 | .block-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px} | |
| ⋯ | |||
| 612 | 685 | .history{margin-top:12px} | |
| 613 | 686 | .bar{list-style:none;margin:0;padding:0;display:flex;gap:2px;height:30px} | |
| 614 | 687 | .bar li{flex:1 1 0;min-width:0;border-radius:2px;background:var(--line);transition:opacity .12s} | |
| 615 | − | .bar li.up{background:var(--up);opacity:.85}.bar li.degraded{background:var(--warn)}.bar li.down{background:var(--down)} | |
| 688 | + | .bar li.up{background:var(--bar-up);opacity:.85}.bar li.degraded{background:var(--bar-warn)}.bar li.down{background:var(--bar-down)} | |
| 616 | 689 | .bar:hover li,.bar:focus li{opacity:.55}.bar li.on,.bar:hover li:hover{opacity:1;transform:scaleY(1.08)} | |
| 617 | 690 | .legend{display:flex;justify-content:space-between;margin-top:6px;font-size:12px;color:var(--faint)} | |
| 618 | 691 | .legend .uptime{color:var(--muted)} | |
| 619 | 692 | .legend .short{display:none} | |
| 620 | 693 | .detail{margin:4px 0 0;min-height:18px;font-size:12px;color:var(--soft)} | |
| 621 | 694 | .incident,.card{border:1px solid var(--line);border-radius:14px;background:var(--surface);padding:16px 18px;margin-bottom:12px} | |
| 622 | − | .incident.open.degraded{border-color:rgba(255,189,140,.4)}.incident.open.down{border-color:rgba(255,131,148,.45)}.incident.maint{border-color:rgba(142,197,255,.35)} | |
| 695 | + | .incident.open.degraded{border-color:color-mix(in srgb,var(--warn) 40%,transparent)}.incident.open.down{border-color:color-mix(in srgb,var(--down) 45%,transparent)}.incident.maint{border-color:color-mix(in srgb,var(--info) 35%,transparent)} | |
| 623 | 696 | .incident header,.page-head{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:6px 12px} | |
| 624 | 697 | .incident h3{margin:0;font-size:15px;font-weight:600} | |
| 625 | 698 | .incident h3 a,.line-head a{text-decoration:none}.incident h3 a:hover,.line-head a:hover{text-decoration:underline;text-underline-offset:3px} | |
| ⋯ | |||
| 627 | 700 | .page-head h1{margin:0;font:600 24px/1.25 var(--display);letter-spacing:-.02em;overflow-wrap:anywhere} | |
| 628 | 701 | .crumbs{margin:24px 0 0;font-size:13px;color:var(--faint)}.crumbs a{color:var(--muted);text-decoration:none}.crumbs a:hover{color:var(--fg)} | |
| 629 | 702 | .pill{font-size:12px;font-weight:500;padding:1px 8px;border-radius:999px;border:1px solid var(--line-strong);color:var(--muted);white-space:nowrap} | |
| 630 | − | .pill.degraded{color:var(--warn);border-color:rgba(255,189,140,.4)}.pill.down{color:var(--down);border-color:rgba(255,131,148,.45)}.pill.maint{color:var(--info);border-color:rgba(142,197,255,.4)} | |
| 703 | + | .pill.degraded{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,transparent)}.pill.down{color:var(--down);border-color:color-mix(in srgb,var(--down) 45%,transparent)}.pill.maint{color:var(--info);border-color:color-mix(in srgb,var(--info) 40%,transparent)} | |
| 631 | 704 | .meta{margin:4px 0 0;font-size:12px;color:var(--faint)} | |
| 632 | 705 | .impacts{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px} | |
| 633 | 706 | .affects{font-size:12px;color:var(--muted)} | |
| ⋯ | |||
| 640 | 713 | .text ul{margin:4px 0 0;padding-left:20px} | |
| 641 | 714 | .empty{margin:0;color:var(--muted);font-size:14px;padding:16px 18px;border:1px dashed var(--line-strong);border-radius:14px} | |
| 642 | 715 | .muted{color:var(--muted);font-size:14px} | |
| 643 | − | .note{margin:14px 0 0;font-size:14px;color:var(--soft);padding:10px 14px;border:1px solid rgba(182,168,255,.35);border-radius:10px;background:rgba(182,168,255,.06)} | |
| 716 | + | .note{margin:14px 0 0;font-size:14px;color:var(--soft);padding:10px 14px;border:1px solid color-mix(in srgb,var(--lav) 35%,transparent);border-radius:10px;background:color-mix(in srgb,var(--lav) 6%,transparent)} | |
| 644 | 717 | .lines{list-style:none;margin:0;padding:0;border:1px solid var(--line);border-radius:14px;background:var(--surface)} | |
| 645 | 718 | .line{padding:12px 18px}.line+.line{border-top:1px solid var(--line)} | |
| 646 | 719 | .line-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;font-weight:600;font-size:15px} | |
| ⋯ | |||
| 653 | 726 | input[type=email]{flex:1 1 220px;min-width:0;font:inherit;font-size:14px;color:var(--fg);background:var(--bg);border:1px solid var(--line-strong);border-radius:8px;padding:9px 12px} | |
| 654 | 727 | input[type=email]::placeholder{color:var(--faint)} | |
| 655 | 728 | button{font:inherit;font-size:14px;font-weight:600;color:var(--bg);background:var(--lav);border:0;border-radius:8px;padding:9px 16px;cursor:pointer} | |
| 656 | − | button:hover{background:#c8bdff} | |
| 729 | + | button:hover{background:var(--lav-hover)} | |
| 657 | 730 | .trap{position:absolute;left:-9999px;width:1px;height:1px;opacity:0} | |
| 658 | 731 | .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)} | |
| 659 | 732 | .feeds{margin:10px 0 0;font-size:13px;color:var(--faint)}.feeds a,.muted a,.note a{color:var(--soft)} | |
| ⋯ | |||
| 672 | 745 | .about dt{color:var(--soft)}.about dd{margin:0;color:var(--faint)} | |
| 673 | 746 | .foot{display:flex;flex-wrap:wrap;gap:8px;margin-top:48px;padding-top:20px;border-top:1px solid var(--line);font-size:13px;color:var(--faint)} | |
| 674 | 747 | .foot a{text-decoration:none;color:var(--muted)}.foot a:hover{color:var(--fg)} | |
| 748 | + | .top-end{display:flex;align-items:center;gap:16px} | |
| 749 | + | .theme{display:flex;gap:2px;padding:2px;border:1px solid var(--line);border-radius:8px;background:var(--surface)} | |
| 750 | + | :root:not([data-theme]) .theme{display:none} | |
| 751 | + | .theme button{display:grid;place-items:center;width:26px;height:24px;padding:0;border-radius:6px;background:transparent;color:var(--faint)} | |
| 752 | + | .theme button:hover{background:var(--raised);color:var(--fg)} | |
| 753 | + | :root[data-theme=auto] .theme [data-theme-choice=auto],:root[data-theme=light] .theme [data-theme-choice=light],:root[data-theme=dark] .theme [data-theme-choice=dark]{background:var(--raised);color:var(--fg)} | |
| 675 | 754 | @media (max-width:640px){ | |
| 676 | 755 | .bar li:nth-child(-n+${HISTORY_DAYS - 30}){display:none} | |
| 677 | 756 | .bar{gap:3px} | |