diff --git a/.superpowers/sdd/2026-08-18-first-visible-vertical-slice/task-8-report.md b/.superpowers/sdd/2026-08-18-first-visible-vertical-slice/task-8-report.md new file mode 100644 index 0000000..8092f55 --- /dev/null +++ b/.superpowers/sdd/2026-08-18-first-visible-vertical-slice/task-8-report.md @@ -0,0 +1,47 @@ +# Task 8 report: reusable Angular application shell + +## Scope delivered + +- Replaced the Angular scaffold with a route-hosting root component and a reusable dark-fantasy shell. +- Added focused standalone components for the app shell, store-driven top bar, side navigation, footer, and context-panel frame. +- Added requested design tokens, spacing, motion, elevation, global reset, responsive shell grid, and reduced-motion protection. +- Configured `/` and fallback routing to `/world`; the route lazy-loads a deliberately scene-free `WorldPageComponent` placeholder so Task 9 can add the world composition without changing the route contract. +- Made Karte the only enabled active navigation entry. Jagd, Quests, Inventar, and Charakter are disabled buttons with accessible explanatory labels. Shop is not rendered. +- Top-bar character identity, level, and hit points are rendered only from `WorldStore.character`; it has no hardcoded character statistics. + +## Preflight and asset evidence + +- Re-read the Task 8 brief, visible-slice implementation plan, progress ledger, prior task reports, and the full user-provided visual asset style guide. +- Visually inspected `combat-screen.png`, `hunting-screen.png`, and `world-travel-screen.png` before editing. +- Visually inspected the relevant user HUD assets before use. The shell uses only: + - `apps/web/public/images/hud/CharacterIcon.png` + - `apps/web/public/images/hud/MapsIcon.png` + - `apps/web/public/images/hud/HuntIcon.png` + - `apps/web/public/images/hud/QuestsIcon.png` + - `apps/web/public/images/hud/InventoryIcon.png` + +## TDD evidence + +### RED + +`npm test --workspace=@ashen-realms/web -- --watch=false` failed as intended before production work: the new shell specification expected `app-top-bar`, but the scaffold only contained a router outlet. The existing two test files remained green (11 passing tests). + +### GREEN + +`npm test --workspace=@ashen-realms/web -- --watch=false` passed with 3 test files and 12 tests. The added shell test asserts top bar, side navigation, semantic main content, footer, enabled active Karte, disabled future destinations, and Shop absence. + +## Final verification + +- `npm test --workspace=@ashen-realms/web -- --watch=false`: 3 files and 12 tests passing. +- `npm run build:web`: production Angular build passed and emits a lazy `world-page-component` chunk. +- `npm exec --workspace=@ashen-realms/web -- prettier --check ...`: all Task 8 files formatted. +- `git diff --check`: no whitespace errors. + +## Visual QA limitation + +The Browser plugin is not available in this session. The repository has no installed Playwright package, and a non-mutating `npm exec --workspace=@ashen-realms/web -- playwright --version` check could not resolve a local runner and timed out. No browser dependency was added outside task scope. Rendered browser verification remains for Task 11, which owns final browser workflow and fidelity captures. + +## Intentional limits + +- The world route placeholder contains no scene, nodes, API loading, or travel panel; Task 9 owns those surfaces. +- The context panel is only a reusable framed shell until Task 9 supplies world data. diff --git a/apps/web/public/images/hud/CharacterIcon.png b/apps/web/public/images/hud/CharacterIcon.png new file mode 100644 index 0000000..9308850 Binary files /dev/null and b/apps/web/public/images/hud/CharacterIcon.png differ diff --git a/apps/web/public/images/hud/HuntIcon.png b/apps/web/public/images/hud/HuntIcon.png new file mode 100644 index 0000000..020ab44 Binary files /dev/null and b/apps/web/public/images/hud/HuntIcon.png differ diff --git a/apps/web/public/images/hud/InventoryIcon.png b/apps/web/public/images/hud/InventoryIcon.png new file mode 100644 index 0000000..ec46ed4 Binary files /dev/null and b/apps/web/public/images/hud/InventoryIcon.png differ diff --git a/apps/web/public/images/hud/MapsIcon.png b/apps/web/public/images/hud/MapsIcon.png new file mode 100644 index 0000000..e5ef70d Binary files /dev/null and b/apps/web/public/images/hud/MapsIcon.png differ diff --git a/apps/web/public/images/hud/QuestsIcon.png b/apps/web/public/images/hud/QuestsIcon.png new file mode 100644 index 0000000..16afa9e Binary files /dev/null and b/apps/web/public/images/hud/QuestsIcon.png differ diff --git a/apps/web/src/app/app.html b/apps/web/src/app/app.html index a1c4296..67e7bd4 100644 --- a/apps/web/src/app/app.html +++ b/apps/web/src/app/app.html @@ -1,344 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - diff --git a/apps/web/src/app/app.routes.ts b/apps/web/src/app/app.routes.ts index dc39edb..41c02b2 100644 --- a/apps/web/src/app/app.routes.ts +++ b/apps/web/src/app/app.routes.ts @@ -1,3 +1,20 @@ import { Routes } from '@angular/router'; +import { AppShellComponent } from './layout/app-shell/app-shell.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { path: '', pathMatch: 'full', redirectTo: 'world' }, + { + path: '', + component: AppShellComponent, + children: [ + { + path: 'world', + loadComponent: () => + import('./features/world/world-page.component').then( + (module) => module.WorldPageComponent, + ), + }, + ], + }, + { path: '**', redirectTo: 'world' }, +]; diff --git a/apps/web/src/app/app.scss b/apps/web/src/app/app.scss index e69de29..9fa2c20 100644 --- a/apps/web/src/app/app.scss +++ b/apps/web/src/app/app.scss @@ -0,0 +1,4 @@ +:host { + display: block; + min-block-size: 100%; +} diff --git a/apps/web/src/app/app.spec.ts b/apps/web/src/app/app.spec.ts index 92618e6..f31b0d5 100644 --- a/apps/web/src/app/app.spec.ts +++ b/apps/web/src/app/app.spec.ts @@ -1,23 +1,44 @@ +import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { App } from './app'; +import { provideRouter } from '@angular/router'; +import { WorldStore } from './features/world/world.store'; +import { AppShellComponent } from './layout/app-shell/app-shell.component'; describe('App', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [App], + imports: [AppShellComponent], + providers: [ + provideRouter([]), + { + provide: WorldStore, + useValue: { character: signal(null) }, + }, + ], }).compileComponents(); }); - it('should create the app', () => { - const fixture = TestBed.createComponent(App); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); + it('renders the reusable game shell with only Karte available', () => { + const fixture = TestBed.createComponent(AppShellComponent); + fixture.detectChanges(); - it('should render title', async () => { - const fixture = TestBed.createComponent(App); - await fixture.whenStable(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, web'); + const element = fixture.nativeElement as HTMLElement; + expect(element.querySelector('app-top-bar')).not.toBeNull(); + expect(element.querySelector('app-side-navigation')).not.toBeNull(); + expect(element.querySelector('main')).not.toBeNull(); + expect(element.querySelector('app-game-footer')).not.toBeNull(); + + const mapButton = element.querySelector('[data-navigation="world"]'); + expect(mapButton).not.toBeNull(); + expect(mapButton?.disabled).toBe(false); + expect(mapButton?.getAttribute('aria-current')).toBe('page'); + + for (const destination of ['hunt', 'quests', 'inventory', 'character']) { + expect( + element.querySelector(`[data-navigation="${destination}"]`)?.disabled, + ).toBe(true); + } + + expect(element.textContent).not.toContain('Shop'); }); }); diff --git a/apps/web/src/app/app.ts b/apps/web/src/app/app.ts index 358960b..372b06c 100644 --- a/apps/web/src/app/app.ts +++ b/apps/web/src/app/app.ts @@ -1,12 +1,10 @@ -import { Component, signal } from '@angular/core'; +import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.html', - styleUrl: './app.scss' + styleUrl: './app.scss', }) -export class App { - protected readonly title = signal('web'); -} +export class App {} diff --git a/apps/web/src/app/features/world/world-page.component.ts b/apps/web/src/app/features/world/world-page.component.ts new file mode 100644 index 0000000..cd54632 --- /dev/null +++ b/apps/web/src/app/features/world/world-page.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-world-page', + template: '
', + styles: [ + ` + .world-route { + min-block-size: 100%; + } + `, + ], +}) +export class WorldPageComponent {} diff --git a/apps/web/src/app/layout/app-shell/app-shell.component.html b/apps/web/src/app/layout/app-shell/app-shell.component.html new file mode 100644 index 0000000..a3fe8f0 --- /dev/null +++ b/apps/web/src/app/layout/app-shell/app-shell.component.html @@ -0,0 +1,13 @@ +
+ + +
+ +
+ +
+ +
+ + +
diff --git a/apps/web/src/app/layout/app-shell/app-shell.component.scss b/apps/web/src/app/layout/app-shell/app-shell.component.scss new file mode 100644 index 0000000..3fdc3b4 --- /dev/null +++ b/apps/web/src/app/layout/app-shell/app-shell.component.scss @@ -0,0 +1,47 @@ +:host { + display: block; + min-block-size: 100dvh; +} + +.app-shell { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + min-block-size: 100dvh; + background: var(--ar-bg); +} + +.app-shell__content { + display: grid; + grid-template-columns: minmax(11rem, 13rem) minmax(0, 1fr) minmax(15rem, 20rem); + min-block-size: 0; +} + +.app-shell__main { + min-inline-size: 0; + min-block-size: 0; + padding: var(--ar-space-5); + border-inline: 1px solid var(--ar-border); + background: + linear-gradient(115deg, rgb(255 255 255 / 0.018), transparent 28%), var(--ar-panel-muted); +} + +@media (width < 900px) { + .app-shell__content { + grid-template-columns: minmax(8.5rem, 10rem) minmax(0, 1fr); + } + + app-context-panel { + display: none; + } +} + +@media (width < 620px) { + .app-shell__content { + display: block; + } + + .app-shell__main { + min-block-size: 20rem; + border-inline: 0; + } +} diff --git a/apps/web/src/app/layout/app-shell/app-shell.component.ts b/apps/web/src/app/layout/app-shell/app-shell.component.ts new file mode 100644 index 0000000..eea9c19 --- /dev/null +++ b/apps/web/src/app/layout/app-shell/app-shell.component.ts @@ -0,0 +1,23 @@ +import { Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { WorldStore } from '../../features/world/world.store'; +import { ContextPanelComponent } from '../context-panel/context-panel.component'; +import { GameFooterComponent } from '../game-footer/game-footer.component'; +import { SideNavigationComponent } from '../side-navigation/side-navigation.component'; +import { TopBarComponent } from '../top-bar/top-bar.component'; + +@Component({ + selector: 'app-app-shell', + imports: [ + RouterOutlet, + TopBarComponent, + SideNavigationComponent, + GameFooterComponent, + ContextPanelComponent, + ], + templateUrl: './app-shell.component.html', + styleUrl: './app-shell.component.scss', +}) +export class AppShellComponent { + protected readonly worldStore = inject(WorldStore); +} diff --git a/apps/web/src/app/layout/context-panel/context-panel.component.html b/apps/web/src/app/layout/context-panel/context-panel.component.html new file mode 100644 index 0000000..b50db00 --- /dev/null +++ b/apps/web/src/app/layout/context-panel/context-panel.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/web/src/app/layout/context-panel/context-panel.component.scss b/apps/web/src/app/layout/context-panel/context-panel.component.scss new file mode 100644 index 0000000..e71058a --- /dev/null +++ b/apps/web/src/app/layout/context-panel/context-panel.component.scss @@ -0,0 +1,26 @@ +:host { + display: block; + background: var(--ar-panel); +} + +.context-panel { + min-block-size: 100%; + padding: var(--ar-space-5); + border-inline-start: 1px solid var(--ar-border); + background: linear-gradient(180deg, rgb(255 255 255 / 0.025), transparent 16%), var(--ar-panel); +} + +.context-panel__eyebrow { + display: block; + padding-block-end: var(--ar-space-3); + border-block-end: 1px solid var(--ar-border); + color: var(--ar-gold); + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.25rem; +} + +.context-panel__hint { + color: var(--ar-text-muted); + font-size: var(--ar-font-sm); + line-height: 1.55; +} diff --git a/apps/web/src/app/layout/context-panel/context-panel.component.ts b/apps/web/src/app/layout/context-panel/context-panel.component.ts new file mode 100644 index 0000000..542ba28 --- /dev/null +++ b/apps/web/src/app/layout/context-panel/context-panel.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-context-panel', + templateUrl: './context-panel.component.html', + styleUrl: './context-panel.component.scss', +}) +export class ContextPanelComponent {} diff --git a/apps/web/src/app/layout/game-footer/game-footer.component.html b/apps/web/src/app/layout/game-footer/game-footer.component.html new file mode 100644 index 0000000..712d765 --- /dev/null +++ b/apps/web/src/app/layout/game-footer/game-footer.component.html @@ -0,0 +1,5 @@ +
+ Verbindung bereit + + Aschenfelder +
diff --git a/apps/web/src/app/layout/game-footer/game-footer.component.scss b/apps/web/src/app/layout/game-footer/game-footer.component.scss new file mode 100644 index 0000000..f4a2617 --- /dev/null +++ b/apps/web/src/app/layout/game-footer/game-footer.component.scss @@ -0,0 +1,30 @@ +:host { + display: block; +} + +.game-footer { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + min-block-size: 3.3rem; + padding-inline: var(--ar-space-5); + border-block-start: 1px solid var(--ar-border-highlight); + color: var(--ar-text-muted); + background: var(--ar-panel); + font-size: var(--ar-font-sm); +} + +.game-footer > :last-child { + justify-self: end; +} + +.game-footer__status span { + color: var(--ar-success); +} + +.game-footer__ornament { + inline-size: 0.8rem; + block-size: 0.8rem; + transform: rotate(45deg); + border: 1px solid var(--ar-gold); +} diff --git a/apps/web/src/app/layout/game-footer/game-footer.component.ts b/apps/web/src/app/layout/game-footer/game-footer.component.ts new file mode 100644 index 0000000..72cd52b --- /dev/null +++ b/apps/web/src/app/layout/game-footer/game-footer.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-game-footer', + templateUrl: './game-footer.component.html', + styleUrl: './game-footer.component.scss', +}) +export class GameFooterComponent {} diff --git a/apps/web/src/app/layout/side-navigation/side-navigation.component.html b/apps/web/src/app/layout/side-navigation/side-navigation.component.html new file mode 100644 index 0000000..719e58d --- /dev/null +++ b/apps/web/src/app/layout/side-navigation/side-navigation.component.html @@ -0,0 +1,53 @@ + diff --git a/apps/web/src/app/layout/side-navigation/side-navigation.component.scss b/apps/web/src/app/layout/side-navigation/side-navigation.component.scss new file mode 100644 index 0000000..cc0e1c1 --- /dev/null +++ b/apps/web/src/app/layout/side-navigation/side-navigation.component.scss @@ -0,0 +1,73 @@ +:host { + display: block; + background: var(--ar-panel); +} + +.side-navigation { + display: grid; + align-content: start; + padding-block: var(--ar-space-3); +} + +.side-navigation__item { + display: flex; + align-items: center; + gap: var(--ar-space-3); + inline-size: 100%; + min-block-size: 4.3rem; + padding: var(--ar-space-3) var(--ar-space-4); + border: 0; + border-block-end: 1px solid color-mix(in srgb, var(--ar-border) 65%, transparent); + border-inline-start: 3px solid transparent; + color: var(--ar-text-muted); + background: transparent; + font: inherit; + text-align: start; + text-decoration: none; + transition: + color var(--ar-motion-fast), + background var(--ar-motion-fast), + border-color var(--ar-motion-fast); +} + +.side-navigation__item img { + inline-size: 2.25rem; + block-size: 2.25rem; + object-fit: cover; + border-radius: 50%; +} + +.side-navigation__item--active { + border-inline-start-color: var(--ar-blue); + color: var(--ar-text); + background: linear-gradient(90deg, rgb(92 169 216 / 0.2), transparent); + box-shadow: inset 0 0 1.25rem rgb(92 169 216 / 0.08); +} + +.side-navigation__item:not(:disabled):hover, +.side-navigation__item:not(:disabled):focus-visible { + color: var(--ar-text); + background: rgb(255 255 255 / 0.04); +} + +.side-navigation__item:disabled { + cursor: not-allowed; + opacity: 0.52; +} + +@media (width < 620px) { + .side-navigation { + grid-template-columns: repeat(5, minmax(0, 1fr)); + padding: 0; + } + + .side-navigation__item { + justify-content: center; + min-block-size: 3.5rem; + padding: var(--ar-space-2); + } + + .side-navigation__item span { + display: none; + } +} diff --git a/apps/web/src/app/layout/side-navigation/side-navigation.component.ts b/apps/web/src/app/layout/side-navigation/side-navigation.component.ts new file mode 100644 index 0000000..8156e5a --- /dev/null +++ b/apps/web/src/app/layout/side-navigation/side-navigation.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-side-navigation', + imports: [RouterLink], + templateUrl: './side-navigation.component.html', + styleUrl: './side-navigation.component.scss', +}) +export class SideNavigationComponent {} diff --git a/apps/web/src/app/layout/top-bar/top-bar.component.html b/apps/web/src/app/layout/top-bar/top-bar.component.html new file mode 100644 index 0000000..783ac05 --- /dev/null +++ b/apps/web/src/app/layout/top-bar/top-bar.component.html @@ -0,0 +1,25 @@ +
+
+ + @if (character(); as character) { +
+ {{ character.name }} + Stufe {{ character.level }} +
+
+ {{ character.currentHp }} / {{ character.maxHp }} + +
+ } @else { + Charakterdaten werden geladen + } +
+ + + Ashen Realms +
diff --git a/apps/web/src/app/layout/top-bar/top-bar.component.scss b/apps/web/src/app/layout/top-bar/top-bar.component.scss new file mode 100644 index 0000000..1da7ca2 --- /dev/null +++ b/apps/web/src/app/layout/top-bar/top-bar.component.scss @@ -0,0 +1,101 @@ +:host { + display: block; +} + +.top-bar { + display: grid; + grid-template-columns: minmax(18rem, 1fr) auto minmax(18rem, 1fr); + align-items: center; + min-block-size: 5.6rem; + padding: var(--ar-space-2) var(--ar-space-5); + border-block: 1px solid var(--ar-border-highlight); + background: linear-gradient(180deg, rgb(255 255 255 / 0.035), transparent 62%), var(--ar-panel); + box-shadow: var(--ar-shadow-raised); +} + +.top-bar__character { + display: flex; + align-items: center; + gap: var(--ar-space-3); + min-inline-size: 0; +} + +.top-bar__portrait { + inline-size: 4.25rem; + block-size: 4.25rem; + object-fit: cover; + border: 1px solid var(--ar-border-highlight); + border-radius: 50%; + background: #080909; +} + +.top-bar__identity, +.top-bar__health { + display: grid; + gap: 0.2rem; +} + +.top-bar__name, +.top-bar__world-name { + color: var(--ar-text); + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.1rem; +} + +.top-bar__level, +.top-bar__loading { + color: var(--ar-text-muted); + font-size: var(--ar-font-sm); +} + +.top-bar__health { + min-inline-size: 8rem; + color: var(--ar-text); + font-size: var(--ar-font-sm); +} + +.top-bar__health-track { + display: block; + overflow: hidden; + block-size: 0.45rem; + border: 1px solid #6d2f2a; + background: #23100f; +} + +.top-bar__health-value { + display: block; + block-size: 100%; + background: linear-gradient(90deg, #8e231f, #c74b42); + transition: inline-size var(--ar-motion-base); +} + +.top-bar__ornament { + inline-size: 1.5rem; + block-size: 1.5rem; + transform: rotate(45deg); + border: 1px solid var(--ar-border-highlight); + box-shadow: 0 0 0 0.25rem var(--ar-panel); +} + +.top-bar__world-name { + justify-self: end; + color: var(--ar-gold); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +@media (width < 620px) { + .top-bar { + grid-template-columns: 1fr auto; + padding-inline: var(--ar-space-3); + } + + .top-bar__ornament, + .top-bar__world-name { + display: none; + } + + .top-bar__health { + min-inline-size: 5rem; + } +} diff --git a/apps/web/src/app/layout/top-bar/top-bar.component.ts b/apps/web/src/app/layout/top-bar/top-bar.component.ts new file mode 100644 index 0000000..f8ed482 --- /dev/null +++ b/apps/web/src/app/layout/top-bar/top-bar.component.ts @@ -0,0 +1,11 @@ +import { Component, input } from '@angular/core'; +import { CharacterResponse } from '../../core/api/game-api.models'; + +@Component({ + selector: 'app-top-bar', + templateUrl: './top-bar.component.html', + styleUrl: './top-bar.component.scss', +}) +export class TopBarComponent { + readonly character = input(null); +} diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index 90d4ee0..4204abf 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -1 +1,77 @@ -/* You can add global styles to this file, and also import other style files */ +:root { + --ar-bg: #090b0d; + --ar-panel: #101316; + --ar-panel-muted: #17191b; + --ar-border: #554a39; + --ar-border-highlight: #9b7a42; + --ar-text: #ede7da; + --ar-text-muted: #aaa397; + --ar-gold: #c9a45f; + --ar-blue: #5ca9d8; + --ar-success: #78b96e; + --ar-warning: #d69445; + --ar-danger: #c74b42; + --ar-space-1: 0.25rem; + --ar-space-2: 0.5rem; + --ar-space-3: 0.75rem; + --ar-space-4: 1rem; + --ar-space-5: 1.5rem; + --ar-space-6: 2rem; + --ar-radius-sm: 0.15rem; + --ar-radius-md: 0.35rem; + --ar-shadow-raised: 0 0.45rem 1.4rem rgb(0 0 0 / 0.5); + --ar-motion-fast: 140ms ease; + --ar-motion-base: 220ms ease; + --ar-font-sm: 0.8125rem; +} + +* { + box-sizing: border-box; +} + +html { + min-block-size: 100%; + color-scheme: dark; + background: var(--ar-bg); +} + +body { + min-block-size: 100dvh; + margin: 0; + color: var(--ar-text); + background: var(--ar-bg); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button:focus-visible, +a:focus-visible { + outline: 2px solid var(--ar-blue); + outline-offset: -2px; +} + +img { + max-inline-size: 100%; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +}