Offline-capable geomap, meant for storing location bookmarks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: start adding download organization

+2239 -288
+8 -8
data/README.md
··· 72 72 73 73 ## CLI Commands 74 74 75 - | Command | Description | 76 - |---|---| 77 - | `list [--search <term>]` | Browse available region slugs | 78 - | `download:osm <region> [--force]` | Download `.osm.pbf` from Geofabrik | 79 - | `download:poly [region] [--all] [--force]` | Download `.poly` boundary file(s) | 80 - | `extract <region> --from <source>` | Carve a sub-region from a larger PBF | 81 - | `build <region>` | Run tilemaker → `.pmtiles` | 82 - | `clean --region <slug> \| --all` | Remove intermediate files | 75 + | Command | Description | 76 + | ------------------------------------------ | ------------------------------------ | 77 + | `list [--search <term>]` | Browse available region slugs | 78 + | `download:osm <region> [--force]` | Download `.osm.pbf` from Geofabrik | 79 + | `download:poly [region] [--all] [--force]` | Download `.poly` boundary file(s) | 80 + | `extract <region> --from <source>` | Carve a sub-region from a larger PBF | 81 + | `build <region>` | Run tilemaker → `.pmtiles` | 82 + | `clean --region <slug> \| --all` | Remove intermediate files | 83 83 84 84 ## Dependencies 85 85
+68 -8
data/cli/commands/build.ts
··· 1 1 import { Command } from '@cliffy/command' 2 2 import { ensureDir } from '@std/fs' 3 3 import { join } from '@std/path' 4 - import { CONFIG_JSON, PBF_DIR, PROCESS_LUA, TILES_OUT_DIR } from '../shared/paths.ts' 4 + import { 5 + CONFIG_JSON, 6 + PBF_DIR, 7 + PROCESS_LUA, 8 + TILES_MANIFEST, 9 + TILES_OUT_DIR, 10 + } from '../shared/paths.ts' 5 11 import { leafName } from '../shared/regions.ts' 6 12 import { run } from '../shared/run.ts' 7 13 14 + type TileEntry = { 15 + id: string 16 + label: string 17 + description: string 18 + filename: string 19 + path: string 20 + group: string 21 + } 22 + 23 + async function upsertManifest(entry: TileEntry): Promise<void> { 24 + let tiles: TileEntry[] = [] 25 + try { 26 + tiles = JSON.parse(await Deno.readTextFile(TILES_MANIFEST)) 27 + } catch { /* first run */ } 28 + const idx = tiles.findIndex((t) => t.id === entry.id) 29 + if (idx >= 0) tiles[idx] = entry 30 + else tiles.push(entry) 31 + await Deno.writeTextFile( 32 + TILES_MANIFEST, 33 + JSON.stringify(tiles, null, 2) + '\n', 34 + ) 35 + } 36 + 8 37 export const buildCmd = new Command() 9 38 .name('build') 10 - .description('Run tilemaker to generate a .pmtiles file from a regional .osm.pbf.') 39 + .description( 40 + 'Run tilemaker to generate a .pmtiles file from a regional .osm.pbf.', 41 + ) 11 42 .arguments('<region:string>') 12 43 .action(async (_, region) => { 13 44 const parts = region.split('/') 14 - const pbfPath = join(PBF_DIR, ...parts.slice(0, -1), `${leafName(region)}-latest.osm.pbf`) 45 + const pbfPath = join( 46 + PBF_DIR, 47 + ...parts.slice(0, -1), 48 + `${leafName(region)}-latest.osm.pbf`, 49 + ) 15 50 const outPath = join(TILES_OUT_DIR, `${leafName(region)}.pmtiles`) 16 51 17 52 if (!(await Deno.stat(pbfPath).catch(() => null))) { 18 - throw new Error(`PBF not found: ${pbfPath}\nRun: deno task data download:osm ${region}`) 53 + throw new Error( 54 + `PBF not found: ${pbfPath}\nRun: deno task data download:osm ${region}`, 55 + ) 19 56 } 20 57 21 58 await ensureDir(TILES_OUT_DIR) ··· 23 60 console.log(`Building tiles for ${region} ...`) 24 61 25 62 await run('tilemaker', [ 26 - '--input', pbfPath, 27 - '--output', outPath, 28 - '--config', CONFIG_JSON, 29 - '--process', PROCESS_LUA, 63 + '--input', 64 + pbfPath, 65 + '--output', 66 + outPath, 67 + '--config', 68 + CONFIG_JSON, 69 + '--process', 70 + PROCESS_LUA, 30 71 ]) 31 72 32 73 console.log(`Saved: ${outPath}`) 74 + 75 + const { size } = await Deno.stat(outPath) 76 + const sizeStr = size < 1024 * 1024 77 + ? `${Math.round(size / 1024)} KB` 78 + : `${Math.round(size / (1024 * 1024))} MB` 79 + const id = leafName(region) 80 + const label = id.split('-').map((w) => w[0].toUpperCase() + w.slice(1)) 81 + .join(' ') 82 + const groupRaw = parts.length > 1 ? parts[parts.length - 2] : 'other' 83 + const group = groupRaw[0].toUpperCase() + groupRaw.slice(1) 84 + await upsertManifest({ 85 + id, 86 + label, 87 + description: `Detailed street map · ~${sizeStr}`, 88 + filename: `${id}.pmtiles`, 89 + path: `/static/tiles/${id}.pmtiles`, 90 + group, 91 + }) 92 + console.log(`Updated tiles manifest`) 33 93 })
+14 -3
data/cli/commands/clean.ts
··· 15 15 export const cleanCmd = new Command() 16 16 .name('clean') 17 17 .description('Remove intermediate .osm.pbf and .poly files.') 18 - .option('--region <region:string>', 'Remove files for a specific region only.') 18 + .option( 19 + '--region <region:string>', 20 + 'Remove files for a specific region only.', 21 + ) 19 22 .option('--all', 'Remove all intermediate files.') 20 23 .action(async ({ region, all }) => { 21 24 if (all) { ··· 23 26 await removeIfExists(POLY_DIR) 24 27 } else if (region) { 25 28 const parts = region.split('/') 26 - const pbfPath = join(PBF_DIR, ...parts.slice(0, -1), `${leafName(region)}-latest.osm.pbf`) 27 - const polyPath = join(POLY_DIR, ...parts.slice(0, -1), `${leafName(region)}.poly`) 29 + const pbfPath = join( 30 + PBF_DIR, 31 + ...parts.slice(0, -1), 32 + `${leafName(region)}-latest.osm.pbf`, 33 + ) 34 + const polyPath = join( 35 + POLY_DIR, 36 + ...parts.slice(0, -1), 37 + `${leafName(region)}.poly`, 38 + ) 28 39 await removeIfExists(pbfPath) 29 40 await removeIfExists(polyPath) 30 41 } else {
+8 -2
data/cli/commands/download_osm.ts
··· 7 7 8 8 export const downloadOsmCmd = new Command() 9 9 .name('download:osm') 10 - .description('Download a regional .osm.pbf file from Geofabrik.') 10 + .description( 11 + 'Download a regional .osm.pbf file from Geofabrik. e.g. "europe/spain"', 12 + ) 11 13 .arguments('<region:string>') 12 14 .option('--force', 'Re-download even if the file already exists.') 13 15 .action(async ({ force }, region) => { ··· 32 34 throw new Error(`HTTP ${response.status}: ${response.statusText}`) 33 35 } 34 36 35 - const file = await Deno.open(outPath, { write: true, create: true, truncate: true }) 37 + const file = await Deno.open(outPath, { 38 + write: true, 39 + create: true, 40 + truncate: true, 41 + }) 36 42 await response.body!.pipeTo(file.writable) 37 43 38 44 console.log(`Saved: ${outPath}`)
+30 -9
data/cli/commands/extract.ts
··· 8 8 .name('extract') 9 9 .description('Extract a sub-region from a larger .osm.pbf using osmium.') 10 10 .arguments('<region:string>') 11 - .option('--from <source:string>', 'Source region slug to extract from.', { required: true }) 11 + .option('--from <source:string>', 'Source region slug to extract from.', { 12 + required: true, 13 + }) 12 14 .action(async ({ from: source }, region) => { 13 15 const sourceParts = source.split('/') 14 - const sourcePbf = join(PBF_DIR, ...sourceParts.slice(0, -1), `${leafName(source)}-latest.osm.pbf`) 16 + const sourcePbf = join( 17 + PBF_DIR, 18 + ...sourceParts.slice(0, -1), 19 + `${leafName(source)}-latest.osm.pbf`, 20 + ) 15 21 16 22 const regionParts = region.split('/') 17 - const polyFile = join(POLY_DIR, ...regionParts.slice(0, -1), `${leafName(region)}.poly`) 18 - const outPbf = join(PBF_DIR, ...regionParts.slice(0, -1), `${leafName(region)}-latest.osm.pbf`) 23 + const polyFile = join( 24 + POLY_DIR, 25 + ...regionParts.slice(0, -1), 26 + `${leafName(region)}.poly`, 27 + ) 28 + const outPbf = join( 29 + PBF_DIR, 30 + ...regionParts.slice(0, -1), 31 + `${leafName(region)}-latest.osm.pbf`, 32 + ) 19 33 20 34 if (!(await Deno.stat(sourcePbf).catch(() => null))) { 21 - throw new Error(`Source PBF not found: ${sourcePbf}\nRun: deno task data download:osm ${source}`) 35 + throw new Error( 36 + `Source PBF not found: ${sourcePbf}\nRun: deno task data download:osm ${source}`, 37 + ) 22 38 } 23 39 if (!(await Deno.stat(polyFile).catch(() => null))) { 24 - throw new Error(`Poly file not found: ${polyFile}\nRun: deno task data download:poly ${region}`) 40 + throw new Error( 41 + `Poly file not found: ${polyFile}\nRun: deno task data download:poly ${region}`, 42 + ) 25 43 } 26 44 27 45 console.log(`Extracting ${region} from ${source} ...`) 28 46 29 47 await run('osmium', [ 30 48 'extract', 31 - '--polygon', polyFile, 32 - '--strategy', 'complete_ways', 49 + '--polygon', 50 + polyFile, 51 + '--strategy', 52 + 'complete_ways', 33 53 '--overwrite', 34 54 sourcePbf, 35 - '--output', outPbf, 55 + '--output', 56 + outPbf, 36 57 ]) 37 58 38 59 console.log(`Saved: ${outPbf}`)
+3 -1
data/cli/commands/list.ts
··· 7 7 .option('--search <term:string>', 'Filter results by search term.') 8 8 .action(async ({ search }) => { 9 9 const regions = await listRegions() 10 - const filtered = search ? regions.filter((r) => r.includes(search)) : regions 10 + const filtered = search 11 + ? regions.filter((r) => r.includes(search)) 12 + : regions 11 13 for (const r of filtered) console.log(r) 12 14 console.log(`\n${filtered.length} region(s)`) 13 15 })
+1
data/cli/shared/paths.ts
··· 10 10 export const PROCESS_LUA = join(DOCS_DIR, 'tilemaker', 'process.lua') 11 11 export const REGIONS_FILE = join(DOCS_DIR, 'geofabrik', 'regions.txt') 12 12 export const TILES_OUT_DIR = join(ROOT_DIR, 'www', 'static', 'tiles') 13 + export const TILES_MANIFEST = join(TILES_OUT_DIR, 'tiles.json') 13 14 export const PBF_DIR = join(DATA_DIR, 'osm') 14 15 export const POLY_DIR = join(DATA_DIR, 'poly')
+1 -1
deno.json
··· 22 22 "@civility/store": "jsr:@civility/store@^0.3.1", 23 23 "@civility/sync": "jsr:@civility/sync@^0.1.1", 24 24 "@civility/ui": "jsr:@civility/ui@^0.2.6", 25 - "@civility/workers": "jsr:@civility/workers@^0.2.5", 25 + "@civility/workers": "jsr:@civility/workers@^0.2.4", 26 26 "@zod/zod": "jsr:@zod/zod@^4.3.6", 27 27 "lit": "npm:lit@^3.3.2", 28 28 "maplibre-gl": "npm:maplibre-gl@^5.21.0",
+41 -1
deno.lock
··· 6 6 "jsr:@civility/sync@~0.1.1": "0.1.1", 7 7 "jsr:@civility/ui@~0.2.6": "0.2.6", 8 8 "jsr:@civility/workers@~0.2.4": "0.2.4", 9 + "jsr:@cliffy/command@1": "1.0.0", 10 + "jsr:@cliffy/flags@1.0.0": "1.0.0", 11 + "jsr:@cliffy/internal@1.0.0": "1.0.0", 12 + "jsr:@cliffy/table@1.0.0": "1.0.0", 9 13 "jsr:@paulmillr/qr@~0.5.5": "0.5.5", 14 + "jsr:@std/fmt@^1.0.9": "1.0.9", 15 + "jsr:@std/fs@1": "1.0.23", 10 16 "jsr:@std/fs@^1.0.23": "1.0.23", 11 17 "jsr:@std/html@^1.0.5": "1.0.5", 12 18 "jsr:@std/internal@^1.0.12": "1.0.12", 13 19 "jsr:@std/path@^1.1.4": "1.1.4", 14 20 "jsr:@std/semver@^1.0.8": "1.0.8", 21 + "jsr:@std/text@^1.0.17": "1.0.17", 15 22 "jsr:@zod/zod@^4.3.6": "4.3.6", 16 23 "npm:lit@^3.3.2": "3.3.2", 17 24 "npm:maplibre-gl@^5.21.0": "5.21.0", ··· 23 30 "@civility/store@0.3.1": { 24 31 "integrity": "0438f2cdb16145a61a97f5be509cd0b34e7cbd9f71dc657feffe2a4dd7dd0ec3", 25 32 "dependencies": [ 26 - "jsr:@std/fs", 33 + "jsr:@std/fs@^1.0.23", 27 34 "jsr:@std/semver" 28 35 ] 29 36 }, ··· 45 52 "@civility/workers@0.2.4": { 46 53 "integrity": "38fafb96bc15a988e7723bc9b021394bdfef842c8e8372b960ec2476e5c74b43" 47 54 }, 55 + "@cliffy/command@1.0.0": { 56 + "integrity": "c52a241ea68857fcdaff4f3173eb404f8017d7bc35553b6f533c592b89dde7d2", 57 + "dependencies": [ 58 + "jsr:@cliffy/flags", 59 + "jsr:@cliffy/internal", 60 + "jsr:@cliffy/table", 61 + "jsr:@std/fmt", 62 + "jsr:@std/text" 63 + ] 64 + }, 65 + "@cliffy/flags@1.0.0": { 66 + "integrity": "8b57698adc644da8f90422d58976362d41a4ebca39c312ca1c101585d0148feb", 67 + "dependencies": [ 68 + "jsr:@cliffy/internal", 69 + "jsr:@std/text" 70 + ] 71 + }, 72 + "@cliffy/internal@1.0.0": { 73 + "integrity": "1e17ccbcd5420093c0a93e5b3827bbdc9abac5195bacf187edc44665e54bdde6" 74 + }, 75 + "@cliffy/table@1.0.0": { 76 + "integrity": "3fdaa9e1ef1ea62022108adabd826932bdea8dd05497079896febcd41322907f", 77 + "dependencies": [ 78 + "jsr:@std/fmt" 79 + ] 80 + }, 48 81 "@paulmillr/qr@0.5.5": { 49 82 "integrity": "2f8ff22c8d2194f2147eac1b3093f5e85f648c0a8005d5635a617fb72bf5ae38" 50 83 }, 84 + "@std/fmt@1.0.9": { 85 + "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" 86 + }, 51 87 "@std/fs@1.0.23": { 52 88 "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", 53 89 "dependencies": [ 90 + "jsr:@std/internal", 54 91 "jsr:@std/path" 55 92 ] 56 93 }, ··· 68 105 }, 69 106 "@std/semver@1.0.8": { 70 107 "integrity": "dc830e8b8b6a380c895d53fbfd1258dc253704ca57bbe1629ac65fd7830179b7" 108 + }, 109 + "@std/text@1.0.17": { 110 + "integrity": "4b2c4ef67ae5b6c1dfd447c81c83a43718f52e3c7e748d8b33f694aba9895f95" 71 111 }, 72 112 "@zod/zod@4.3.6": { 73 113 "integrity": "7144e5e11f8ffc3cf6e2fca624f6597a8762898aac9868cc8938e9398b96ffe4"
+10 -2
www/index.html
··· 17 17 <link rel="canonical" href="https://world.bpev.me" /> 18 18 <title>world</title> 19 19 20 - <link rel="stylesheet" type="text/css" href="/static/styles/maplibre-gl.css"> 21 - <link rel="stylesheet" type="text/css" href="/static/styles/civility.min.css"> 20 + <link 21 + rel="stylesheet" 22 + type="text/css" 23 + href="/static/styles/maplibre-gl.css" 24 + > 25 + <link 26 + rel="stylesheet" 27 + type="text/css" 28 + href="/static/styles/civility.min.css" 29 + > 22 30 <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 23 31 24 32 <script src="/dist/index.js" type="module"></script>
+1 -1
www/index.ts
··· 8 8 import './routes/bookmarks.ts' 9 9 10 10 client.init() 11 - client.watchForUpdates() 11 + // client.watchForUpdates() 12 12 13 13 interface NavMeta { 14 14 title?: string
+9 -2
www/models/app.ts
··· 1 1 import State from '@civility/store/state' 2 - import { AppState, Bookmark, BookmarkFolder, SearchHistoryEntry } from './schema.ts' 2 + import { 3 + AppState, 4 + Bookmark, 5 + BookmarkFolder, 6 + SearchHistoryEntry, 7 + } from './schema.ts' 3 8 import { Store } from './store.ts' 4 9 5 10 export class App extends State<AppState> { ··· 76 81 error?: string 77 82 }> { 78 83 try { 79 - return await this.store.exportToFile(filename ?? `world-export_${Date.now()}`) 84 + return await this.store.exportToFile( 85 + filename ?? `world-export_${Date.now()}`, 86 + ) 80 87 } catch (error) { 81 88 return { 82 89 success: false,
+7 -1
www/models/schema.ts
··· 1 - export { AppState, Bookmark, BookmarkFolder, SearchHistoryEntry, StoreState } from './schema/v0.ts' 1 + export { 2 + AppState, 3 + Bookmark, 4 + BookmarkFolder, 5 + SearchHistoryEntry, 6 + StoreState, 7 + } from './schema/v0.ts' 2 8 import { StoreState } from './schema/v0.ts' 3 9 4 10 const currentVersion = globalThis.__APP_VERSION__
+10 -3
www/models/store.ts
··· 45 45 const trimmed = query.trim() 46 46 if (!trimmed) return 47 47 const data = await this.#sync.get() 48 - const existing = (data.searchHistory ?? []).filter((e) => e.query !== trimmed) 48 + const existing = (data.searchHistory ?? []).filter((e) => 49 + e.query !== trimmed 50 + ) 49 51 data.searchHistory = [ 50 - SearchHistoryEntry.parse({ query: trimmed, timestamp: new Date().toISOString() }), 52 + SearchHistoryEntry.parse({ 53 + query: trimmed, 54 + timestamp: new Date().toISOString(), 55 + }), 51 56 ...existing, 52 57 ].slice(0, 20) 53 58 await this.#sync.set(data) ··· 115 120 116 121 async deleteFolder(id: string): Promise<void> { 117 122 const data = await this.#sync.get() 118 - data.bookmarkFolders = (data.bookmarkFolders ?? []).filter((f) => f.id !== id) 123 + data.bookmarkFolders = (data.bookmarkFolders ?? []).filter((f) => 124 + f.id !== id 125 + ) 119 126 // unfiled bookmarks that were in this folder 120 127 data.bookmarks = (data.bookmarks ?? []).map((b) => 121 128 b.folderId === id ? { ...b, folderId: null } : b
+92 -58
www/routes/bookmarks.ts
··· 131 131 <button type="submit">Add</button> 132 132 <button 133 133 type="button" 134 - @click="${() => { this.showAddFolder = false; this.requestUpdate() }}" 135 - >Cancel</button> 134 + @click="${() => { 135 + this.showAddFolder = false 136 + this.requestUpdate() 137 + }}" 138 + > 139 + Cancel 140 + </button> 136 141 </div> 137 142 </form> 138 143 ` 139 - : ''} 140 - 141 - ${isEmpty 142 - ? html`<p class="bm-empty">No bookmarks yet. Add one with the button above.</p>` 143 - : ''} 144 - 145 - ${this.folders.map((folder) => { 146 - const items = this.bookmarks.filter((b) => b.folderId === folder.id) 147 - const expanded = this.expandedFolders.has(folder.id) 148 - return html` 149 - <div class="bm-folder"> 150 - <div 151 - class="bm-folder-header" 152 - role="button" 153 - @click="${() => this.#toggleFolder(folder.id)}" 154 - > 155 - <span class="bm-folder-toggle" aria-hidden="true">${expanded ? '▾' : '▸'}</span> 156 - <span class="bm-folder-name">${folder.name}</span> 157 - <span class="bm-folder-count">${items.length}</span> 158 - <button 159 - class="bm-icon-btn" 160 - aria-label="Delete folder" 161 - @click="${(e: Event) => { e.stopPropagation(); this.#deleteFolder(folder.id) }}" 144 + : ''} ${isEmpty 145 + ? html` 146 + <p class="bm-empty">No bookmarks yet. Add one with the button above.</p> 147 + ` 148 + : ''} ${this.folders.map((folder) => { 149 + const items = this.bookmarks.filter((b) => b.folderId === folder.id) 150 + const expanded = this.expandedFolders.has(folder.id) 151 + return html` 152 + <div class="bm-folder"> 153 + <div 154 + class="bm-folder-header" 155 + role="button" 156 + @click="${() => this.#toggleFolder(folder.id)}" 162 157 > 163 - <img src="/static/icons/x.svg" alt="" aria-hidden="true"> 164 - </button> 158 + <span class="bm-folder-toggle" aria-hidden="true">${expanded 159 + ? '▾' 160 + : '▸'}</span> 161 + <span class="bm-folder-name">${folder.name}</span> 162 + <span class="bm-folder-count">${items.length}</span> 163 + <button 164 + class="bm-icon-btn" 165 + aria-label="Delete folder" 166 + @click="${(e: Event) => { 167 + e.stopPropagation() 168 + this.#deleteFolder(folder.id) 169 + }}" 170 + > 171 + <img src="/static/icons/x.svg" alt="" aria-hidden="true"> 172 + </button> 173 + </div> 174 + ${expanded 175 + ? html` 176 + <div class="bm-folder-body"> 177 + ${items.length === 0 178 + ? html` 179 + <p class="bm-empty bm-empty--folder">No bookmarks in this folder.</p> 180 + ` 181 + : items.map((b) => this.#renderBookmark(b))} 182 + </div> 183 + ` 184 + : ''} 165 185 </div> 166 - ${expanded 167 - ? html` 168 - <div class="bm-folder-body"> 169 - ${items.length === 0 170 - ? html`<p class="bm-empty bm-empty--folder">No bookmarks in this folder.</p>` 171 - : items.map((b) => this.#renderBookmark(b))} 172 - </div> 173 - ` 174 - : ''} 175 - </div> 176 - ` 177 - })} 178 - 179 - ${unfiled.length > 0 186 + ` 187 + })} ${unfiled.length > 0 180 188 ? html` 181 189 <div class="bm-section"> 182 190 ${hasFolders 183 - ? html`<h3 class="bm-section-title">Unfiled</h3>` 184 - : ''} 185 - ${unfiled.map((b) => this.#renderBookmark(b))} 191 + ? html` 192 + <h3 class="bm-section-title">Unfiled</h3> 193 + ` 194 + : ''} ${unfiled.map((b) => this.#renderBookmark(b))} 186 195 </div> 187 196 ` 188 - : ''} 189 - 190 - ${this.showAddBookmark 197 + : ''} ${this.showAddBookmark 191 198 ? html` 192 - <form class="bm-add-form bm-add-form--bookmark" @submit="${this.#submitAddBookmark}"> 199 + <form class="bm-add-form bm-add-form--bookmark" @submit="${this 200 + .#submitAddBookmark}"> 193 201 <h3 class="bm-section-title">Add Bookmark</h3> 194 202 <input 195 203 name="name" ··· 202 210 <div class="bm-coord-row"> 203 211 <input name="lat" type="number" step="any" placeholder="Latitude" required> 204 212 <input name="lng" type="number" step="any" placeholder="Longitude" required> 205 - <input name="zoom" type="number" step="any" placeholder="Zoom" min="1" max="22"> 213 + <input 214 + name="zoom" 215 + type="number" 216 + step="any" 217 + placeholder="Zoom" 218 + min="1" 219 + max="22" 220 + > 206 221 </div> 207 222 ${hasFolders 208 223 ? html` 209 224 <select name="folderId"> 210 225 <option value="">Unfiled</option> 211 - ${this.folders.map((f) => html`<option value="${f.id}">${f.name}</option>`)} 226 + ${this.folders.map((f) => 227 + html` 228 + <option value="${f.id}">${f.name}</option> 229 + ` 230 + )} 212 231 </select> 213 232 ` 214 233 : ''} ··· 216 235 <button type="submit">Save</button> 217 236 <button 218 237 type="button" 219 - @click="${() => { this.showAddBookmark = false; this.requestUpdate() }}" 220 - >Cancel</button> 238 + @click="${() => { 239 + this.showAddBookmark = false 240 + this.requestUpdate() 241 + }}" 242 + > 243 + Cancel 244 + </button> 221 245 </div> 222 246 </form> 223 247 ` ··· 230 254 <div class="bm-item"> 231 255 <div class="bm-item-info"> 232 256 <span class="bm-item-name">${b.name}</span> 233 - <span class="bm-item-coords">${b.lat.toFixed(5)}, ${b.lng.toFixed(5)}</span> 257 + <span class="bm-item-coords">${b.lat.toFixed(5)}, ${b.lng.toFixed( 258 + 5, 259 + )}</span> 234 260 </div> 235 261 <div class="bm-item-actions"> 236 262 ${this.folders.length > 0 ··· 239 265 class="bm-item-folder" 240 266 aria-label="Move to folder" 241 267 @change="${(e: Event) => 242 - this.#moveBookmark(b.id, (e.target as HTMLSelectElement).value || null)}" 268 + this.#moveBookmark( 269 + b.id, 270 + (e.target as HTMLSelectElement).value || null, 271 + )}" 243 272 > 244 - <option value="" ?selected="${b.folderId === null}">Unfiled</option> 245 - ${this.folders.map((f) => html` 246 - <option value="${f.id}" ?selected="${b.folderId === f.id}">${f.name}</option> 247 - `)} 273 + <option value="" ?selected="${b.folderId === 274 + null}">Unfiled</option> 275 + ${this.folders.map((f) => 276 + html` 277 + <option value="${f 278 + .id}" ?selected="${b.folderId === f.id}">${f 279 + .name}</option> 280 + ` 281 + )} 248 282 </select> 249 283 ` 250 284 : ''}
+35 -10
www/routes/map.ts
··· 1 1 import { html, LitElement, type TemplateResult } from 'lit' 2 2 import maplibregl from 'maplibre-gl' 3 3 import { Protocol } from 'pmtiles' 4 - import { getCachedPMTiles, downloadAndSavePMTiles } from '../utils/fs.ts' 4 + import { downloadAndSavePMTiles, getCachedPMTiles } from '../utils/fs.ts' 5 5 import { getMapNav, setMapNav } from '../utils/nav.ts' 6 6 import layers from '../utils/layers.ts' 7 7 import worldLayers from '../utils/world_layers.ts' ··· 37 37 } 38 38 39 39 override render(): TemplateResult { 40 - return html`<div id="map"></div>` 40 + return html` 41 + <div id="map"></div> 42 + ` 41 43 } 42 44 43 45 override async firstUpdated(): Promise<void> { ··· 76 78 const target = getMapNav() 77 79 if (target) { 78 80 setMapNav(null) 79 - this.#map!.flyTo({ center: [target.lng, target.lat], zoom: target.zoom }) 81 + this.#map!.flyTo({ 82 + center: [target.lng, target.lat], 83 + zoom: target.zoom, 84 + }) 80 85 if (target.marker) { 81 86 this.#marker?.remove() 82 87 this.#marker = new maplibregl.Marker() 83 88 .setLngLat([target.lng, target.lat]) 84 - .setPopup(this.#buildPopup(target.name ?? 'Unknown location', target.lat, target.lng, target.zoom)) 89 + .setPopup( 90 + this.#buildPopup( 91 + target.name ?? 'Unknown location', 92 + target.lat, 93 + target.lng, 94 + target.zoom, 95 + ), 96 + ) 85 97 .addTo(this.#map!) 86 98 this.#marker.togglePopup() 87 99 } ··· 103 115 const { lat, lng } = this.#map.getCenter() 104 116 const zoom = this.#map.getZoom() 105 117 try { 106 - localStorage.setItem('world-last-view', JSON.stringify({ lat, lng, zoom })) 118 + localStorage.setItem( 119 + 'world-last-view', 120 + JSON.stringify({ lat, lng, zoom }), 121 + ) 107 122 } catch { 108 123 // storage may be unavailable 109 124 } 110 125 } 111 126 112 - #buildPopup(name: string, lat: number, lng: number, zoom: number): maplibregl.Popup { 127 + #buildPopup( 128 + name: string, 129 + lat: number, 130 + lng: number, 131 + zoom: number, 132 + ): maplibregl.Popup { 113 133 const container = document.createElement('div') 114 134 115 135 const label = document.createElement('p') 116 - label.style.cssText = 'margin:0 0 8px;font-size:0.85em;max-width:200px;word-break:break-word' 136 + label.style.cssText = 137 + 'margin:0 0 8px;font-size:0.85em;max-width:200px;word-break:break-word' 117 138 label.textContent = name 118 139 119 140 const button = document.createElement('button') ··· 134 155 this.#bookmarkMarkers.forEach((m) => m.remove()) 135 156 this.#bookmarkMarkers = [] 136 157 for (const b of app.bookmarks) { 137 - const popup = new maplibregl.Popup({ offset: 25 }).setHTML(`<strong>${b.name}</strong>`) 158 + const popup = new maplibregl.Popup({ offset: 25 }).setHTML( 159 + `<strong>${b.name}</strong>`, 160 + ) 138 161 const marker = new maplibregl.Marker({ color: '#e05c2a' }) 139 162 .setLngLat([b.lng, b.lat]) 140 163 .setPopup(popup) ··· 157 180 async #loadWorldTiles(): Promise<void> { 158 181 if (!this.#map) return 159 182 if (!registeredSources.has('world')) { 160 - const pmtiles = await downloadAndSavePMTiles( 161 - '/static/tiles/world.pmtiles', 183 + const z7 = await getCachedPMTiles('world_z7.pmtiles') 184 + const z6 = !z7 ? await getCachedPMTiles('world_z6.pmtiles') : null 185 + const pmtiles = z7 ?? z6 ?? await downloadAndSavePMTiles( 186 + '/static/tiles/world/world.pmtiles', 162 187 'world.pmtiles', 163 188 ) 164 189 protocol.add(pmtiles)
+55 -49
www/routes/search.ts
··· 128 128 </form> 129 129 130 130 ${this.loading 131 - ? html`<p class="search-empty">Searching…</p>` 131 + ? html` 132 + <p class="search-empty">Searching…</p> 133 + ` 132 134 : this.error 133 - ? html`<p class="search-empty">${this.error}</p>` 134 - : this.results.length > 0 135 - ? html` 136 - <div class="search-history-list" role="list"> 137 - ${this.results.map((result) => 138 - html` 139 - <button 140 - class="search-history-item" 141 - role="listitem" 142 - @click="${() => this.#handleResultClick(result)}" 143 - > 144 - <span>${result.display_name}</span> 145 - <img 146 - src="/static/icons/navigation.svg" 147 - alt="" 148 - aria-hidden="true" 149 - style="width:16px;height:16px;opacity:0.4" 150 - > 151 - </button> 152 - ` 153 - )} 154 - </div> 155 - ` 156 - : this.history.length > 0 157 - ? html` 158 - <div class="search-history-clear"> 159 - <button @click="${this.#handleClearHistory}">Clear history</button> 160 - </div> 161 - <div class="search-history-list" role="list"> 162 - ${this.history.map((entry) => 163 - html` 164 - <button 165 - class="search-history-item" 166 - role="listitem" 167 - @click="${() => this.#handleHistoryClick(entry.query)}" 168 - > 169 - <span>${entry.query}</span> 170 - <img 171 - src="/static/icons/navigation.svg" 172 - alt="" 173 - aria-hidden="true" 174 - style="width:16px;height:16px;opacity:0.4" 175 - > 176 - </button> 177 - ` 178 - )} 179 - </div> 135 + ? html` 136 + <p class="search-empty">${this.error}</p> 137 + ` 138 + : this.results.length > 0 139 + ? html` 140 + <div class="search-history-list" role="list"> 141 + ${this.results.map((result) => 142 + html` 143 + <button 144 + class="search-history-item" 145 + role="listitem" 146 + @click="${() => this.#handleResultClick(result)}" 147 + > 148 + <span>${result.display_name}</span> 149 + <img 150 + src="/static/icons/navigation.svg" 151 + alt="" 152 + aria-hidden="true" 153 + style="width:16px;height:16px;opacity:0.4" 154 + > 155 + </button> 180 156 ` 181 - : html`<p class="search-empty">Search for a location to get started.</p>`} 157 + )} 158 + </div> 159 + ` 160 + : this.history.length > 0 161 + ? html` 162 + <div class="search-history-clear"> 163 + <button @click="${this.#handleClearHistory}">Clear history</button> 164 + </div> 165 + <div class="search-history-list" role="list"> 166 + ${this.history.map((entry) => 167 + html` 168 + <button 169 + class="search-history-item" 170 + role="listitem" 171 + @click="${() => this.#handleHistoryClick(entry.query)}" 172 + > 173 + <span>${entry.query}</span> 174 + <img 175 + src="/static/icons/navigation.svg" 176 + alt="" 177 + aria-hidden="true" 178 + style="width:16px;height:16px;opacity:0.4" 179 + > 180 + </button> 181 + ` 182 + )} 183 + </div> 184 + ` 185 + : html` 186 + <p class="search-empty">Search for a location to get started.</p> 187 + `} 182 188 ` 183 189 } 184 190 }
+14 -8
www/routes/settings-about.ts
··· 10 10 <section> 11 11 <h2>World</h2> 12 12 <p> 13 - An offline-first map PWA. Browse detailed street maps and natural 14 - features without an internet connection once tiles are downloaded. 13 + An offline-first map PWA. Browse detailed street maps and natural features 14 + without an internet connection once tiles are downloaded. 15 15 </p> 16 16 <ui-pwa-version></ui-pwa-version> 17 17 </section> ··· 24 24 href="https://www.naturalearthdata.com" 25 25 rel="noopener noreferrer" 26 26 target="_blank" 27 - >Natural Earth</a>. 28 - Regional tiles are built from 27 + >Natural Earth</a>. Regional tiles are built from 29 28 <a 30 29 href="https://www.openstreetmap.org" 31 30 rel="noopener noreferrer" ··· 48 47 MapLibre GL JS 49 48 </a> 50 49 and 51 - <a href="https://protomaps.com/docs/pmtiles" rel="noopener noreferrer" target="_blank"> 50 + <a 51 + href="https://protomaps.com/docs/pmtiles" 52 + rel="noopener noreferrer" 53 + target="_blank" 54 + > 52 55 PMTiles 53 56 </a> 54 - for offline tile storage. 55 - Rendered as a PWA using 56 - <a href="https://github.com/bpev/civility" rel="noopener noreferrer" target="_blank"> 57 + for offline tile storage. Rendered as a PWA using 58 + <a 59 + href="https://github.com/bpev/civility" 60 + rel="noopener noreferrer" 61 + target="_blank" 62 + > 57 63 Civility 58 64 </a>. 59 65 </p>
+85 -32
www/routes/settings-downloads.ts
··· 1 1 import { html, LitElement, type TemplateResult } from 'lit' 2 - import { deletePMTiles, downloadAndSavePMTiles, isPMTilesCached } from '../utils/fs.ts' 2 + import { 3 + deletePMTiles, 4 + downloadAndSavePMTiles, 5 + isPMTilesCached, 6 + } from '../utils/fs.ts' 3 7 4 - type TileStatus = 'checking' | 'cached' | 'available' | 'downloading' | 'deleting' | 'error' 8 + type TileStatus = 9 + | 'checking' 10 + | 'cached' 11 + | 'available' 12 + | 'downloading' 13 + | 'deleting' 14 + | 'error' 5 15 6 16 type TileConfig = { 7 17 id: string ··· 9 19 description: string 10 20 filename: string 11 21 path: string 22 + group: string 12 23 } 13 24 14 - const TILES: TileConfig[] = [ 15 - { 16 - id: 'monaco', 17 - label: 'Monaco', 18 - description: 'Detailed street map · ~250 KB', 19 - filename: 'monaco.pmtiles', 20 - path: '/static/tiles/monaco.pmtiles', 21 - }, 22 - { 23 - id: 'taiwan', 24 - label: 'Taiwan', 25 - description: 'Detailed street map · ~130 MB', 26 - filename: 'taiwan.pmtiles', 27 - path: '/static/tiles/taiwan.pmtiles', 28 - }, 29 - ] 30 - 31 25 export class SettingsDownloadsPage extends LitElement { 32 - private statuses: Record<string, TileStatus> = Object.fromEntries( 33 - TILES.map((t) => [t.id, 'checking']), 34 - ) 26 + private tiles: TileConfig[] = [] 27 + private statuses: Record<string, TileStatus> = {} 35 28 private errors: Record<string, string> = {} 29 + private filter = '' 36 30 37 31 protected override createRenderRoot() { 38 32 return this ··· 40 34 41 35 override async connectedCallback() { 42 36 super.connectedCallback() 37 + const res = await fetch('/static/tiles/tiles.json') 38 + this.tiles = await res.json() 39 + this.statuses = Object.fromEntries( 40 + this.tiles.map((t) => [t.id, 'checking']), 41 + ) 42 + this.requestUpdate() 43 43 await this.#checkStatuses() 44 44 } 45 45 46 46 async #checkStatuses(): Promise<void> { 47 47 await Promise.all( 48 - TILES.map(async (tile) => { 48 + this.tiles.map(async (tile) => { 49 49 const cached = await isPMTilesCached(tile.filename) 50 50 this.statuses[tile.id] = cached ? 'cached' : 'available' 51 51 }), ··· 61 61 this.statuses[tile.id] = 'cached' 62 62 } catch (err) { 63 63 this.statuses[tile.id] = 'error' 64 - this.errors[tile.id] = err instanceof Error ? err.message : 'Download failed' 64 + this.errors[tile.id] = err instanceof Error 65 + ? err.message 66 + : 'Download failed' 65 67 } 66 68 this.requestUpdate() 67 69 } ··· 74 76 this.statuses[tile.id] = 'available' 75 77 } catch (err) { 76 78 this.statuses[tile.id] = 'error' 77 - this.errors[tile.id] = err instanceof Error ? err.message : 'Delete failed' 79 + this.errors[tile.id] = err instanceof Error 80 + ? err.message 81 + : 'Delete failed' 78 82 } 79 83 this.requestUpdate() 80 84 } 81 85 86 + #filteredGroups(): Map<string, TileConfig[]> { 87 + const q = this.filter.toLowerCase() 88 + const groups = new Map<string, TileConfig[]>() 89 + for (const tile of this.tiles) { 90 + if ( 91 + q && !tile.label.toLowerCase().includes(q) && 92 + !tile.group.toLowerCase().includes(q) 93 + ) { 94 + continue 95 + } 96 + const list = groups.get(tile.group) ?? [] 97 + list.push(tile) 98 + groups.set(tile.group, list) 99 + } 100 + return groups 101 + } 102 + 82 103 override render(): TemplateResult { 104 + const groups = this.#filteredGroups() 83 105 return html` 84 106 <section> 85 107 <h2>Offline Maps</h2> ··· 87 109 Download regions to use the app without an internet connection. Downloaded 88 110 maps are stored in your browser and persist across sessions. 89 111 </p> 90 - <div class="tile-list"> 91 - ${TILES.map((tile) => this.#renderTileItem(tile))} 92 - </div> 112 + <input 113 + class="w-100" 114 + type="search" 115 + placeholder="Search regions…" 116 + .value="${this.filter}" 117 + @input="${(e: Event) => { 118 + this.filter = (e.target as HTMLInputElement).value 119 + this.requestUpdate() 120 + }}" 121 + > 122 + ${this.filter 123 + ? html` 124 + <div class="tile-list"> 125 + ${[...groups.values()].flat().map((tile) => 126 + this.#renderTileItem(tile) 127 + )} 128 + </div> 129 + ` 130 + : [...groups.entries()].map(([group, tiles]) => 131 + html` 132 + <details> 133 + <summary>${group}</summary> 134 + <div class="tile-list"> 135 + ${tiles.map((tile) => this.#renderTileItem(tile))} 136 + </div> 137 + </details> 138 + ` 139 + )} 93 140 </section> 94 141 ` 95 142 } ··· 102 149 <span class="tile-item-name">${tile.label}</span> 103 150 <div class="tile-item-meta">${tile.description}</div> 104 151 ${status === 'error' 105 - ? html`<div class="tile-item-meta settings-data-status--error"> 152 + ? html` 153 + <div class="tile-item-meta settings-data-status--error"> 106 154 ${this.errors[tile.id] ?? 'Download failed'} 107 - </div>` 155 + </div> 156 + ` 108 157 : ''} 109 158 </div> 110 159 <div class="tile-item-action"> ··· 116 165 117 166 #renderTileAction(tile: TileConfig, status: TileStatus): TemplateResult { 118 167 if (status === 'checking') { 119 - return html`<ui-spinner></ui-spinner>` 168 + return html` 169 + <ui-spinner></ui-spinner> 170 + ` 120 171 } 121 172 if (status === 'cached') { 122 173 return html` ··· 137 188 ` 138 189 } 139 190 if (status === 'downloading' || status === 'deleting') { 140 - return html`<ui-spinner></ui-spinner>` 191 + return html` 192 + <ui-spinner></ui-spinner> 193 + ` 141 194 } 142 195 return html` 143 196 <button
+7 -3
www/routes/settings.ts
··· 20 20 <a class="settings-nav-link" href="#!/settings/downloads"> 21 21 <div> 22 22 <span>Offline Maps</span> 23 - <div class="settings-nav-link-meta">Download regions for offline use</div> 23 + <div class="settings-nav-link-meta"> 24 + Download regions for offline use 25 + </div> 24 26 </div> 25 27 <img 26 28 src="/static/icons/download.svg" ··· 36 38 <h2>Data</h2> 37 39 <p>Export your data to a file, or import a previously exported file.</p> 38 40 <div class="settings-data-actions"> 39 - <button class="action" id="settings-export" @click="${this.#handleExport}"> 41 + <button class="action" id="settings-export" @click="${this 42 + .#handleExport}"> 40 43 Export 41 44 </button> 42 - <button class="action" id="settings-import" @click="${this.#handleImport}"> 45 + <button class="action" id="settings-import" @click="${this 46 + .#handleImport}"> 43 47 Import 44 48 </button> 45 49 </div>
+1647 -1
www/static/styles/civility.min.css
··· 1 1 @layer normalize, base, base-theme, components, theme, utilities; 2 2 3 - /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */@layer normalize{*,:after,:before{box-sizing:border-box}html{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{margin:0}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentcolor}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@layer base{:root{--black:hsl(var(--blackH),var(--blackS),var(--blackL));--gray:hsl(var(--grayH),var(--grayS),var(--grayL));--white:hsl(var(--whiteH),var(--whiteS),var(--whiteL));--blue:hsl(var(--blueH),var(--blueS),var(--blueL));--green:hsl(var(--greenH),var(--greenS),var(--greenL));--yellow:hsl(var(--yellowH),var(--yellowS),var(--yellowL));--orange:hsl(var(--orangeH),var(--orangeS),var(--orangeL));--red:hsl(var(--redH),var(--redS),var(--redL));--pink:hsl(var(--pinkH),var(--pinkS),var(--pinkL));--purple:hsl(var(--purpleH),var(--purpleS),var(--purpleL));--error:hsl(var(--errorH),var(--errorS),var(--errorL));--error-dull:hsl(var(--errorH),var(--errorS),var(--errorL),0.2);--warning:hsl(var(--warningH),var(--warningS),var(--warningL));--warning-dull:hsl(var(--warningH),var(--warningS),var(--warningL),0.2);--info:hsl(var(--infoH),var(--infoS),var(--infoL));--info-dull:hsl(var(--infoH),var(--infoS),var(--infoL),0.2);--success:hsl(var(--successH),var(--successS),var(--successL));--success-dull:hsl(var(--successH),var(--successS),var(--successL),0.2);--background:hsl(var(--backgroundH),var(--backgroundS),var(--backgroundL));--background-dull:hsl(var(--backgroundH),var(--backgroundS),var(--backgroundL),0.2);--body:hsl(var(--bodyH),var(--bodyS),var(--bodyL));--body-dull:hsl(var(--bodyH),var(--bodyS),var(--bodyL),0.2);--secondary:hsl(var(--secondaryH),var(--secondaryS),var(--secondaryL));--secondary-dull:hsl(var(--secondaryH),var(--secondaryS),var(--secondaryL),0.2);--primary:hsl(var(--primaryH),var(--primaryS),var(--primaryL));--primary-dull:hsl(var(--primaryH),var(--primaryS),var(--primaryL),0.2)}form{margin:0 0 var(--s4) 0}fieldset{border:1px solid;border-radius:var(--br-base);margin:0 0 var(--s3) 0;padding:var(--s3)}legend{font-weight:var(--fw-semibold);opacity:1;padding:0 var(--s2)}label{display:block;font-weight:var(--fw-medium);margin-bottom:var(--s2)}input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{border:1px solid;border-radius:var(--br-base);box-sizing:border-box;font-size:var(--f5);line-height:var(--lh-base);margin-bottom:var(--s3);padding:var(--s3);transition:border-color var(--transition-base),box-shadow var(--transition-base),opacity var(--transition-base)}input:focus,select:focus,textarea:focus{opacity:1;outline:2px solid currentColor;outline-offset:2px}button,input[type=button],input[type=submit]{background:none;border:1px solid;border-radius:var(--br-base);color:inherit;cursor:pointer;display:inline-block;font-size:var(--f5);font-weight:var(--fw-medium);padding:var(--s2) var(--s2);text-decoration:none;transition:opacity var(--transition-base),transform var(--transition-fast)}button:hover,input[type=button]:hover,input[type=submit]:hover{opacity:.8;transform:translateY(-1px)}button:active,input[type=button]:active,input[type=submit]:active{transform:translateY(0)}button:disabled,input[type=button]:disabled,input[type=submit]:disabled{cursor:not-allowed;opacity:.3;transform:none}:root{--nav-drawer-duration:250ms;--nav-footer-height:72px}body{background-color:var(--background);color:var(--body);display:grid;grid-template-areas:"banner banner banner" "header header header" "subhead subhead subhead" "navhd mainhd aside" "nav main aside" "navft mainft aside" "footer footer footer";grid-template-columns:var(--nav-width,auto) 1fr var(--aside-width,0);grid-template-rows:auto auto auto auto 1fr auto auto;margin:0;min-height:100vh}}@layer base{}@layer base{body:has(footer[fixed]){padding-bottom:var(--footer-height,56px)}body>header{grid-area:header}body>nav{grid-area:nav}body>main{grid-area:main}body>aside{grid-area:aside}body>footer{grid-area:footer}body>header{background:var(--background);margin:0;padding:0;position:sticky;top:var(--banner-height,0);z-index:var(--z-sticky)}body>nav{align-self:start;display:flex;flex-direction:column;gap:var(--s1);max-height:calc(100vh - var(--banner-height, 0px) - var(--header-height, 56px) - var(--footer-height, 76px));overflow-y:auto;padding:var(--s3) var(--s2);padding-bottom:calc(var(--nav-footer-height) + var(--s3));position:sticky;top:calc(var(--banner-height, 0px) + var(--header-height, 56px))}body:not(:has(ui-nav-footer))>nav{padding-bottom:var(--s3)}body>nav ul{display:flex;flex-direction:column;gap:var(--s1);list-style:none;margin:0;padding:0}body>nav li{position:relative}body>nav a{border-radius:var(--br-base);color:inherit;display:block;font-weight:var(--fw-medium);padding:var(--s2) var(--s3);text-decoration:none;transition:opacity var(--transition-base)}body>nav a:hover{opacity:.7}body>nav a[aria-current=page]{cursor:default;opacity:.5}body>nav ul ul{border-left:1px solid;margin-left:var(--s3);margin-top:var(--s1);padding-left:var(--s3)}body>main{box-sizing:border-box;margin:0 auto;max-width:var(--main-max-width,var(--max-width-lg));padding:var(--main-padding,0);width:100%}body>aside{align-self:start;max-height:calc(100vh - var(--banner-height, 0px) - var(--header-height, 56px));overflow-y:auto;padding:var(--aside-padding,var(--s3) var(--s2));position:sticky;top:calc(var(--banner-height, 0px) + var(--header-height, 56px))}body>footer{background:var(--background);border-top:1px solid;padding:var(--footer-padding,var(--s3) var(--container-padding))}body>footer[fixed]{border-top:1px solid;bottom:0;grid-area:unset;left:0;position:fixed;right:0;z-index:var(--z-fixed)}ui-banner{grid-area:banner;top:0;z-index:var(--z-sticky)}ui-banner,ui-sub-header{background:var(--background);display:block;position:sticky}ui-sub-header{grid-area:subhead;top:calc(var(--banner-height, 0px) + var(--header-height, 0px));z-index:calc(var(--z-sticky) - 1)}ui-nav-header{align-self:start;grid-area:navhd;padding:var(--s3) var(--s2) var(--s2);top:calc(var(--banner-height, 0px) + var(--header-height, 56px))}ui-nav-footer,ui-nav-header{background:var(--background);display:block;position:sticky;z-index:var(--z-sticky)}ui-nav-footer{align-self:end;bottom:var(--footer-height,0);grid-area:navft;padding:var(--s2) var(--s2) var(--s3)}ui-main-header{display:block;grid-area:mainhd;padding:var(--s3) var(--container-padding) var(--s2)}ui-main-footer{display:block;grid-area:mainft;padding:var(--s2) var(--container-padding) var(--s3)}#nav-toggle,ui-nav-toggle{display:none}ui-nav-toggle label[for=nav-toggle]{align-items:center;background:transparent;border:1px solid;border-radius:var(--br-base);cursor:pointer;display:flex;flex-direction:column;gap:4px;height:44px;justify-content:center;padding:0;transition:opacity var(--transition-base);width:44px}ui-nav-toggle label[for=nav-toggle]:hover{opacity:.7}ui-nav-toggle label[for=nav-toggle] span,ui-nav-toggle label[for=nav-toggle]:after,ui-nav-toggle label[for=nav-toggle]:before{background:currentColor;content:"";display:block;height:2px;transition:all .3s ease;width:20px}ui-nav-toggle label[for=nav-toggle] span{pointer-events:none}#nav-toggle:checked~* ui-nav-toggle label[for=nav-toggle]:before,#nav-toggle:checked~header ui-nav-toggle label[for=nav-toggle]:before{transform:translateY(6px) rotate(45deg)}#nav-toggle:checked~* ui-nav-toggle label[for=nav-toggle] span,#nav-toggle:checked~header ui-nav-toggle label[for=nav-toggle] span{opacity:0}#nav-toggle:checked~* ui-nav-toggle label[for=nav-toggle]:after,#nav-toggle:checked~header ui-nav-toggle label[for=nav-toggle]:after{transform:translateY(-6px) rotate(-45deg)}label.nav-backdrop{background:rgba(0,0,0,.5);bottom:0;cursor:pointer;display:none;height:100vh;left:0;opacity:0;position:fixed;right:0;top:0;transition:var(--nav-drawer-duration) allow-discrete;transition-property:display,opacity,overlay;width:100vw;z-index:calc(var(--z-fixed) + 1)}#nav-toggle:checked~label.nav-backdrop{display:block;opacity:1}@starting-style{#nav-toggle:checked~label.nav-backdrop{opacity:0}}@media (max-width:900px){body{grid-template-areas:"banner" "header" "subhead" "mainhd" "main" "mainft" "footer";grid-template-columns:1fr;grid-template-rows:auto auto auto auto 1fr auto auto}body>aside,body>nav,ui-nav-footer,ui-nav-header{display:none}ui-nav-toggle{display:block}body>nav{background:var(--background);bottom:0;box-shadow:4px 0 12px rgba(0,0,0,.15);height:100vh;left:0;max-height:calc(100vh - var(--nav-header-height, 56px));overflow-y:auto;padding:var(--s3);padding-bottom:calc(var(--nav-footer-height) + var(--s3));padding-top:calc(var(--banner-height, 0px) + var(--header-height, 56px) + var(--s3));position:fixed;top:0;transform:translateX(-100%);transition:var(--nav-drawer-duration) allow-discrete;transition-property:display,transform;width:min(80vw,320px);z-index:calc(var(--z-fixed) + 2)}body:not(:has(ui-nav-footer))>nav{max-height:100vh}#nav-toggle:checked~nav{display:flex;transform:translateX(0)}@starting-style{#nav-toggle:checked~nav{transform:translateX(-100%)}}ui-nav-header{align-items:center;background:var(--background);border-bottom:1px solid;height:var(--header-height,56px);left:0;position:fixed;top:var(--banner-height,0);transform:translateX(-100%);transition:var(--nav-drawer-duration) allow-discrete;transition-property:display,transform;width:min(80vw,320px);z-index:calc(var(--z-fixed) + 3)}#nav-toggle:checked~ui-nav-header{display:flex;transform:translateX(0)}@starting-style{#nav-toggle:checked~ui-nav-header{transform:translateX(-100%)}}ui-nav-footer{align-items:center;background:var(--background);border-top:1px solid;bottom:0;height:var(--nav-footer-height);left:0;position:fixed;transform:translateX(-100%);transition:var(--nav-drawer-duration) allow-discrete;transition-property:display,transform;width:min(80vw,320px);z-index:calc(var(--z-fixed) + 3)}#nav-toggle:checked~ui-nav-footer{display:flex;transform:translateX(0)}@starting-style{#nav-toggle:checked~ui-nav-footer{transform:translateX(-100%)}}}}@layer base{h1,h2,h3,h4,h5,h6{font-weight:var(--fw-semibold);line-height:var(--lh-tight);margin:0 0 var(--s3) 0}h1{font-size:var(--f1)}h2{font-size:var(--f2)}h3{font-size:var(--f3)}h4{font-size:var(--f4)}h5{font-size:var(--f5)}h6{font-size:var(--f6)}a{color:var(--primary);text-decoration:underline;transition:color var(--transition-base)}@media (max-width:768px){h1{font-size:var(--f3)}h2,h3{font-size:var(--f4)}}@media (max-width:480px){main{padding:var(--s3)}h1{font-size:var(--f3)}h2{font-size:var(--f4)}}}@layer base{html{background-color:var(--background);color:var(--body);font-family:var(--font-family-base);font-size:var(--f5);height:100%;line-height:var(--lh-base)}p{line-height:var(--lh-relaxed);text-wrap:balance}ol,p,ul{margin:0 0 var(--s3) 0}ol,ul{padding:var(--s2) 0 var(--s2) var(--s4)}li{margin-bottom:var(--s2)}table{border-collapse:collapse;margin:0 0 var(--s4) 0;width:100%}td,th{border-bottom:1px solid;opacity:.9;padding:var(--s3) var(--s3);text-align:left}th{font-weight:var(--fw-semibold);opacity:1}code,kbd,samp{border:1px solid;border-radius:var(--br-sm);font-family:var(--font-family-mono);font-size:.9em;padding:.2em .4em}pre{border:1px solid;border-radius:var(--br-base);margin:0 0 var(--s3) 0;overflow-x:auto;padding:var(--s3)}pre code{border:none;opacity:1;padding:0}[role=tablist]{border-bottom:1px solid;display:flex;margin-bottom:var(--s3);opacity:.8}[role=tab]{background:none;border:none;border-bottom:2px solid transparent;border-radius:0;cursor:pointer;font-weight:var(--fw-medium);opacity:1;padding:var(--s3) var(--s3);transition:all var(--transition-base)}[role=tab]:hover{opacity:1}[role=tab][aria-selected=true]{border-bottom-color:currentColor;opacity:1}[role=tabpanel]{padding:var(--s3) 0}[role=tabpanel][hidden]{display:none}details{margin:0 0 var(--s2) 0}details,summary{border-radius:var(--br-base)}summary{cursor:pointer;font-weight:var(--fw-medium);opacity:1;padding:var(--s2);transition:opacity var(--transition-base)}summary:hover{opacity:.7}details[open] summary{border-radius:var(--br-base) var(--br-base) 0 0}details>:not(summary):not(ul){padding:var(--s2)}:focus-visible{outline:2px solid currentColor;outline-offset:2px}.skip-to-main{background:currentColor;border-radius:var(--br-base);color:#fff;left:6px;padding:8px;position:absolute;text-decoration:none;top:-40px;z-index:var(--z-tooltip)}.skip-to-main:focus{top:6px}}@layer base-theme{:root{--font-family-base:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--font-family-mono:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;--f-headline:6rem;--f-subheadline:5rem;--f1:3rem;--f2:2.25rem;--f3:1.5rem;--f4:1.25rem;--f5:1rem;--f6:0.875rem;--f7:0.75rem;--s0:0;--s1:0.25rem;--s2:0.5rem;--s3:1rem;--s4:2rem;--s5:3rem;--max-width-sm:640px;--max-width-md:768px;--max-width-lg:1024px;--max-width-xl:1280px;--max-width-2xl:1536px;--banner-height:0px;--header-height:56px;--sub-header-height:0px;--fw-normal:400;--fw-medium:500;--fw-semibold:600;--fw-bold:700;--lh-tight:1.2;--lh-base:1.5;--lh-relaxed:1.7;--br-none:0;--br-sm:0.25rem;--br-base:0.5rem;--br-lg:0.75rem;--br-xl:1rem;--br-full:9999px;--shadow-sm:0 1px 2px 0 rgba(0,0,0,.05);--shadow-base:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06);--shadow-md:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);--shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);--shadow-xl:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);--transition-fast:0.15s ease-in-out;--transition-base:0.2s ease-in-out;--transition-slow:0.3s ease-in-out;--z-dropdown:1000;--z-sticky:1020;--z-fixed:1030;--z-modal-backdrop:1040;--z-modal:1050;--z-popover:1060;--z-tooltip:1070;--grid-columns:12;--container-padding:var(--s3);--ease-in:cubic-bezier(0.4,0,1,1);--ease-out:cubic-bezier(0,0,0.2,1);--ease-in-out:cubic-bezier(0.4,0,0.2,1);--blackH:0;--blackS:0%;--blackL:0%;--grayH:220;--grayS:14%;--grayL:41%;--whiteH:0;--whiteS:0%;--whiteL:100%;--blueH:217;--blueS:91%;--blueL:60%;--greenH:142;--greenS:71%;--greenL:45%;--yellowH:45;--yellowS:93%;--yellowL:47%;--orangeH:25;--orangeS:95%;--orangeL:53%;--redH:0;--redS:84%;--redL:60%;--purpleH:262;--purpleS:83%;--purpleL:58%;--errorH:var(--redH);--errorS:var(--redS);--errorL:var(--redL);--warningH:var(--orangeH);--warningS:var(--orangeS);--warningL:var(--orangeL);--infoH:var(--blueH);--infoS:var(--blueS);--infoL:var(--blueL);--successH:var(--greenH);--successS:var(--greenS);--successL:var(--greenL);--backgroundH:var(--whiteH);--backgroundS:var(--whiteS);--backgroundL:var(--whiteL);--bodyH:var(--blackH);--bodyS:var(--blackS);--bodyL:13%;--secondaryH:var(--grayH);--secondaryS:var(--grayS);--secondaryL:var(--grayL);--primaryH:var(--blueH);--primaryS:var(--blueS);--primaryL:var(--blueL)}}@layer components{ui-card{border:1px solid;border-radius:var(--br-lg);box-shadow:var(--shadow-sm);display:block;overflow:hidden;transition:all var(--transition-base)}ui-card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px)}ui-card[clickable]{cursor:pointer}ui-card[clickable]:active{transform:translateY(0)}ui-badge{border:1px solid;border-radius:var(--br-full);display:inline-block;font-size:var(--f7);font-weight:var(--fw-medium);letter-spacing:.05em;padding:var(--s1) var(--s2);text-transform:uppercase}ui-badge[variant=success]{background-color:var(--success-dull);color:var(--success)}ui-badge[variant=warning]{background-color:var(--warning-dull);color:var(--warning)}ui-badge[variant=error]{background-color:var(--error-dull);color:var(--error)}ui-badge[variant=info]{background-color:var(--info-dull);color:var(--info)}ui-button-group{display:flex;flex-wrap:wrap;gap:var(--s2)}ui-button-group[orientation=vertical]{flex-direction:column}ui-button-group[attached]{gap:0}ui-button-group[attached]>button:not(:first-child),ui-button-group[attached]>input:not(:first-child){border-bottom-left-radius:0;border-left:none;border-top-left-radius:0}ui-button-group[attached]>button:not(:last-child),ui-button-group[attached]>input:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}ui-grid{display:grid;gap:var(--s3);grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}ui-grid[cols="1"]{grid-template-columns:1fr}ui-grid[cols="2"]{grid-template-columns:repeat(2,1fr)}ui-grid[cols="3"]{grid-template-columns:repeat(3,1fr)}ui-grid[cols="4"]{grid-template-columns:repeat(4,1fr)}@media (max-width:768px){ui-grid[cols="2"],ui-grid[cols="3"],ui-grid[cols="4"]{grid-template-columns:1fr}}ui-stack{display:flex;flex-direction:column;gap:var(--s3)}ui-stack[spacing=xs]{gap:var(--s1)}ui-stack[spacing=sm]{gap:var(--s2)}ui-stack[spacing=lg],ui-stack[spacing=md],ui-stack[spacing=xl]{gap:var(--s3)}ui-stack[direction=row]{align-items:center;flex-direction:row}ui-progress{border:1px solid;height:8px;overflow:hidden;width:100%}ui-progress,ui-progress:before{border-radius:var(--br-full);display:block}ui-progress:before{background-color:currentColor;content:"";height:100%;transition:width var(--transition-base);width:var(--progress-value,0)}ui-progress[variant=success]:before{background-color:var(--success)}ui-progress[variant=warning]:before{background-color:var(--warning)}ui-progress[variant=error]:before{background-color:var(--error)}ui-alert{border:1px solid;border-radius:var(--br-base);display:block;padding:var(--s3)}ui-alert[variant=success]{background-color:var(--success-dull);border-color:var(--success);color:var(--success)}ui-alert[variant=warning]{background-color:var(--warning-dull);border-color:var(--warning);color:var(--warning)}ui-alert[variant=error]{background-color:var(--error-dull);border-color:var(--error);color:var(--error)}ui-alert[variant=info]{background-color:var(--info-dull);border-color:var(--info);color:var(--info)}ui-divider{border:none;border-top:1px solid;display:block;height:1px;margin:var(--s4) 0}ui-divider[orientation=vertical]{height:auto;margin:0 var(--s3);width:1px}ui-spinner{animation:spin 1s linear infinite;border-color:currentcolor transparent;border-radius:50%;border-style:solid;border-width:2px;display:inline-block;height:20px;width:20px}ui-spinner[size=sm]{height:16px;width:16px}ui-spinner[size=lg]{border-width:3px;height:32px;width:32px}ui-breadcrumb{align-items:center;display:flex;font-size:var(--f5);gap:var(--s2)}ui-breadcrumb a{border-radius:var(--br-sm);color:var(--primary);padding:var(--s1) var(--s2);text-decoration:none;transition:opacity var(--transition-base)}ui-breadcrumb a:hover{opacity:.7}ui-breadcrumb:before{content:attr(separator);margin:0 var(--s1);opacity:.5}ui-breadcrumb:first-child:before{display:none}ui-avatar{border:1px solid;border-radius:var(--br-full);display:inline-block;font-size:var(--f5);font-weight:var(--fw-medium);height:40px;line-height:40px;overflow:hidden;text-align:center;width:40px}ui-avatar[size=sm]{font-size:var(--f7);height:32px;line-height:32px;width:32px}ui-avatar[size=lg]{font-size:var(--f4);height:56px;line-height:56px;width:56px}ui-avatar img{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}ui-dropdown{display:inline-block;position:relative}ui-dropdown-content{background:var(--background);border:1px solid;border-radius:var(--br-base);box-shadow:var(--shadow-lg);left:0;min-width:200px;opacity:0;position:absolute;top:100%;transform:translateY(-8px);transition:all var(--transition-base);visibility:hidden;z-index:var(--z-dropdown)}ui-dropdown[open] ui-dropdown-content{opacity:1;transform:translateY(0);visibility:visible}ui-dropdown-item{cursor:pointer;display:block;padding:var(--s3);text-decoration:none;transition:opacity var(--transition-base)}ui-dropdown-item:hover{opacity:.7}ui-dropdown-item[active]{font-weight:var(--fw-medium);opacity:1}@media (max-width:768px){ui-dropdown-content{left:auto;min-width:150px;right:0}}.drawer-toggle{display:none}.drawer-backdrop{background:rgba(0,0,0,.5);bottom:0;cursor:pointer;display:none;height:100vh;left:0;opacity:0;position:fixed;right:0;top:0;transition:allow-discrete .25s;transition-property:display,opacity,overlay;width:100vw;z-index:calc(var(--z-fixed) + 1)}.drawer-toggle:checked~.drawer-backdrop{display:block;opacity:1}@starting-style{.drawer-toggle:checked~.drawer-backdrop{opacity:0}}ui-drawer{background:var(--background);box-shadow:4px 0 12px rgba(0,0,0,.15);display:none;overflow-y:auto;padding:var(--s3);position:fixed;transition:allow-discrete .25s;transition-property:display,transform;z-index:calc(var(--z-fixed) + 2)}ui-drawer,ui-drawer[position=left]{bottom:0;height:100vh;left:0;top:0;transform:translateX(-100%);width:min(80vw,320px)}.drawer-toggle:checked~* ui-drawer[position=left],.drawer-toggle:checked~ui-drawer[position=left]{display:block;transform:translateX(0)}@starting-style{.drawer-toggle:checked~* ui-drawer[position=left],.drawer-toggle:checked~ui-drawer[position=left]{transform:translateX(-100%)}}ui-drawer[position=right]{bottom:0;box-shadow:-4px 0 12px rgba(0,0,0,.15);height:100vh;left:auto;right:0;top:0;transform:translateX(100%);width:min(80vw,320px)}.drawer-toggle:checked~* ui-drawer[position=right],.drawer-toggle:checked~ui-drawer[position=right]{display:block;transform:translateX(0)}@starting-style{.drawer-toggle:checked~* ui-drawer[position=right],.drawer-toggle:checked~ui-drawer[position=right]{transform:translateX(100%)}}ui-drawer[position=top]{bottom:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);height:min(80vh,400px);left:0;right:0;top:0;transform:translateY(-100%);width:100vw}.drawer-toggle:checked~* ui-drawer[position=top],.drawer-toggle:checked~ui-drawer[position=top]{display:block;transform:translateY(0)}@starting-style{.drawer-toggle:checked~* ui-drawer[position=top],.drawer-toggle:checked~ui-drawer[position=top]{transform:translateY(-100%)}}ui-drawer[position=bottom]{bottom:0;box-shadow:0 -4px 12px rgba(0,0,0,.15);height:min(80vh,400px);left:0;right:0;top:auto;transform:translateY(100%);width:100vw}.drawer-toggle:checked~* ui-drawer[position=bottom],.drawer-toggle:checked~ui-drawer[position=bottom]{display:block;transform:translateY(0)}@starting-style{.drawer-toggle:checked~* ui-drawer[position=bottom],.drawer-toggle:checked~ui-drawer[position=bottom]{transform:translateY(100%)}}ui-bottom-bar{align-items:stretch;display:flex;flex-direction:row;gap:0;height:100%;margin:0;padding:0;width:100%}ui-bottom-bar>a,ui-bottom-bar>button{align-items:center;background:transparent;border:none;color:inherit;cursor:pointer;display:flex;flex:1;flex-direction:column;font-size:var(--f7);font-weight:var(--fw-medium);gap:2px;justify-content:center;opacity:1;padding:var(--s1) var(--s2);text-decoration:none;transition:opacity var(--transition-base)}ui-bottom-bar>a:hover,ui-bottom-bar>button:hover{opacity:.7}ui-bottom-bar>button:disabled{cursor:not-allowed;opacity:.3}ui-bottom-bar>a[aria-current=page],ui-bottom-bar>button[aria-current=page]{opacity:.5}ui-bottom-bar .icon,ui-bottom-bar ui-icon{font-size:var(--f4);line-height:1}ui-bottom-bar[hide-text]>a>span:not(.icon),ui-bottom-bar[hide-text]>button>span:not(.icon){display:none}ui-bottom-bar[hide-text]>a,ui-bottom-bar[hide-text]>button{gap:0}ui-bottom-bar[orientation=horizontal]>a,ui-bottom-bar[orientation=horizontal]>button{flex-direction:row;gap:var(--s2)}ui-pwa-version{display:block}ui-pwa-version p{margin:0 0 var(--s2) 0}ui-pwa-version button{margin-top:var(--s1)}ui-counter{align-items:stretch;display:flex;gap:var(--s2)}ui-counter button{align-items:center;display:flex;flex-shrink:0;justify-content:center}ui-counter input[type=number]{-webkit-appearance:textfield;appearance:textfield;-moz-appearance:textfield;flex:1;margin:0;padding:0;text-align:center}ui-counter input[type=number]::-webkit-inner-spin-button,ui-counter input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none}ui-dialog{display:contents}dialog{background:Canvas;border:none;border-radius:.5rem;box-shadow:0 8px 32px rgba(0,0,0,.2);max-width:min(90vw,560px);opacity:0;padding:0;transform:scale(.6);transform-origin:top center;transition:allow-discrete .25s;transition-property:display,opacity,overlay,transform;width:100%}dialog::backdrop{background-color:#000;opacity:0;transition:allow-discrete .25s;transition-property:display,opacity,overlay}dialog[open]{opacity:1;transform:scale(1)}dialog[open]::backdrop{opacity:.5}@starting-style{dialog[open]{opacity:0;transform:scale(.6)}dialog[open]::backdrop{opacity:0}}}@layer utilities{.flex{display:flex}.flex-column{flex-direction:column}.flex-row{flex-direction:row}.items-center{align-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.justify-center{justify-content:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.w-100{width:100%}.mw6{max-width:var(--max-width-sm)}.measure{max-width:30em}.container-sm{max-width:var(--max-width-sm)}.container-md,.container-sm{margin:0 auto;padding:0 var(--container-padding)}.container-md{max-width:var(--max-width-md)}.container-lg{max-width:var(--max-width-lg)}.container-lg,.container-xl{margin:0 auto;padding:0 var(--container-padding)}.container-xl{max-width:var(--max-width-xl)}.container-fluid{padding:0 var(--container-padding);width:100%}.pa0{padding:0}.pa1{padding:var(--s1)}.pa2{padding:var(--s2)}.pa3{padding:var(--s3)}.pa4{padding:var(--s4)}.ma0{margin:0}.ma1{margin:var(--s1)}.ma2{margin:var(--s2)}.ma3{margin:var(--s3)}.mh-auto{margin-left:auto;margin-right:auto}.mv3{margin-bottom:var(--s3);margin-top:var(--s3)}.mb2{margin-bottom:var(--s2)}.mb0{margin-bottom:0}.mt3{margin-top:var(--s3)}.mt2{margin-top:var(--s2)}.mr3{margin-right:var(--s3)}.pr3{padding-right:var(--s3)}.pl2{padding-left:var(--s2)}.pt2{padding-top:var(--s2)}.pb4{padding-bottom:var(--s4)}.pb0{padding-bottom:0}.pb3{padding-bottom:var(--s3)}.f3{font-size:var(--f3)}.f4{font-size:var(--f4)}.f5{font-size:var(--f5)}.f6{font-size:var(--f6)}.tc{text-align:center}.tr{text-align:right}.b{font-weight:var(--fw-bold)}.br2{border-radius:var(--br2)}.ba{border:1px solid}.b1{border-width:1px}.t-border{border:1px solid transparent}.border{border:1px solid}.list{list-style:none}.cursor,.pointer{cursor:pointer}.o-30{opacity:.3}.o-40{opacity:.4}.o-60{opacity:.6}.underline{text-decoration:underline}.link{text-decoration:none}.db{display:block}.h2{height:2rem}.h3{height:3rem}.input{border:1px solid;border-radius:var(--br2);padding:var(--s2)}.input-reset{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent}.shadow{box-shadow:var(--shadow-base)}.pv2{padding-bottom:var(--s2);padding-top:var(--s2)}.pv1{padding-bottom:var(--s1);padding-top:var(--s1)}.ph2{padding-left:var(--s2);padding-right:var(--s2)}.lh-copy{line-height:var(--lh-copy)}.center{text-align:center}.flex-wrap{flex-wrap:wrap}.no-select{cursor:default;-webkit-user-select:none;-moz-user-select:none;user-select:none}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.absolute-center{left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}.overflow-hidden,.text-ellipsis{overflow:hidden}.text-ellipsis{text-overflow:ellipsis;white-space:nowrap}.z-dropdown{z-index:var(--z-dropdown)}.z-modal{z-index:var(--z-modal)}.z-tooltip{z-index:var(--z-tooltip)}} 3 + /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ 4 + @layer normalize { 5 + *, :after, :before { 6 + box-sizing: border-box; 7 + } 8 + html { 9 + font-family: 10 + system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color 11 + Emoji, Segoe UI Emoji; 12 + line-height: 1.15; 13 + -webkit-text-size-adjust: 100%; 14 + -moz-tab-size: 4; 15 + -o-tab-size: 4; 16 + tab-size: 4; 17 + } 18 + body { 19 + margin: 0; 20 + } 21 + b, strong { 22 + font-weight: bolder; 23 + } 24 + code, kbd, pre, samp { 25 + font-family: 26 + ui-monospace, SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 27 + font-size: 1em; 28 + } 29 + small { 30 + font-size: 80%; 31 + } 32 + sub, sup { 33 + font-size: 75%; 34 + line-height: 0; 35 + position: relative; 36 + vertical-align: baseline; 37 + } 38 + sub { 39 + bottom: -0.25em; 40 + } 41 + sup { 42 + top: -0.5em; 43 + } 44 + table { 45 + border-color: currentcolor; 46 + } 47 + button, input, optgroup, select, textarea { 48 + font-family: inherit; 49 + font-size: 100%; 50 + line-height: 1.15; 51 + margin: 0; 52 + } 53 + [type='button'], [type='reset'], [type='submit'], button { 54 + -webkit-appearance: button; 55 + } 56 + legend { 57 + padding: 0; 58 + } 59 + progress { 60 + vertical-align: baseline; 61 + } 62 + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { 63 + height: auto; 64 + } 65 + [type='search'] { 66 + -webkit-appearance: textfield; 67 + outline-offset: -2px; 68 + } 69 + ::-webkit-search-decoration { 70 + -webkit-appearance: none; 71 + } 72 + ::-webkit-file-upload-button { 73 + -webkit-appearance: button; 74 + font: inherit; 75 + } 76 + summary { 77 + display: list-item; 78 + } 79 + } 80 + @media (prefers-reduced-motion: reduce) { 81 + * { 82 + animation-duration: 0.01ms !important; 83 + animation-iteration-count: 1 !important; 84 + transition-duration: 0.01ms !important; 85 + } 86 + } 87 + @keyframes spin { 88 + 0% { 89 + transform: rotate(0deg); 90 + } 91 + to { 92 + transform: rotate(1turn); 93 + } 94 + } 95 + @layer base { 96 + :root { 97 + --black: hsl(var(--blackH), var(--blackS), var(--blackL)); 98 + --gray: hsl(var(--grayH), var(--grayS), var(--grayL)); 99 + --white: hsl(var(--whiteH), var(--whiteS), var(--whiteL)); 100 + --blue: hsl(var(--blueH), var(--blueS), var(--blueL)); 101 + --green: hsl(var(--greenH), var(--greenS), var(--greenL)); 102 + --yellow: hsl(var(--yellowH), var(--yellowS), var(--yellowL)); 103 + --orange: hsl(var(--orangeH), var(--orangeS), var(--orangeL)); 104 + --red: hsl(var(--redH), var(--redS), var(--redL)); 105 + --pink: hsl(var(--pinkH), var(--pinkS), var(--pinkL)); 106 + --purple: hsl(var(--purpleH), var(--purpleS), var(--purpleL)); 107 + --error: hsl(var(--errorH), var(--errorS), var(--errorL)); 108 + --error-dull: hsl(var(--errorH), var(--errorS), var(--errorL), 0.2); 109 + --warning: hsl(var(--warningH), var(--warningS), var(--warningL)); 110 + --warning-dull: hsl(var(--warningH), var(--warningS), var(--warningL), 0.2); 111 + --info: hsl(var(--infoH), var(--infoS), var(--infoL)); 112 + --info-dull: hsl(var(--infoH), var(--infoS), var(--infoL), 0.2); 113 + --success: hsl(var(--successH), var(--successS), var(--successL)); 114 + --success-dull: hsl(var(--successH), var(--successS), var(--successL), 0.2); 115 + --background: hsl( 116 + var(--backgroundH), 117 + var(--backgroundS), 118 + var(--backgroundL) 119 + ); 120 + --background-dull: hsl( 121 + var(--backgroundH), 122 + var(--backgroundS), 123 + var(--backgroundL), 124 + 0.2 125 + ); 126 + --body: hsl(var(--bodyH), var(--bodyS), var(--bodyL)); 127 + --body-dull: hsl(var(--bodyH), var(--bodyS), var(--bodyL), 0.2); 128 + --secondary: hsl(var(--secondaryH), var(--secondaryS), var(--secondaryL)); 129 + --secondary-dull: hsl( 130 + var(--secondaryH), 131 + var(--secondaryS), 132 + var(--secondaryL), 133 + 0.2 134 + ); 135 + --primary: hsl(var(--primaryH), var(--primaryS), var(--primaryL)); 136 + --primary-dull: hsl(var(--primaryH), var(--primaryS), var(--primaryL), 0.2); 137 + } 138 + form { 139 + margin: 0 0 var(--s4) 0; 140 + } 141 + fieldset { 142 + border: 1px solid; 143 + border-radius: var(--br-base); 144 + margin: 0 0 var(--s3) 0; 145 + padding: var(--s3); 146 + } 147 + legend { 148 + font-weight: var(--fw-semibold); 149 + opacity: 1; 150 + padding: 0 var(--s2); 151 + } 152 + label { 153 + display: block; 154 + font-weight: var(--fw-medium); 155 + margin-bottom: var(--s2); 156 + } 157 + input[type='email'], 158 + input[type='number'], 159 + input[type='password'], 160 + input[type='search'], 161 + input[type='tel'], 162 + input[type='text'], 163 + input[type='url'], 164 + select, 165 + textarea { 166 + border: 1px solid; 167 + border-radius: var(--br-base); 168 + box-sizing: border-box; 169 + font-size: var(--f5); 170 + line-height: var(--lh-base); 171 + margin-bottom: var(--s3); 172 + padding: var(--s3); 173 + transition: 174 + border-color var(--transition-base), 175 + box-shadow var(--transition-base), 176 + opacity var(--transition-base); 177 + } 178 + input:focus, select:focus, textarea:focus { 179 + opacity: 1; 180 + outline: 2px solid currentColor; 181 + outline-offset: 2px; 182 + } 183 + button, input[type='button'], input[type='submit'] { 184 + background: none; 185 + border: 1px solid; 186 + border-radius: var(--br-base); 187 + color: inherit; 188 + cursor: pointer; 189 + display: inline-block; 190 + font-size: var(--f5); 191 + font-weight: var(--fw-medium); 192 + padding: var(--s2) var(--s2); 193 + text-decoration: none; 194 + transition: 195 + opacity var(--transition-base), 196 + transform var(--transition-fast); 197 + } 198 + button:hover, input[type='button']:hover, input[type='submit']:hover { 199 + opacity: 0.8; 200 + transform: translateY(-1px); 201 + } 202 + button:active, input[type='button']:active, input[type='submit']:active { 203 + transform: translateY(0); 204 + } 205 + button:disabled, 206 + input[type='button']:disabled, 207 + input[type='submit']:disabled { 208 + cursor: not-allowed; 209 + opacity: 0.3; 210 + transform: none; 211 + } 212 + :root { 213 + --nav-drawer-duration: 250ms; 214 + --nav-footer-height: 72px; 215 + } 216 + body { 217 + background-color: var(--background); 218 + color: var(--body); 219 + display: grid; 220 + grid-template-areas: 221 + 'banner banner banner' 'header header header' 'subhead subhead subhead' 'navhd mainhd aside' 'nav main aside' 'navft mainft aside' 'footer footer footer'; 222 + grid-template-columns: var(--nav-width, auto) 1fr var(--aside-width, 0); 223 + grid-template-rows: auto auto auto auto 1fr auto auto; 224 + margin: 0; 225 + min-height: 100vh; 226 + } 227 + } 228 + @layer base {} 229 + @layer base { 230 + body:has(footer[fixed]) { 231 + padding-bottom: var(--footer-height, 56px); 232 + } 233 + body > header { 234 + grid-area: header; 235 + } 236 + body > nav { 237 + grid-area: nav; 238 + } 239 + body > main { 240 + grid-area: main; 241 + } 242 + body > aside { 243 + grid-area: aside; 244 + } 245 + body > footer { 246 + grid-area: footer; 247 + } 248 + body > header { 249 + background: var(--background); 250 + margin: 0; 251 + padding: 0; 252 + position: sticky; 253 + top: var(--banner-height, 0); 254 + z-index: var(--z-sticky); 255 + } 256 + body > nav { 257 + align-self: start; 258 + display: flex; 259 + flex-direction: column; 260 + gap: var(--s1); 261 + max-height: calc( 262 + 100vh - var(--banner-height, 0px) - var(--header-height, 56px) 263 + - var(--footer-height, 76px) 264 + ); 265 + overflow-y: auto; 266 + padding: var(--s3) var(--s2); 267 + padding-bottom: calc(var(--nav-footer-height) + var(--s3)); 268 + position: sticky; 269 + top: calc(var(--banner-height, 0px) + var(--header-height, 56px)); 270 + } 271 + body:not(:has(ui-nav-footer)) > nav { 272 + padding-bottom: var(--s3); 273 + } 274 + body > nav ul { 275 + display: flex; 276 + flex-direction: column; 277 + gap: var(--s1); 278 + list-style: none; 279 + margin: 0; 280 + padding: 0; 281 + } 282 + body > nav li { 283 + position: relative; 284 + } 285 + body > nav a { 286 + border-radius: var(--br-base); 287 + color: inherit; 288 + display: block; 289 + font-weight: var(--fw-medium); 290 + padding: var(--s2) var(--s3); 291 + text-decoration: none; 292 + transition: opacity var(--transition-base); 293 + } 294 + body > nav a:hover { 295 + opacity: 0.7; 296 + } 297 + body > nav a[aria-current='page'] { 298 + cursor: default; 299 + opacity: 0.5; 300 + } 301 + body > nav ul ul { 302 + border-left: 1px solid; 303 + margin-left: var(--s3); 304 + margin-top: var(--s1); 305 + padding-left: var(--s3); 306 + } 307 + body > main { 308 + box-sizing: border-box; 309 + margin: 0 auto; 310 + max-width: var(--main-max-width, var(--max-width-lg)); 311 + padding: var(--main-padding, 0); 312 + width: 100%; 313 + } 314 + body > aside { 315 + align-self: start; 316 + max-height: calc( 317 + 100vh - var(--banner-height, 0px) - var(--header-height, 56px) 318 + ); 319 + overflow-y: auto; 320 + padding: var(--aside-padding, var(--s3) var(--s2)); 321 + position: sticky; 322 + top: calc(var(--banner-height, 0px) + var(--header-height, 56px)); 323 + } 324 + body > footer { 325 + background: var(--background); 326 + border-top: 1px solid; 327 + padding: var(--footer-padding, var(--s3) var(--container-padding)); 328 + } 329 + body > footer[fixed] { 330 + border-top: 1px solid; 331 + bottom: 0; 332 + grid-area: unset; 333 + left: 0; 334 + position: fixed; 335 + right: 0; 336 + z-index: var(--z-fixed); 337 + } 338 + ui-banner { 339 + grid-area: banner; 340 + top: 0; 341 + z-index: var(--z-sticky); 342 + } 343 + ui-banner, ui-sub-header { 344 + background: var(--background); 345 + display: block; 346 + position: sticky; 347 + } 348 + ui-sub-header { 349 + grid-area: subhead; 350 + top: calc(var(--banner-height, 0px) + var(--header-height, 0px)); 351 + z-index: calc(var(--z-sticky) - 1); 352 + } 353 + ui-nav-header { 354 + align-self: start; 355 + grid-area: navhd; 356 + padding: var(--s3) var(--s2) var(--s2); 357 + top: calc(var(--banner-height, 0px) + var(--header-height, 56px)); 358 + } 359 + ui-nav-footer, ui-nav-header { 360 + background: var(--background); 361 + display: block; 362 + position: sticky; 363 + z-index: var(--z-sticky); 364 + } 365 + ui-nav-footer { 366 + align-self: end; 367 + bottom: var(--footer-height, 0); 368 + grid-area: navft; 369 + padding: var(--s2) var(--s2) var(--s3); 370 + } 371 + ui-main-header { 372 + display: block; 373 + grid-area: mainhd; 374 + padding: var(--s3) var(--container-padding) var(--s2); 375 + } 376 + ui-main-footer { 377 + display: block; 378 + grid-area: mainft; 379 + padding: var(--s2) var(--container-padding) var(--s3); 380 + } 381 + #nav-toggle, ui-nav-toggle { 382 + display: none; 383 + } 384 + ui-nav-toggle label[for='nav-toggle'] { 385 + align-items: center; 386 + background: transparent; 387 + border: 1px solid; 388 + border-radius: var(--br-base); 389 + cursor: pointer; 390 + display: flex; 391 + flex-direction: column; 392 + gap: 4px; 393 + height: 44px; 394 + justify-content: center; 395 + padding: 0; 396 + transition: opacity var(--transition-base); 397 + width: 44px; 398 + } 399 + ui-nav-toggle label[for='nav-toggle']:hover { 400 + opacity: 0.7; 401 + } 402 + ui-nav-toggle label[for='nav-toggle'] span, 403 + ui-nav-toggle label[for='nav-toggle']:after, 404 + ui-nav-toggle label[for='nav-toggle']:before { 405 + background: currentColor; 406 + content: ''; 407 + display: block; 408 + height: 2px; 409 + transition: all 0.3s ease; 410 + width: 20px; 411 + } 412 + ui-nav-toggle label[for='nav-toggle'] span { 413 + pointer-events: none; 414 + } 415 + #nav-toggle:checked ~ * ui-nav-toggle label[for='nav-toggle']:before, 416 + #nav-toggle:checked ~ header ui-nav-toggle label[for='nav-toggle']:before { 417 + transform: translateY(6px) rotate(45deg); 418 + } 419 + #nav-toggle:checked ~ * ui-nav-toggle label[for='nav-toggle'] span, 420 + #nav-toggle:checked ~ header ui-nav-toggle label[for='nav-toggle'] span { 421 + opacity: 0; 422 + } 423 + #nav-toggle:checked ~ * ui-nav-toggle label[for='nav-toggle']:after, 424 + #nav-toggle:checked ~ header ui-nav-toggle label[for='nav-toggle']:after { 425 + transform: translateY(-6px) rotate(-45deg); 426 + } 427 + label.nav-backdrop { 428 + background: rgba(0, 0, 0, 0.5); 429 + bottom: 0; 430 + cursor: pointer; 431 + display: none; 432 + height: 100vh; 433 + left: 0; 434 + opacity: 0; 435 + position: fixed; 436 + right: 0; 437 + top: 0; 438 + transition: var(--nav-drawer-duration) allow-discrete; 439 + transition-property: display, opacity, overlay; 440 + width: 100vw; 441 + z-index: calc(var(--z-fixed) + 1); 442 + } 443 + #nav-toggle:checked ~ label.nav-backdrop { 444 + display: block; 445 + opacity: 1; 446 + } 447 + @starting-style { 448 + #nav-toggle:checked ~ label.nav-backdrop { 449 + opacity: 0; 450 + } 451 + } 452 + @media (max-width: 900px) { 453 + body { 454 + grid-template-areas: 455 + 'banner' 'header' 'subhead' 'mainhd' 'main' 'mainft' 'footer'; 456 + grid-template-columns: 1fr; 457 + grid-template-rows: auto auto auto auto 1fr auto auto; 458 + } 459 + body > aside, body > nav, ui-nav-footer, ui-nav-header { 460 + display: none; 461 + } 462 + ui-nav-toggle { 463 + display: block; 464 + } 465 + body > nav { 466 + background: var(--background); 467 + bottom: 0; 468 + box-shadow: 4px 0 12px rgba(0, 0, 0, 0.15); 469 + height: 100vh; 470 + left: 0; 471 + max-height: calc(100vh - var(--nav-header-height, 56px)); 472 + overflow-y: auto; 473 + padding: var(--s3); 474 + padding-bottom: calc(var(--nav-footer-height) + var(--s3)); 475 + padding-top: calc( 476 + var(--banner-height, 0px) + var(--header-height, 56px) + var(--s3) 477 + ); 478 + position: fixed; 479 + top: 0; 480 + transform: translateX(-100%); 481 + transition: var(--nav-drawer-duration) allow-discrete; 482 + transition-property: display, transform; 483 + width: min(80vw, 320px); 484 + z-index: calc(var(--z-fixed) + 2); 485 + } 486 + body:not(:has(ui-nav-footer)) > nav { 487 + max-height: 100vh; 488 + } 489 + #nav-toggle:checked ~ nav { 490 + display: flex; 491 + transform: translateX(0); 492 + } 493 + @starting-style { 494 + #nav-toggle:checked ~ nav { 495 + transform: translateX(-100%); 496 + } 497 + } 498 + ui-nav-header { 499 + align-items: center; 500 + background: var(--background); 501 + border-bottom: 1px solid; 502 + height: var(--header-height, 56px); 503 + left: 0; 504 + position: fixed; 505 + top: var(--banner-height, 0); 506 + transform: translateX(-100%); 507 + transition: var(--nav-drawer-duration) allow-discrete; 508 + transition-property: display, transform; 509 + width: min(80vw, 320px); 510 + z-index: calc(var(--z-fixed) + 3); 511 + } 512 + #nav-toggle:checked ~ ui-nav-header { 513 + display: flex; 514 + transform: translateX(0); 515 + } 516 + @starting-style { 517 + #nav-toggle:checked ~ ui-nav-header { 518 + transform: translateX(-100%); 519 + } 520 + } 521 + ui-nav-footer { 522 + align-items: center; 523 + background: var(--background); 524 + border-top: 1px solid; 525 + bottom: 0; 526 + height: var(--nav-footer-height); 527 + left: 0; 528 + position: fixed; 529 + transform: translateX(-100%); 530 + transition: var(--nav-drawer-duration) allow-discrete; 531 + transition-property: display, transform; 532 + width: min(80vw, 320px); 533 + z-index: calc(var(--z-fixed) + 3); 534 + } 535 + #nav-toggle:checked ~ ui-nav-footer { 536 + display: flex; 537 + transform: translateX(0); 538 + } 539 + @starting-style { 540 + #nav-toggle:checked ~ ui-nav-footer { 541 + transform: translateX(-100%); 542 + } 543 + } 544 + } 545 + } 546 + @layer base { 547 + h1, h2, h3, h4, h5, h6 { 548 + font-weight: var(--fw-semibold); 549 + line-height: var(--lh-tight); 550 + margin: 0 0 var(--s3) 0; 551 + } 552 + h1 { 553 + font-size: var(--f1); 554 + } 555 + h2 { 556 + font-size: var(--f2); 557 + } 558 + h3 { 559 + font-size: var(--f3); 560 + } 561 + h4 { 562 + font-size: var(--f4); 563 + } 564 + h5 { 565 + font-size: var(--f5); 566 + } 567 + h6 { 568 + font-size: var(--f6); 569 + } 570 + a { 571 + color: var(--primary); 572 + text-decoration: underline; 573 + transition: color var(--transition-base); 574 + } 575 + @media (max-width: 768px) { 576 + h1 { 577 + font-size: var(--f3); 578 + } 579 + h2, h3 { 580 + font-size: var(--f4); 581 + } 582 + } 583 + @media (max-width: 480px) { 584 + main { 585 + padding: var(--s3); 586 + } 587 + h1 { 588 + font-size: var(--f3); 589 + } 590 + h2 { 591 + font-size: var(--f4); 592 + } 593 + } 594 + } 595 + @layer base { 596 + html { 597 + background-color: var(--background); 598 + color: var(--body); 599 + font-family: var(--font-family-base); 600 + font-size: var(--f5); 601 + height: 100%; 602 + line-height: var(--lh-base); 603 + } 604 + p { 605 + line-height: var(--lh-relaxed); 606 + text-wrap: balance; 607 + } 608 + ol, p, ul { 609 + margin: 0 0 var(--s3) 0; 610 + } 611 + ol, ul { 612 + padding: var(--s2) 0 var(--s2) var(--s4); 613 + } 614 + li { 615 + margin-bottom: var(--s2); 616 + } 617 + table { 618 + border-collapse: collapse; 619 + margin: 0 0 var(--s4) 0; 620 + width: 100%; 621 + } 622 + td, th { 623 + border-bottom: 1px solid; 624 + opacity: 0.9; 625 + padding: var(--s3) var(--s3); 626 + text-align: left; 627 + } 628 + th { 629 + font-weight: var(--fw-semibold); 630 + opacity: 1; 631 + } 632 + code, kbd, samp { 633 + border: 1px solid; 634 + border-radius: var(--br-sm); 635 + font-family: var(--font-family-mono); 636 + font-size: 0.9em; 637 + padding: 0.2em 0.4em; 638 + } 639 + pre { 640 + border: 1px solid; 641 + border-radius: var(--br-base); 642 + margin: 0 0 var(--s3) 0; 643 + overflow-x: auto; 644 + padding: var(--s3); 645 + } 646 + pre code { 647 + border: none; 648 + opacity: 1; 649 + padding: 0; 650 + } 651 + [role='tablist'] { 652 + border-bottom: 1px solid; 653 + display: flex; 654 + margin-bottom: var(--s3); 655 + opacity: 0.8; 656 + } 657 + [role='tab'] { 658 + background: none; 659 + border: none; 660 + border-bottom: 2px solid transparent; 661 + border-radius: 0; 662 + cursor: pointer; 663 + font-weight: var(--fw-medium); 664 + opacity: 1; 665 + padding: var(--s3) var(--s3); 666 + transition: all var(--transition-base); 667 + } 668 + [role='tab']:hover { 669 + opacity: 1; 670 + } 671 + [role='tab'][aria-selected='true'] { 672 + border-bottom-color: currentColor; 673 + opacity: 1; 674 + } 675 + [role='tabpanel'] { 676 + padding: var(--s3) 0; 677 + } 678 + [role='tabpanel'][hidden] { 679 + display: none; 680 + } 681 + details { 682 + margin: 0 0 var(--s2) 0; 683 + } 684 + details, summary { 685 + border-radius: var(--br-base); 686 + } 687 + summary { 688 + cursor: pointer; 689 + font-weight: var(--fw-medium); 690 + opacity: 1; 691 + padding: var(--s2); 692 + transition: opacity var(--transition-base); 693 + } 694 + summary:hover { 695 + opacity: 0.7; 696 + } 697 + details[open] summary { 698 + border-radius: var(--br-base) var(--br-base) 0 0; 699 + } 700 + details > :not(summary):not(ul) { 701 + padding: var(--s2); 702 + } 703 + :focus-visible { 704 + outline: 2px solid currentColor; 705 + outline-offset: 2px; 706 + } 707 + .skip-to-main { 708 + background: currentColor; 709 + border-radius: var(--br-base); 710 + color: #fff; 711 + left: 6px; 712 + padding: 8px; 713 + position: absolute; 714 + text-decoration: none; 715 + top: -40px; 716 + z-index: var(--z-tooltip); 717 + } 718 + .skip-to-main:focus { 719 + top: 6px; 720 + } 721 + } 722 + @layer base-theme { 723 + :root { 724 + --font-family-base: 725 + system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; 726 + --font-family-mono: 727 + ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace; 728 + --f-headline: 6rem; 729 + --f-subheadline: 5rem; 730 + --f1: 3rem; 731 + --f2: 2.25rem; 732 + --f3: 1.5rem; 733 + --f4: 1.25rem; 734 + --f5: 1rem; 735 + --f6: 0.875rem; 736 + --f7: 0.75rem; 737 + --s0: 0; 738 + --s1: 0.25rem; 739 + --s2: 0.5rem; 740 + --s3: 1rem; 741 + --s4: 2rem; 742 + --s5: 3rem; 743 + --max-width-sm: 640px; 744 + --max-width-md: 768px; 745 + --max-width-lg: 1024px; 746 + --max-width-xl: 1280px; 747 + --max-width-2xl: 1536px; 748 + --banner-height: 0px; 749 + --header-height: 56px; 750 + --sub-header-height: 0px; 751 + --fw-normal: 400; 752 + --fw-medium: 500; 753 + --fw-semibold: 600; 754 + --fw-bold: 700; 755 + --lh-tight: 1.2; 756 + --lh-base: 1.5; 757 + --lh-relaxed: 1.7; 758 + --br-none: 0; 759 + --br-sm: 0.25rem; 760 + --br-base: 0.5rem; 761 + --br-lg: 0.75rem; 762 + --br-xl: 1rem; 763 + --br-full: 9999px; 764 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 765 + --shadow-base: 766 + 0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06); 767 + --shadow-md: 768 + 0 4px 6px -1px rgba(0, 0, 0, 0.1),0 2px 4px -1px rgba(0, 0, 0, 0.06); 769 + --shadow-lg: 770 + 0 10px 15px -3px rgba(0, 0, 0, 0.1),0 4px 6px -2px rgba(0, 0, 0, 0.05); 771 + --shadow-xl: 772 + 0 20px 25px -5px rgba(0, 0, 0, 0.1),0 10px 10px -5px rgba(0, 0, 0, 0.04); 773 + --transition-fast: 0.15s ease-in-out; 774 + --transition-base: 0.2s ease-in-out; 775 + --transition-slow: 0.3s ease-in-out; 776 + --z-dropdown: 1000; 777 + --z-sticky: 1020; 778 + --z-fixed: 1030; 779 + --z-modal-backdrop: 1040; 780 + --z-modal: 1050; 781 + --z-popover: 1060; 782 + --z-tooltip: 1070; 783 + --grid-columns: 12; 784 + --container-padding: var(--s3); 785 + --ease-in: cubic-bezier(0.4, 0, 1, 1); 786 + --ease-out: cubic-bezier(0, 0, 0.2, 1); 787 + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); 788 + --blackH: 0; 789 + --blackS: 0%; 790 + --blackL: 0%; 791 + --grayH: 220; 792 + --grayS: 14%; 793 + --grayL: 41%; 794 + --whiteH: 0; 795 + --whiteS: 0%; 796 + --whiteL: 100%; 797 + --blueH: 217; 798 + --blueS: 91%; 799 + --blueL: 60%; 800 + --greenH: 142; 801 + --greenS: 71%; 802 + --greenL: 45%; 803 + --yellowH: 45; 804 + --yellowS: 93%; 805 + --yellowL: 47%; 806 + --orangeH: 25; 807 + --orangeS: 95%; 808 + --orangeL: 53%; 809 + --redH: 0; 810 + --redS: 84%; 811 + --redL: 60%; 812 + --purpleH: 262; 813 + --purpleS: 83%; 814 + --purpleL: 58%; 815 + --errorH: var(--redH); 816 + --errorS: var(--redS); 817 + --errorL: var(--redL); 818 + --warningH: var(--orangeH); 819 + --warningS: var(--orangeS); 820 + --warningL: var(--orangeL); 821 + --infoH: var(--blueH); 822 + --infoS: var(--blueS); 823 + --infoL: var(--blueL); 824 + --successH: var(--greenH); 825 + --successS: var(--greenS); 826 + --successL: var(--greenL); 827 + --backgroundH: var(--whiteH); 828 + --backgroundS: var(--whiteS); 829 + --backgroundL: var(--whiteL); 830 + --bodyH: var(--blackH); 831 + --bodyS: var(--blackS); 832 + --bodyL: 13%; 833 + --secondaryH: var(--grayH); 834 + --secondaryS: var(--grayS); 835 + --secondaryL: var(--grayL); 836 + --primaryH: var(--blueH); 837 + --primaryS: var(--blueS); 838 + --primaryL: var(--blueL); 839 + } 840 + } 841 + @layer components { 842 + ui-card { 843 + border: 1px solid; 844 + border-radius: var(--br-lg); 845 + box-shadow: var(--shadow-sm); 846 + display: block; 847 + overflow: hidden; 848 + transition: all var(--transition-base); 849 + } 850 + ui-card:hover { 851 + box-shadow: var(--shadow-md); 852 + transform: translateY(-2px); 853 + } 854 + ui-card[clickable] { 855 + cursor: pointer; 856 + } 857 + ui-card[clickable]:active { 858 + transform: translateY(0); 859 + } 860 + ui-badge { 861 + border: 1px solid; 862 + border-radius: var(--br-full); 863 + display: inline-block; 864 + font-size: var(--f7); 865 + font-weight: var(--fw-medium); 866 + letter-spacing: 0.05em; 867 + padding: var(--s1) var(--s2); 868 + text-transform: uppercase; 869 + } 870 + ui-badge[variant='success'] { 871 + background-color: var(--success-dull); 872 + color: var(--success); 873 + } 874 + ui-badge[variant='warning'] { 875 + background-color: var(--warning-dull); 876 + color: var(--warning); 877 + } 878 + ui-badge[variant='error'] { 879 + background-color: var(--error-dull); 880 + color: var(--error); 881 + } 882 + ui-badge[variant='info'] { 883 + background-color: var(--info-dull); 884 + color: var(--info); 885 + } 886 + ui-button-group { 887 + display: flex; 888 + flex-wrap: wrap; 889 + gap: var(--s2); 890 + } 891 + ui-button-group[orientation='vertical'] { 892 + flex-direction: column; 893 + } 894 + ui-button-group[attached] { 895 + gap: 0; 896 + } 897 + ui-button-group[attached] > button:not(:first-child), 898 + ui-button-group[attached] > input:not(:first-child) { 899 + border-bottom-left-radius: 0; 900 + border-left: none; 901 + border-top-left-radius: 0; 902 + } 903 + ui-button-group[attached] > button:not(:last-child), 904 + ui-button-group[attached] > input:not(:last-child) { 905 + border-bottom-right-radius: 0; 906 + border-top-right-radius: 0; 907 + } 908 + ui-grid { 909 + display: grid; 910 + gap: var(--s3); 911 + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 912 + } 913 + ui-grid[cols='1'] { 914 + grid-template-columns: 1fr; 915 + } 916 + ui-grid[cols='2'] { 917 + grid-template-columns: repeat(2, 1fr); 918 + } 919 + ui-grid[cols='3'] { 920 + grid-template-columns: repeat(3, 1fr); 921 + } 922 + ui-grid[cols='4'] { 923 + grid-template-columns: repeat(4, 1fr); 924 + } 925 + @media (max-width: 768px) { 926 + ui-grid[cols='2'], ui-grid[cols='3'], ui-grid[cols='4'] { 927 + grid-template-columns: 1fr; 928 + } 929 + } 930 + ui-stack { 931 + display: flex; 932 + flex-direction: column; 933 + gap: var(--s3); 934 + } 935 + ui-stack[spacing='xs'] { 936 + gap: var(--s1); 937 + } 938 + ui-stack[spacing='sm'] { 939 + gap: var(--s2); 940 + } 941 + ui-stack[spacing='lg'], ui-stack[spacing='md'], ui-stack[spacing='xl'] { 942 + gap: var(--s3); 943 + } 944 + ui-stack[direction='row'] { 945 + align-items: center; 946 + flex-direction: row; 947 + } 948 + ui-progress { 949 + border: 1px solid; 950 + height: 8px; 951 + overflow: hidden; 952 + width: 100%; 953 + } 954 + ui-progress, ui-progress:before { 955 + border-radius: var(--br-full); 956 + display: block; 957 + } 958 + ui-progress:before { 959 + background-color: currentColor; 960 + content: ''; 961 + height: 100%; 962 + transition: width var(--transition-base); 963 + width: var(--progress-value, 0); 964 + } 965 + ui-progress[variant='success']:before { 966 + background-color: var(--success); 967 + } 968 + ui-progress[variant='warning']:before { 969 + background-color: var(--warning); 970 + } 971 + ui-progress[variant='error']:before { 972 + background-color: var(--error); 973 + } 974 + ui-alert { 975 + border: 1px solid; 976 + border-radius: var(--br-base); 977 + display: block; 978 + padding: var(--s3); 979 + } 980 + ui-alert[variant='success'] { 981 + background-color: var(--success-dull); 982 + border-color: var(--success); 983 + color: var(--success); 984 + } 985 + ui-alert[variant='warning'] { 986 + background-color: var(--warning-dull); 987 + border-color: var(--warning); 988 + color: var(--warning); 989 + } 990 + ui-alert[variant='error'] { 991 + background-color: var(--error-dull); 992 + border-color: var(--error); 993 + color: var(--error); 994 + } 995 + ui-alert[variant='info'] { 996 + background-color: var(--info-dull); 997 + border-color: var(--info); 998 + color: var(--info); 999 + } 1000 + ui-divider { 1001 + border: none; 1002 + border-top: 1px solid; 1003 + display: block; 1004 + height: 1px; 1005 + margin: var(--s4) 0; 1006 + } 1007 + ui-divider[orientation='vertical'] { 1008 + height: auto; 1009 + margin: 0 var(--s3); 1010 + width: 1px; 1011 + } 1012 + ui-spinner { 1013 + animation: spin 1s linear infinite; 1014 + border-color: currentcolor transparent; 1015 + border-radius: 50%; 1016 + border-style: solid; 1017 + border-width: 2px; 1018 + display: inline-block; 1019 + height: 20px; 1020 + width: 20px; 1021 + } 1022 + ui-spinner[size='sm'] { 1023 + height: 16px; 1024 + width: 16px; 1025 + } 1026 + ui-spinner[size='lg'] { 1027 + border-width: 3px; 1028 + height: 32px; 1029 + width: 32px; 1030 + } 1031 + ui-breadcrumb { 1032 + align-items: center; 1033 + display: flex; 1034 + font-size: var(--f5); 1035 + gap: var(--s2); 1036 + } 1037 + ui-breadcrumb a { 1038 + border-radius: var(--br-sm); 1039 + color: var(--primary); 1040 + padding: var(--s1) var(--s2); 1041 + text-decoration: none; 1042 + transition: opacity var(--transition-base); 1043 + } 1044 + ui-breadcrumb a:hover { 1045 + opacity: 0.7; 1046 + } 1047 + ui-breadcrumb:before { 1048 + content: attr(separator); 1049 + margin: 0 var(--s1); 1050 + opacity: 0.5; 1051 + } 1052 + ui-breadcrumb:first-child:before { 1053 + display: none; 1054 + } 1055 + ui-avatar { 1056 + border: 1px solid; 1057 + border-radius: var(--br-full); 1058 + display: inline-block; 1059 + font-size: var(--f5); 1060 + font-weight: var(--fw-medium); 1061 + height: 40px; 1062 + line-height: 40px; 1063 + overflow: hidden; 1064 + text-align: center; 1065 + width: 40px; 1066 + } 1067 + ui-avatar[size='sm'] { 1068 + font-size: var(--f7); 1069 + height: 32px; 1070 + line-height: 32px; 1071 + width: 32px; 1072 + } 1073 + ui-avatar[size='lg'] { 1074 + font-size: var(--f4); 1075 + height: 56px; 1076 + line-height: 56px; 1077 + width: 56px; 1078 + } 1079 + ui-avatar img { 1080 + height: 100%; 1081 + -o-object-fit: cover; 1082 + object-fit: cover; 1083 + width: 100%; 1084 + } 1085 + ui-dropdown { 1086 + display: inline-block; 1087 + position: relative; 1088 + } 1089 + ui-dropdown-content { 1090 + background: var(--background); 1091 + border: 1px solid; 1092 + border-radius: var(--br-base); 1093 + box-shadow: var(--shadow-lg); 1094 + left: 0; 1095 + min-width: 200px; 1096 + opacity: 0; 1097 + position: absolute; 1098 + top: 100%; 1099 + transform: translateY(-8px); 1100 + transition: all var(--transition-base); 1101 + visibility: hidden; 1102 + z-index: var(--z-dropdown); 1103 + } 1104 + ui-dropdown[open] ui-dropdown-content { 1105 + opacity: 1; 1106 + transform: translateY(0); 1107 + visibility: visible; 1108 + } 1109 + ui-dropdown-item { 1110 + cursor: pointer; 1111 + display: block; 1112 + padding: var(--s3); 1113 + text-decoration: none; 1114 + transition: opacity var(--transition-base); 1115 + } 1116 + ui-dropdown-item:hover { 1117 + opacity: 0.7; 1118 + } 1119 + ui-dropdown-item[active] { 1120 + font-weight: var(--fw-medium); 1121 + opacity: 1; 1122 + } 1123 + @media (max-width: 768px) { 1124 + ui-dropdown-content { 1125 + left: auto; 1126 + min-width: 150px; 1127 + right: 0; 1128 + } 1129 + } 1130 + .drawer-toggle { 1131 + display: none; 1132 + } 1133 + .drawer-backdrop { 1134 + background: rgba(0, 0, 0, 0.5); 1135 + bottom: 0; 1136 + cursor: pointer; 1137 + display: none; 1138 + height: 100vh; 1139 + left: 0; 1140 + opacity: 0; 1141 + position: fixed; 1142 + right: 0; 1143 + top: 0; 1144 + transition: allow-discrete 0.25s; 1145 + transition-property: display, opacity, overlay; 1146 + width: 100vw; 1147 + z-index: calc(var(--z-fixed) + 1); 1148 + } 1149 + .drawer-toggle:checked ~ .drawer-backdrop { 1150 + display: block; 1151 + opacity: 1; 1152 + } 1153 + @starting-style { 1154 + .drawer-toggle:checked ~ .drawer-backdrop { 1155 + opacity: 0; 1156 + } 1157 + } 1158 + ui-drawer { 1159 + background: var(--background); 1160 + box-shadow: 4px 0 12px rgba(0, 0, 0, 0.15); 1161 + display: none; 1162 + overflow-y: auto; 1163 + padding: var(--s3); 1164 + position: fixed; 1165 + transition: allow-discrete 0.25s; 1166 + transition-property: display, transform; 1167 + z-index: calc(var(--z-fixed) + 2); 1168 + } 1169 + ui-drawer, ui-drawer[position='left'] { 1170 + bottom: 0; 1171 + height: 100vh; 1172 + left: 0; 1173 + top: 0; 1174 + transform: translateX(-100%); 1175 + width: min(80vw, 320px); 1176 + } 1177 + .drawer-toggle:checked ~ * ui-drawer[position='left'], 1178 + .drawer-toggle:checked ~ ui-drawer[position='left'] { 1179 + display: block; 1180 + transform: translateX(0); 1181 + } 1182 + @starting-style { 1183 + .drawer-toggle:checked ~ * ui-drawer[position='left'], 1184 + .drawer-toggle:checked ~ ui-drawer[position='left'] { 1185 + transform: translateX(-100%); 1186 + } 1187 + } 1188 + ui-drawer[position='right'] { 1189 + bottom: 0; 1190 + box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15); 1191 + height: 100vh; 1192 + left: auto; 1193 + right: 0; 1194 + top: 0; 1195 + transform: translateX(100%); 1196 + width: min(80vw, 320px); 1197 + } 1198 + .drawer-toggle:checked ~ * ui-drawer[position='right'], 1199 + .drawer-toggle:checked ~ ui-drawer[position='right'] { 1200 + display: block; 1201 + transform: translateX(0); 1202 + } 1203 + @starting-style { 1204 + .drawer-toggle:checked ~ * ui-drawer[position='right'], 1205 + .drawer-toggle:checked ~ ui-drawer[position='right'] { 1206 + transform: translateX(100%); 1207 + } 1208 + } 1209 + ui-drawer[position='top'] { 1210 + bottom: auto; 1211 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 1212 + height: min(80vh, 400px); 1213 + left: 0; 1214 + right: 0; 1215 + top: 0; 1216 + transform: translateY(-100%); 1217 + width: 100vw; 1218 + } 1219 + .drawer-toggle:checked ~ * ui-drawer[position='top'], 1220 + .drawer-toggle:checked ~ ui-drawer[position='top'] { 1221 + display: block; 1222 + transform: translateY(0); 1223 + } 1224 + @starting-style { 1225 + .drawer-toggle:checked ~ * ui-drawer[position='top'], 1226 + .drawer-toggle:checked ~ ui-drawer[position='top'] { 1227 + transform: translateY(-100%); 1228 + } 1229 + } 1230 + ui-drawer[position='bottom'] { 1231 + bottom: 0; 1232 + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 1233 + height: min(80vh, 400px); 1234 + left: 0; 1235 + right: 0; 1236 + top: auto; 1237 + transform: translateY(100%); 1238 + width: 100vw; 1239 + } 1240 + .drawer-toggle:checked ~ * ui-drawer[position='bottom'], 1241 + .drawer-toggle:checked ~ ui-drawer[position='bottom'] { 1242 + display: block; 1243 + transform: translateY(0); 1244 + } 1245 + @starting-style { 1246 + .drawer-toggle:checked ~ * ui-drawer[position='bottom'], 1247 + .drawer-toggle:checked ~ ui-drawer[position='bottom'] { 1248 + transform: translateY(100%); 1249 + } 1250 + } 1251 + ui-bottom-bar { 1252 + align-items: stretch; 1253 + display: flex; 1254 + flex-direction: row; 1255 + gap: 0; 1256 + height: 100%; 1257 + margin: 0; 1258 + padding: 0; 1259 + width: 100%; 1260 + } 1261 + ui-bottom-bar > a, ui-bottom-bar > button { 1262 + align-items: center; 1263 + background: transparent; 1264 + border: none; 1265 + color: inherit; 1266 + cursor: pointer; 1267 + display: flex; 1268 + flex: 1; 1269 + flex-direction: column; 1270 + font-size: var(--f7); 1271 + font-weight: var(--fw-medium); 1272 + gap: 2px; 1273 + justify-content: center; 1274 + opacity: 1; 1275 + padding: var(--s1) var(--s2); 1276 + text-decoration: none; 1277 + transition: opacity var(--transition-base); 1278 + } 1279 + ui-bottom-bar > a:hover, ui-bottom-bar > button:hover { 1280 + opacity: 0.7; 1281 + } 1282 + ui-bottom-bar > button:disabled { 1283 + cursor: not-allowed; 1284 + opacity: 0.3; 1285 + } 1286 + ui-bottom-bar > a[aria-current='page'], 1287 + ui-bottom-bar > button[aria-current='page'] { 1288 + opacity: 0.5; 1289 + } 1290 + ui-bottom-bar .icon, ui-bottom-bar ui-icon { 1291 + font-size: var(--f4); 1292 + line-height: 1; 1293 + } 1294 + ui-bottom-bar[hide-text] > a > span:not(.icon), 1295 + ui-bottom-bar[hide-text] > button > span:not(.icon) { 1296 + display: none; 1297 + } 1298 + ui-bottom-bar[hide-text] > a, ui-bottom-bar[hide-text] > button { 1299 + gap: 0; 1300 + } 1301 + ui-bottom-bar[orientation='horizontal'] > a, 1302 + ui-bottom-bar[orientation='horizontal'] > button { 1303 + flex-direction: row; 1304 + gap: var(--s2); 1305 + } 1306 + ui-pwa-version { 1307 + display: block; 1308 + } 1309 + ui-pwa-version p { 1310 + margin: 0 0 var(--s2) 0; 1311 + } 1312 + ui-pwa-version button { 1313 + margin-top: var(--s1); 1314 + } 1315 + ui-counter { 1316 + align-items: stretch; 1317 + display: flex; 1318 + gap: var(--s2); 1319 + } 1320 + ui-counter button { 1321 + align-items: center; 1322 + display: flex; 1323 + flex-shrink: 0; 1324 + justify-content: center; 1325 + } 1326 + ui-counter input[type='number'] { 1327 + -webkit-appearance: textfield; 1328 + appearance: textfield; 1329 + -moz-appearance: textfield; 1330 + flex: 1; 1331 + margin: 0; 1332 + padding: 0; 1333 + text-align: center; 1334 + } 1335 + ui-counter input[type='number']::-webkit-inner-spin-button, 1336 + ui-counter input[type='number']::-webkit-outer-spin-button { 1337 + -webkit-appearance: none; 1338 + } 1339 + ui-dialog { 1340 + display: contents; 1341 + } 1342 + dialog { 1343 + background: Canvas; 1344 + border: none; 1345 + border-radius: 0.5rem; 1346 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 1347 + max-width: min(90vw, 560px); 1348 + opacity: 0; 1349 + padding: 0; 1350 + transform: scale(0.6); 1351 + transform-origin: top center; 1352 + transition: allow-discrete 0.25s; 1353 + transition-property: display, opacity, overlay, transform; 1354 + width: 100%; 1355 + } 1356 + dialog::backdrop { 1357 + background-color: #000; 1358 + opacity: 0; 1359 + transition: allow-discrete 0.25s; 1360 + transition-property: display, opacity, overlay; 1361 + } 1362 + dialog[open] { 1363 + opacity: 1; 1364 + transform: scale(1); 1365 + } 1366 + dialog[open]::backdrop { 1367 + opacity: 0.5; 1368 + } 1369 + @starting-style { 1370 + dialog[open] { 1371 + opacity: 0; 1372 + transform: scale(0.6); 1373 + } 1374 + dialog[open]::backdrop { 1375 + opacity: 0; 1376 + } 1377 + } 1378 + } 1379 + @layer utilities { 1380 + .flex { 1381 + display: flex; 1382 + } 1383 + .flex-column { 1384 + flex-direction: column; 1385 + } 1386 + .flex-row { 1387 + flex-direction: row; 1388 + } 1389 + .items-center { 1390 + align-items: center; 1391 + } 1392 + .items-start { 1393 + align-items: flex-start; 1394 + } 1395 + .items-end { 1396 + align-items: flex-end; 1397 + } 1398 + .justify-center { 1399 + justify-content: center; 1400 + } 1401 + .justify-start { 1402 + justify-content: flex-start; 1403 + } 1404 + .justify-end { 1405 + justify-content: flex-end; 1406 + } 1407 + .justify-between { 1408 + justify-content: space-between; 1409 + } 1410 + .w-100 { 1411 + width: 100%; 1412 + } 1413 + .mw6 { 1414 + max-width: var(--max-width-sm); 1415 + } 1416 + .measure { 1417 + max-width: 30em; 1418 + } 1419 + .container-sm { 1420 + max-width: var(--max-width-sm); 1421 + } 1422 + .container-md, .container-sm { 1423 + margin: 0 auto; 1424 + padding: 0 var(--container-padding); 1425 + } 1426 + .container-md { 1427 + max-width: var(--max-width-md); 1428 + } 1429 + .container-lg { 1430 + max-width: var(--max-width-lg); 1431 + } 1432 + .container-lg, .container-xl { 1433 + margin: 0 auto; 1434 + padding: 0 var(--container-padding); 1435 + } 1436 + .container-xl { 1437 + max-width: var(--max-width-xl); 1438 + } 1439 + .container-fluid { 1440 + padding: 0 var(--container-padding); 1441 + width: 100%; 1442 + } 1443 + .pa0 { 1444 + padding: 0; 1445 + } 1446 + .pa1 { 1447 + padding: var(--s1); 1448 + } 1449 + .pa2 { 1450 + padding: var(--s2); 1451 + } 1452 + .pa3 { 1453 + padding: var(--s3); 1454 + } 1455 + .pa4 { 1456 + padding: var(--s4); 1457 + } 1458 + .ma0 { 1459 + margin: 0; 1460 + } 1461 + .ma1 { 1462 + margin: var(--s1); 1463 + } 1464 + .ma2 { 1465 + margin: var(--s2); 1466 + } 1467 + .ma3 { 1468 + margin: var(--s3); 1469 + } 1470 + .mh-auto { 1471 + margin-left: auto; 1472 + margin-right: auto; 1473 + } 1474 + .mv3 { 1475 + margin-bottom: var(--s3); 1476 + margin-top: var(--s3); 1477 + } 1478 + .mb2 { 1479 + margin-bottom: var(--s2); 1480 + } 1481 + .mb0 { 1482 + margin-bottom: 0; 1483 + } 1484 + .mt3 { 1485 + margin-top: var(--s3); 1486 + } 1487 + .mt2 { 1488 + margin-top: var(--s2); 1489 + } 1490 + .mr3 { 1491 + margin-right: var(--s3); 1492 + } 1493 + .pr3 { 1494 + padding-right: var(--s3); 1495 + } 1496 + .pl2 { 1497 + padding-left: var(--s2); 1498 + } 1499 + .pt2 { 1500 + padding-top: var(--s2); 1501 + } 1502 + .pb4 { 1503 + padding-bottom: var(--s4); 1504 + } 1505 + .pb0 { 1506 + padding-bottom: 0; 1507 + } 1508 + .pb3 { 1509 + padding-bottom: var(--s3); 1510 + } 1511 + .f3 { 1512 + font-size: var(--f3); 1513 + } 1514 + .f4 { 1515 + font-size: var(--f4); 1516 + } 1517 + .f5 { 1518 + font-size: var(--f5); 1519 + } 1520 + .f6 { 1521 + font-size: var(--f6); 1522 + } 1523 + .tc { 1524 + text-align: center; 1525 + } 1526 + .tr { 1527 + text-align: right; 1528 + } 1529 + .b { 1530 + font-weight: var(--fw-bold); 1531 + } 1532 + .br2 { 1533 + border-radius: var(--br2); 1534 + } 1535 + .ba { 1536 + border: 1px solid; 1537 + } 1538 + .b1 { 1539 + border-width: 1px; 1540 + } 1541 + .t-border { 1542 + border: 1px solid transparent; 1543 + } 1544 + .border { 1545 + border: 1px solid; 1546 + } 1547 + .list { 1548 + list-style: none; 1549 + } 1550 + .cursor, .pointer { 1551 + cursor: pointer; 1552 + } 1553 + .o-30 { 1554 + opacity: 0.3; 1555 + } 1556 + .o-40 { 1557 + opacity: 0.4; 1558 + } 1559 + .o-60 { 1560 + opacity: 0.6; 1561 + } 1562 + .underline { 1563 + text-decoration: underline; 1564 + } 1565 + .link { 1566 + text-decoration: none; 1567 + } 1568 + .db { 1569 + display: block; 1570 + } 1571 + .h2 { 1572 + height: 2rem; 1573 + } 1574 + .h3 { 1575 + height: 3rem; 1576 + } 1577 + .input { 1578 + border: 1px solid; 1579 + border-radius: var(--br2); 1580 + padding: var(--s2); 1581 + } 1582 + .input-reset { 1583 + -webkit-appearance: none; 1584 + -moz-appearance: none; 1585 + appearance: none; 1586 + background: transparent; 1587 + } 1588 + .shadow { 1589 + box-shadow: var(--shadow-base); 1590 + } 1591 + .pv2 { 1592 + padding-bottom: var(--s2); 1593 + padding-top: var(--s2); 1594 + } 1595 + .pv1 { 1596 + padding-bottom: var(--s1); 1597 + padding-top: var(--s1); 1598 + } 1599 + .ph2 { 1600 + padding-left: var(--s2); 1601 + padding-right: var(--s2); 1602 + } 1603 + .lh-copy { 1604 + line-height: var(--lh-copy); 1605 + } 1606 + .center { 1607 + text-align: center; 1608 + } 1609 + .flex-wrap { 1610 + flex-wrap: wrap; 1611 + } 1612 + .no-select { 1613 + cursor: default; 1614 + -webkit-user-select: none; 1615 + -moz-user-select: none; 1616 + user-select: none; 1617 + } 1618 + .relative { 1619 + position: relative; 1620 + } 1621 + .absolute { 1622 + position: absolute; 1623 + } 1624 + .fixed { 1625 + position: fixed; 1626 + } 1627 + .absolute-center { 1628 + left: 50%; 1629 + position: fixed; 1630 + top: 50%; 1631 + transform: translate(-50%, -50%); 1632 + } 1633 + .overflow-hidden, .text-ellipsis { 1634 + overflow: hidden; 1635 + } 1636 + .text-ellipsis { 1637 + text-overflow: ellipsis; 1638 + white-space: nowrap; 1639 + } 1640 + .z-dropdown { 1641 + z-index: var(--z-dropdown); 1642 + } 1643 + .z-modal { 1644 + z-index: var(--z-modal); 1645 + } 1646 + .z-tooltip { 1647 + z-index: var(--z-tooltip); 1648 + } 1649 + }
+83 -83
www/static/styles/utilities.min.css
··· 1 1 @layer utilities { 2 2 .flex { 3 - display: flex 3 + display: flex; 4 4 } 5 5 6 6 .flex-column { 7 - flex-direction: column 7 + flex-direction: column; 8 8 } 9 9 10 10 .flex-row { 11 - flex-direction: row 11 + flex-direction: row; 12 12 } 13 13 14 14 .items-center { 15 - align-items: center 15 + align-items: center; 16 16 } 17 17 18 18 .items-start { 19 - align-items: flex-start 19 + align-items: flex-start; 20 20 } 21 21 22 22 .items-end { 23 - align-items: flex-end 23 + align-items: flex-end; 24 24 } 25 25 26 26 .justify-center { 27 - justify-content: center 27 + justify-content: center; 28 28 } 29 29 30 30 .justify-start { 31 - justify-content: flex-start 31 + justify-content: flex-start; 32 32 } 33 33 34 34 .justify-end { 35 - justify-content: flex-end 35 + justify-content: flex-end; 36 36 } 37 37 38 38 .justify-between { 39 - justify-content: space-between 39 + justify-content: space-between; 40 40 } 41 41 42 42 .w-100 { 43 - width: 100% 43 + width: 100%; 44 44 } 45 45 46 46 .mw6 { 47 - max-width: var(--max-width-sm) 47 + max-width: var(--max-width-sm); 48 48 } 49 49 50 50 .measure { 51 - max-width: 30em 51 + max-width: 30em; 52 52 } 53 53 54 54 .container-sm { 55 - max-width: var(--max-width-sm) 55 + max-width: var(--max-width-sm); 56 56 } 57 57 58 58 .container-md, 59 59 .container-sm { 60 60 margin: 0 auto; 61 - padding: 0 var(--container-padding) 61 + padding: 0 var(--container-padding); 62 62 } 63 63 64 64 .container-md { 65 - max-width: var(--max-width-md) 65 + max-width: var(--max-width-md); 66 66 } 67 67 68 68 .container-lg { 69 - max-width: var(--max-width-lg) 69 + max-width: var(--max-width-lg); 70 70 } 71 71 72 72 .container-lg, 73 73 .container-xl { 74 74 margin: 0 auto; 75 - padding: 0 var(--container-padding) 75 + padding: 0 var(--container-padding); 76 76 } 77 77 78 78 .container-xl { 79 - max-width: var(--max-width-xl) 79 + max-width: var(--max-width-xl); 80 80 } 81 81 82 82 .container-fluid { 83 83 padding: 0 var(--container-padding); 84 - width: 100% 84 + width: 100%; 85 85 } 86 86 87 87 .pa0 { 88 - padding: 0 88 + padding: 0; 89 89 } 90 90 91 91 .pa1 { 92 - padding: var(--s1) 92 + padding: var(--s1); 93 93 } 94 94 95 95 .pa2 { 96 - padding: var(--s2) 96 + padding: var(--s2); 97 97 } 98 98 99 99 .pa3 { 100 - padding: var(--s3) 100 + padding: var(--s3); 101 101 } 102 102 103 103 .pa4 { 104 - padding: var(--s4) 104 + padding: var(--s4); 105 105 } 106 106 107 107 .ma0 { 108 - margin: 0 108 + margin: 0; 109 109 } 110 110 111 111 .ma1 { 112 - margin: var(--s1) 112 + margin: var(--s1); 113 113 } 114 114 115 115 .ma2 { 116 - margin: var(--s2) 116 + margin: var(--s2); 117 117 } 118 118 119 119 .ma3 { 120 - margin: var(--s3) 120 + margin: var(--s3); 121 121 } 122 122 123 123 .mh-auto { 124 124 margin-left: auto; 125 - margin-right: auto 125 + margin-right: auto; 126 126 } 127 127 128 128 .mv3 { 129 129 margin-bottom: var(--s3); 130 - margin-top: var(--s3) 130 + margin-top: var(--s3); 131 131 } 132 132 133 133 .mb2 { 134 - margin-bottom: var(--s2) 134 + margin-bottom: var(--s2); 135 135 } 136 136 137 137 .mb0 { 138 - margin-bottom: 0 138 + margin-bottom: 0; 139 139 } 140 140 141 141 .mt3 { 142 - margin-top: var(--s3) 142 + margin-top: var(--s3); 143 143 } 144 144 145 145 .mt2 { 146 - margin-top: var(--s2) 146 + margin-top: var(--s2); 147 147 } 148 148 149 149 .mr3 { 150 - margin-right: var(--s3) 150 + margin-right: var(--s3); 151 151 } 152 152 153 153 .pr3 { 154 - padding-right: var(--s3) 154 + padding-right: var(--s3); 155 155 } 156 156 157 157 .pl2 { 158 - padding-left: var(--s2) 158 + padding-left: var(--s2); 159 159 } 160 160 161 161 .pt2 { 162 - padding-top: var(--s2) 162 + padding-top: var(--s2); 163 163 } 164 164 165 165 .pb4 { 166 - padding-bottom: var(--s4) 166 + padding-bottom: var(--s4); 167 167 } 168 168 169 169 .pb0 { 170 - padding-bottom: 0 170 + padding-bottom: 0; 171 171 } 172 172 173 173 .pb3 { 174 - padding-bottom: var(--s3) 174 + padding-bottom: var(--s3); 175 175 } 176 176 177 177 .f3 { 178 - font-size: var(--f3) 178 + font-size: var(--f3); 179 179 } 180 180 181 181 .f4 { 182 - font-size: var(--f4) 182 + font-size: var(--f4); 183 183 } 184 184 185 185 .f5 { 186 - font-size: var(--f5) 186 + font-size: var(--f5); 187 187 } 188 188 189 189 .f6 { 190 - font-size: var(--f6) 190 + font-size: var(--f6); 191 191 } 192 192 193 193 .tc { 194 - text-align: center 194 + text-align: center; 195 195 } 196 196 197 197 .tr { 198 - text-align: right 198 + text-align: right; 199 199 } 200 200 201 201 .b { 202 - font-weight: var(--fw-bold) 202 + font-weight: var(--fw-bold); 203 203 } 204 204 205 205 .br2 { 206 - border-radius: var(--br2) 206 + border-radius: var(--br2); 207 207 } 208 208 209 209 .ba { 210 - border: 1px solid 210 + border: 1px solid; 211 211 } 212 212 213 213 .b1 { 214 - border-width: 1px 214 + border-width: 1px; 215 215 } 216 216 217 217 .t-border { 218 - border: 1px solid transparent 218 + border: 1px solid transparent; 219 219 } 220 220 221 221 .border { 222 - border: 1px solid 222 + border: 1px solid; 223 223 } 224 224 225 225 .list { 226 - list-style: none 226 + list-style: none; 227 227 } 228 228 229 229 .cursor, 230 230 .pointer { 231 - cursor: pointer 231 + cursor: pointer; 232 232 } 233 233 234 234 .o-30 { 235 - opacity: .3 235 + opacity: 0.3; 236 236 } 237 237 238 238 .o-40 { 239 - opacity: .4 239 + opacity: 0.4; 240 240 } 241 241 242 242 .o-60 { 243 - opacity: .6 243 + opacity: 0.6; 244 244 } 245 245 246 246 .underline { 247 - text-decoration: underline 247 + text-decoration: underline; 248 248 } 249 249 250 250 .link { 251 - text-decoration: none 251 + text-decoration: none; 252 252 } 253 253 254 254 .db { 255 - display: block 255 + display: block; 256 256 } 257 257 258 258 .h2 { 259 - height: 2rem 259 + height: 2rem; 260 260 } 261 261 262 262 .h3 { 263 - height: 3rem 263 + height: 3rem; 264 264 } 265 265 266 266 .input { 267 267 border: 1px solid; 268 268 border-radius: var(--br2); 269 - padding: var(--s2) 269 + padding: var(--s2); 270 270 } 271 271 272 272 .input-reset { 273 273 -webkit-appearance: none; 274 274 -moz-appearance: none; 275 275 appearance: none; 276 - background: transparent 276 + background: transparent; 277 277 } 278 278 279 279 .shadow { 280 - box-shadow: var(--shadow-base) 280 + box-shadow: var(--shadow-base); 281 281 } 282 282 283 283 .pv2 { 284 284 padding-bottom: var(--s2); 285 - padding-top: var(--s2) 285 + padding-top: var(--s2); 286 286 } 287 287 288 288 .pv1 { 289 289 padding-bottom: var(--s1); 290 - padding-top: var(--s1) 290 + padding-top: var(--s1); 291 291 } 292 292 293 293 .ph2 { 294 294 padding-left: var(--s2); 295 - padding-right: var(--s2) 295 + padding-right: var(--s2); 296 296 } 297 297 298 298 .lh-copy { 299 - line-height: var(--lh-copy) 299 + line-height: var(--lh-copy); 300 300 } 301 301 302 302 .center { 303 - text-align: center 303 + text-align: center; 304 304 } 305 305 306 306 .flex-wrap { 307 - flex-wrap: wrap 307 + flex-wrap: wrap; 308 308 } 309 309 310 310 .no-select { 311 311 cursor: default; 312 312 -webkit-user-select: none; 313 313 -moz-user-select: none; 314 - user-select: none 314 + user-select: none; 315 315 } 316 316 317 317 .relative { 318 - position: relative 318 + position: relative; 319 319 } 320 320 321 321 .absolute { 322 - position: absolute 322 + position: absolute; 323 323 } 324 324 325 325 .fixed { 326 - position: fixed 326 + position: fixed; 327 327 } 328 328 329 329 .absolute-center { 330 330 left: 50%; 331 331 position: fixed; 332 332 top: 50%; 333 - transform: translate(-50%, -50%) 333 + transform: translate(-50%, -50%); 334 334 } 335 335 336 336 .overflow-hidden, 337 337 .text-ellipsis { 338 - overflow: hidden 338 + overflow: hidden; 339 339 } 340 340 341 341 .text-ellipsis { 342 342 text-overflow: ellipsis; 343 - white-space: nowrap 343 + white-space: nowrap; 344 344 } 345 345 346 346 .z-dropdown { 347 - z-index: var(--z-dropdown) 347 + z-index: var(--z-dropdown); 348 348 } 349 349 350 350 .z-modal { 351 - z-index: var(--z-modal) 351 + z-index: var(--z-modal); 352 352 } 353 353 354 354 .z-tooltip { 355 - z-index: var(--z-tooltip) 355 + z-index: var(--z-tooltip); 356 356 } 357 357 }
+3 -1
www/utils/fs.ts
··· 41 41 return await source.exists() 42 42 } 43 43 44 - export async function getCachedPMTiles(filename: string): Promise<PMTiles | null> { 44 + export async function getCachedPMTiles( 45 + filename: string, 46 + ): Promise<PMTiles | null> { 45 47 const db = await getDb() 46 48 const source = new IndexedDBSource(db, filename, STORE_NAME) 47 49 if (!(await source.exists())) return null
+7 -1
www/utils/nav.ts
··· 1 - export type MapTarget = { lat: number; lng: number; zoom: number; marker?: boolean; name?: string } 1 + export type MapTarget = { 2 + lat: number 3 + lng: number 4 + zoom: number 5 + marker?: boolean 6 + name?: string 7 + } 2 8 3 9 let pending: MapTarget | null = null 4 10