docs: web env vars, response shapes and 2.0.0 web review updates (#11)

This commit is contained in:
MrAlders0n authored and GitHub committed 2026-10-02 19:19:43 -04:00
1 parent d325d986b7
commit b38be34aad
6 files changed
+101 -41

No files matched your search

+11 -5
View File
@@ -73,6 +73,11 @@ Set every `CHANGE_*` value. The variables you must fill in:
| `VITE_API_BASE` | `web` | `https://<your-domain>/api/v1` — must be the **public** domain, never localhost. |
| `VITE_WS_URL` | `web` | `wss://<your-domain>/ws` |
| `VITE_MAP_CENTER` / `VITE_MAP_ZOOM` | `web` | *(Optional)* Fallback "All" map view. The app auto-fits the map to all IATA locations from `config.yaml`; these values are only used as a fallback when those IATAs have no location set. Omit for a world view. |
| `VITE_DISABLED_TABS` | `web` | *(Optional)* Comma list of tabs to hide, e.g. `Map,Traces`. Options: `Packets,Channels,Map,Nodes,Observers,Routes,Traces,Analytics`. |
| `VITE_ENABLED_THEMES` | `web` | *(Optional)* Comma list of theme ids. When set, only these themes are offered; listing `meshmapper_dark` / `meshmapper_light` is the only way to enable those. |
| `VITE_APP_NAME` | `web` | *(Optional)* Top-left wordmark text. Default `BEACON`. |
| `VITE_SKIP_SPLASH` | `web` | *(Optional)* `true` skips the once-per-session load splash. |
| `VITE_BANNER` | `web` | *(Optional)* Notice above the header on every page, e.g. for a dev instance. `[label](url)` and bare URLs become links. |
> ⚠️ **Password must match in two places.** The password inside `POSTGRES_DSN` (in `.env`) must equal `POSTGRES_PASSWORD` in `docker-compose.yml`. Update both before bringing the stack up.
@@ -113,11 +118,11 @@ docker compose logs -f
Visit `https://<your-domain>` and you're off to the races. 🚀
> After changing any `VITE_*` value later, recreate the web container so the new values get baked into the JS bundle:
> After changing any `VITE_*` value later, apply it with:
> ```bash
> docker compose up -d --force-recreate web
> docker compose up -d web
> ```
> (then hard-refresh / use incognito, since `/assets/*` is cached immutable.)
> The web container writes these to `/config.js` on every start, so visitors get the new values on their next page load.
### Container images
@@ -127,8 +132,9 @@ is required**.
Tags: `latest` follows stable releases on `main`, `dev` follows the development branch, and each
release is also published as `X.Y.Z` and `X.Y`. The compose file uses `latest`; to control when
upgrades happen, pin both images to a release line, e.g. `beacon-server:2.0` and `beacon-web:2.0`
(server and web share major.minor versions).
upgrades happen, pin both images to a release line (`beacon-server:2.0` and `beacon-web:2.0`) or an
exact release (`:2.0.0`). Server and web share major.minor versions, so web `2.0.x` pairs with server
`2.0.x`; patch numbers move independently.
> **Troubleshooting — `403 Forbidden` on pull.** If `docker compose up` fails with a
> `... manifests/<tag>: 403 Forbidden` error, the package has been set (or defaulted)
+16 -4
View File
@@ -39,18 +39,30 @@ MQTT_BROKER_2_PASSWORD=CHANGE_PASS
# ---- Public domain / TLS (service: caddy) -------------------
# Site address + automatic Let's Encrypt certificate.
DOMAIN=dev.meshcore.ca
DOMAIN=beacon.example.com
# ---- Frontend URLs (service: web) ---------------------------
# Baked into the static JS bundle when the web container starts.
# Written to /config.js each time the web container starts.
# MUST be the public domain (never localhost) and match DOMAIN above,
# the browser makes these calls, not the container.
# After changing these, recreate the web container so they re-inject:
# docker compose up -d --force-recreate web
# (and hard-refresh / incognito, since /assets/* is cached immutable)
VITE_API_BASE=https://dev.meshcore.ca/api/v1
VITE_WS_URL=wss://dev.meshcore.ca/ws
VITE_API_BASE=https://beacon.example.com/api/v1
VITE_WS_URL=wss://beacon.example.com/ws
# Default "All" map view (decimal "lat,lon" + zoom). Unset = world view.
VITE_MAP_CENTER=52.5,-96.8
VITE_MAP_ZOOM=3.2
# Optional web settings. Unset = default.
# Hide tabs (comma list): Packets,Channels,Map,Nodes,Observers,Routes,Traces,Analytics
# VITE_DISABLED_TABS=Map,Traces
# Only offer these themes (comma list of ids); the only way to enable meshmapper_dark/meshmapper_light.
# VITE_ENABLED_THEMES=meshmapper_dark,meshmapper_light
# Top-left wordmark (default BEACON).
# VITE_APP_NAME=BEACON
# Skip the once-per-session load splash.
# VITE_SKIP_SPLASH=true
# Notice above the header; [label](url) and bare URLs become links.
# VITE_BANNER=Test instance, data may be reset. Main site: [beacon.example.com](https://beacon.example.com)
+55 -11
View File
@@ -222,7 +222,7 @@ Adverts with a bad signature are not decoded and have no `parsedPayload`.
| `GET /nodes` | Page of node summaries. Cursor is `lastSeen` epoch ms. Default limit 50. |
| `GET /nodes/{nodeId}` | Node detail: capability flags, `minFirmwareVersion`, `iatas` (`[{iata, lastHeard}]`), `neighbors`, clock drift for repeaters and room servers. |
| `GET /nodes/{nodeId}/observations` | Page of observations of packets the node originated. Cursor is the observation ID. |
| `GET /nodes/{nodeId}/neighbors` | Plain array of neighbours. |
| `GET /nodes/{nodeId}/neighbors` | Plain array of neighbours: `{ id, name, publicKey, nodeType, nodeTypeName, lat, lng, iata, observationCount, firstSeen, lastSeen, snr }`. |
`/nodes` params: `type` (1 companion, 2 repeater, 3 room server, 4 sensor) or `typeName`, the location filters, `name` (partial, case-insensitive), `scope`, `pubkey` (exact hex), `pubkeyPrefix`, `supportsMultibytePaths`, `supportsMultibyteTraces`, `neighbors` (adds `neighborIds`), `cursor`, `limit`. Summaries include `stale` (not seen within `nodes.stale_threshold`) and, when `nodes.mark_foreign` is on, `possiblyForeign`.
@@ -289,11 +289,9 @@ breakdown; none exist within the retention window in practice.
backfills the trailing 7 days, so for the first 30 days after deploy buckets older than 7 days may be
only partially costed, and observations recorded without radio parameters are never costed.
Intervals of `1h` and up are read from the hourly analytics rollups. Each UTC hour is rolled about
95 minutes after it closes, so the newest hour or two are not yet included at those intervals. Sub-hour
intervals read live observation rows and are always current. The response also reports `windowStart`,
`windowEnd`, `generatedAt`, `source` (`raw` or `hourly`) and a `summary` of recorded packets and the
last complete hour.
Sub-hour intervals read live observation rows and are always current. The response also reports
`windowStart`, `windowEnd`, `generatedAt`, `source` (`raw` or `hourly`) and a `summary` of recorded
packets and the last complete hour.
```json
{
@@ -383,6 +381,25 @@ Known routes are fully resolved multi-hop paths distilled from packet history.
To page `/routes`, send the last item's `lastSeen` as `cursor` and its `id` as `cursorId`. Routes share a millisecond often (one batch of upserts), and `cursorId` returns the ones that didn't fit on the previous page. `cursor` alone still works but can skip those ties. `cursorId` must be a positive integer and requires `cursor`; otherwise `400`.
`/routes` and `/routes/search` return a plain array of routes:
```json
{
"id": 412,
"pathKey": "5d1c0e7a9b2f4c8d6e3a1b0f9c8d7e6a",
"iata": "YOW",
"hopCount": 3,
"hops": [ { "nodeId": "uuid", "hashBytes": "ae", "node": { "id": "uuid", "name": "YOW_Kanata", "...": "" } } ],
"firstSeen": 1747526400000,
"lastSeen": 1747665456000,
"observationCount": 57
}
```
`/routes/cross` returns `[{ sourceSegment, crossHop, targetSegment, totalHops }]`, where the segments are hop arrays as above and `crossHop` is `{ fromNode, toNode, fromIata, toIata, lastSeen }`.
The observations endpoint returns `{ items, hasMore, nextPageCursor, route, windowStart, windowEnd, generatedAt, matchType, matchAvailable, hashSize, pathBytes }`. Each item is `{ id, packetHash, observerId, observerName, heardAt, payloadType, payloadTypeName, rssi, snr }`.
### Stats
```
@@ -467,14 +484,21 @@ clamped to the oldest hour the rollups still hold. An empty region returns zeros
- `top-nodes` ranks nodes by advert hearings. `TopNode` and `TopAdvertiser` carry `publicKey` (hex,
always set) and a nullable `nodeId` (null once the node row has been deleted); key rows by `publicKey`.
- `scopes` counts packets heard since `since`. Observer and node counts are current memberships:
observers filter by the IATA they last reported from. `hourly` splits the packet count by UTC hour.
- `signal` gives SNR/RSSI distributions and hourly trends; `paths` gives path-entry and hash-width
distributions. Both require `since` and `until` (at most 30 days) and read hourly snapshots refreshed
every `background.view_refresh`, so the current hour is excluded.
observers filter by the IATA they last reported from. `hourly` is `[{ hour, packets, observers, nodes }]`,
oldest first with empty hours omitted: the scope's packets that hour, plus the distinct observers that
heard them and nodes whose adverts were heard in the scope.
- `signal` gives SNR/RSSI histograms and averages; `paths` gives hash-width and path-length
distributions. Both have an `hourly` array, require `since` and `until` (at most 30 days) and read the
hourly rollups, so empty or not-yet-rolled hours are missing. Each `paths` hour carries `maxEntries`,
the longest path heard that hour (0 if only empty paths were heard).
- `observer-comparison` counts distinct flood packets heard by A only, B only and both, over a required
`since`/`until`. The observers must be different; an unknown one is a `404`.
`since`/`until`. The observers must be different; an unknown one is a `404`. Response:
`{ observerA, observerB, since, until, totalPackets, onlyA, onlyB, both }`.
- `clock-drift` lists repeaters and room servers whose last advert clock is off by more than
`nodes.clock_drift_threshold`, worst first. It is current state, not windowed.
- `radio-presets` is `[{ preset, iata, sourceType, count }]`, where `preset` is `freqMhz,bwKhz,sf` and
`sourceType` is `observer` or `node`; it is rebuilt every `background.view_refresh` (default `1h`).
`node-types` is `[{ nodeType, nodeTypeName, count }]` over all known nodes.
### Traces
@@ -492,6 +516,26 @@ It returns a plain array, most recently heard first, default limit 50. To page,
`lastHeardAt` as `cursor` and its trace tag (hex) as `cursorTag`; the tag breaks ties between traces
last heard in the same millisecond. `/traces/{tag}` is a `404` for an unknown tag.
A list item:
```json
{
"traceTag": "a3f1b2c4",
"firstHeardAt": 1747665450000,
"lastHeardAt": 1747665462000,
"packetCount": 3,
"iataCount": 2,
"traceType": "TRACE",
"pathHashes": ["ae", "9b", "f3"],
"snrValues": [7.25, 4.5, -1.0]
}
```
`pathHashes` and `snrValues` come from the most complete observation. The detail is
`{ traceTag, packets }`, one entry per packet: `{ packetHash, routeType, routeTypeName, scope, firstHeardAt,
lastHeardAt, rawPath, resolvedRoute }`. `rawPath` is `[{ hash, snr }]` as received; `resolvedRoute` uses the
resolved hop shape from packet detail.
### Admin
All under `/api/v1/admin/`, bearer key required (see [Auth](#auth)).
+17 -19
View File
@@ -30,7 +30,7 @@ This is the single source of truth for the project; sub-documents (deployment, r
- **Backend:** Single Go binary, pgx + pgxpool + sqlc for Postgres, `github.com/meshcore-go/meshcore-go` for packet decoding, internal Go channels for live WebSocket fanout
- **Database:** Postgres with BRIN + composite indexes, hourly rollup tables for historical stats
- **Cache:** Redis for hot reads (recent packets, region stats, node metadata) plus in-memory LRU in Go in front
- **Web client:** React + Vite + TypeScript + Tailwind + TanStack Query/Virtual + shadcn/ui
- **Web client:** React 19 + Vite + TypeScript + Tailwind 4 + TanStack Query/Virtual, MapLibre GL for the map, ECharts for analytics charts, i18next for translations
- **Mobile client:** Flutter (native iOS + Android)
- **Edge:** Caddy for TLS and reverse proxy
- **Deployment:** Docker Compose with five services (app, db, redis, web, caddy)
@@ -930,10 +930,23 @@ Covers REST endpoints (`/api/v1/`), auth and rate limits, WebSocket protocol (`/
- Per-observer page: payload type breakdown of their contributions, recent observations, battery and uptime curves from `status_metadata` history (if we capture deltas), software version
- Filter pills: by observer type, by broker (mqtt1-only / mqtt2-only / both), by IATA or super-region
### Routes
- `known_routes`: fully resolved multi-hop paths per IATA, every hop high confidence. Listed by IATA and hop count, searchable between two node hashes, and across IATA boundaries.
- Route detail pages the retained observations whose path matches the route exactly (`/routes/{iata}/{pathKey}/observations`).
### Traces
- One row per trace tag from the `trace_tags` summary kept at ingest: type (TRACE or PING), packet count, IATAs heard, first and last heard.
- Detail shows each packet's raw hops with per-hop SNR and the resolved route.
### Observer page
- Observer summary, telemetry curves (battery, airtime, noise floor) from the telemetry history, and an activity chart of what it heard, read from raw observations for sub-hour buckets and the hourly rollups otherwise.
- A page of the adverts the observer heard.
### Stats
- Queries against the hourly rollup tables. Regions expand to their IATAs through `region_iatas`; distinct counts come from the IATA-set tables, so a packet heard in several IATAs counts once.
- Top-line: total packets last 24h, total observations last 24h, active observers, active IATAs, unique nodes seen
- Charts: observations over time by IATA (with optional super-region rollup), payload type breakdown, top contributing observers, top contributing nodes
- The web Analytics tab adds signal and path-length distributions, per-scope activity, clock drift, radio presets, an observer comparison and the neighbour graph, with hourly sparklines from `/stats/series`
---
@@ -972,6 +985,8 @@ New observations of the same packet arrive via WebSocket and appear inline in th
These are out of scope for v1 but worth keeping in mind so the schema and architecture don't paint us into a corner.
Already shipped from this list: the trace explorer (Traces tab, with per-hop SNR), live packet flow on the map, and the neighbour graph (map overlay and Analytics).
### Web-based admin UI
The admin API exists (see [Admin API and access control](#admin-api-and-access-control)), but there is
no login or browser UI yet. A future version could add:
@@ -983,25 +998,8 @@ no login or browser UI yet. A future version could add:
The config file should remain the primary source of truth; admin UI writes would update the file (or a parallel DB table that overlays it) so a snapshot of operational state is always version-controllable.
### Trace packet visualization (payload type 0x09)
Trace packets carry per-hop SNR values (`[snr_1][snr_2]...[snr_N]` where each is a signed byte representing SNR × 4). Once decoded, these unlock a dedicated per-hop signal quality view: for any traced path, render the actual SNR at each hop on the map, color-coded by signal strength. This is the only way to see real RF link quality between specific repeaters rather than just observer-reported reception. The schema already supports this since trace packets parse into `parsed_payload` like any other type. A future trace explorer view would query packets where `payload_type = 0x09` and pivot the per-hop SNR data into a visualization.
### Live pew pew map
A real-time animated map showing packets propagating across the mesh as they happen. Each new observation fires an animated arc or pulse from the resolved sending node (or first known hop) to the observing node's location, color-coded by payload type. Multiple observations of the same packet from different observers light up in sequence, visualizing flood propagation as it spreads. Filterable by region, payload type, and channel. The schema already supports this since every observation has timestamps, observer coordinates, and (when paths resolve) node coordinates. Mostly useful as an "is the mesh alive right now?" glance view and as eye candy for the project landing page.
**Constraints for whoever builds this:**
- Must handle high traffic without melting the browser (likely needs WebGL or canvas, not SVG)
- Must degrade gracefully under load (throttle, drop frames, queue events)
- Must NOT draw ambiguous paths (any hop without confidence "high" → don't animate that segment)
- Must NOT zoom-jump the map as new observations come in
- Must support payload-type filters so users can isolate (e.g.) only chat traffic
- Must work from slim WebSocket events where possible to keep bandwidth low, fetch enrichment lazily
### Live neighbor activity graph
Different from pew pew: less detail, more focused on local pathing. Shows live packet activity flowing node-to-node so users can answer "did my local pathing go the way I wanted?" Less about flood visualization, more about understanding whether a specific node's traffic is taking the expected routes through nearby repeaters.
### Neighbor maps (static topology)
Node response payloads can carry full neighbor tables with SNR values per neighbor. This is similar to traces but for adjacency rather than path. A future view could render a graph of node-to-node SNR relationships, giving a true picture of the mesh topology beyond just observer-reported sightings.
Different from the live packet flow on the map: less detail, more focused on local pathing. Shows live packet activity flowing node-to-node so users can answer "did my local pathing go the way I wanted?" Less about flood visualization, more about understanding whether a specific node's traffic is taking the expected routes through nearby repeaters.
### Mobile push notifications
The Flutter app could let users set up notifications for specific events: a keyword appearing in a specific channel, a specific node starting to talk, an observer going offline, a packet matching arbitrary filter criteria. The `channelMessage` and `packetObservation` WebSocket events already carry everything a push service would need. This is purely a feature of the mobile app plus a notification dispatch service (Firebase or APNs).
+1 -1
View File
@@ -221,4 +221,4 @@
font-display: swap;
src: url("data:font/woff2;base64,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 truncated
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}</style></head><body style="margin:0"><div class="dc-card" style="accent-color: auto; place-content: normal; place-items: normal; place-self: auto; vertical-align: baseline; anchor-name: none; anchor-scope: none; animation-composition: replace; appearance: none; aspect-ratio: auto; backdrop-filter: none; backface-visibility: visible; background: rgb(255, 255, 255); background-blend-mode: normal; block-size: 320px; border-block: 0px rgb(0, 0, 0); border-bottom: 0px rgb(0, 0, 0); border-collapse: separate; border-end-end-radius: 2px; border-end-start-radius: 2px; border-image: none; border-inline: 0px rgb(0, 0, 0); border-left: 0px rgb(0, 0, 0); border-right: 0px rgb(0, 0, 0); border-spacing: 0px; border-start-end-radius: 2px; border-start-start-radius: 2px; border-top: 0px rgb(0, 0, 0); border-radius: 0px; bottom: auto; box-decoration-break: slice; box-shadow: none; box-sizing: content-box; break-after: auto; break-before: auto; break-inside: auto; caption-side: top; caret-color: rgb(0, 0, 0); clear: none; clip: auto; clip-path: none; clip-rule: nonzero; color: rgb(0, 0, 0); color-interpolation: srgb; color-interpolation-filters: linearrgb; color-scheme: normal; columns: auto; column-fill: balance; gap: normal; column-rule: 3px none rgb(0, 0, 0); column-span: none; contain: none; contain-intrinsic-block-size: none; contain-intrinsic-height: none; contain-intrinsic-inline-size: none; contain-intrinsic-width: none; container: none; content: normal; content-visibility: visible; counter-increment: none; counter-reset: none; counter-set: none; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: block; dominant-baseline: auto; empty-cells: show; fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; filter: none; flex: 0 1 auto; flex-flow: row; float: none; flood-color: rgb(0, 0, 0); flood-opacity: 1; font: 16px -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, system-ui, sans-serif; font-palette: normal; font-synthesis: weight style small-caps position; forced-color-adjust: auto; grid: none; grid-area: auto; height: 320px; hyphenate-character: auto; hyphenate-limit-chars: auto; hyphens: manual; image-orientation: from-image; image-rendering: auto; ime-mode: auto; inline-size: 260px; inset-block: auto; inset-inline: auto; isolation: isolate; left: auto; letter-spacing: normal; lighting-color: rgb(255, 255, 255); line-break: auto; list-style: outside; margin-block: 0px; margin-bottom: 0px; margin-inline: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; marker: none; mask: none; mask-type: luminance; math-depth: 0; math-shift: normal; math-style: normal; max-block-size: none; max-height: none; max-inline-size: none; max-width: none; min-block-size: 0px; min-height: 0px; min-inline-size: 0px; min-width: 0px; mix-blend-mode: normal; object-fit: fill; object-position: 50% 50%; offset: normal; opacity: 1; order: 0; outline: rgb(0, 0, 0) 3px; outline-offset: 0px; overflow-anchor: auto; overflow-block: hidden; overflow-clip-margin: 0px; overflow-inline: hidden; overflow-wrap: normal; overflow: hidden; overscroll-behavior-block: auto; overscroll-behavior-inline: auto; overscroll-behavior: auto; padding-block: 0px; padding-bottom: 0px; padding-inline: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; page: auto; paint-order: normal; perspective: none; perspective-origin: 130px 160px; pointer-events: auto; position: static; position-anchor: normal; position-area: none; position-try: none; position-visibility: anchors-visible; print-color-adjust: economy; quotes: auto; r: 0px; resize: none; right: auto; rotate: none; ruby-align: space-around; ruby-position: alternate; rx: auto; ry: auto; scale: none; scroll-behavior: auto; scroll-margin-block: 0px; scroll-margin-bottom: 0px; scroll-margin-inline: 0px; scroll-margin-left: 0px; scroll-margin-right: 0px; scroll-margin-top: 0px; scroll-padding-block: auto; scroll-padding-bottom: auto; scroll-padding-inline: auto; scroll-padding-left: auto; scroll-padding-right: auto; scroll-padding-top: auto; scroll-snap-align: none; scroll-snap-stop: normal; scroll-snap-type: none; scrollbar-color: auto; scrollbar-gutter: auto; scrollbar-width: auto; shape-image-threshold: 0; shape-margin: 0px; shape-outside: none; shape-rendering: auto; stop-color: rgb(0, 0, 0); stop-opacity: 1; stroke: none; stroke-dasharray: none; stroke-dashoffset: 0px; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 1px; tab-size: 8; table-layout: auto; text-align: start; text-align-last: auto; text-anchor: start; text-autospace: no-autospace; text-combine-upright: none; text-decoration: rgb(0, 0, 0); text-decoration-inset: 0px; text-decoration-skip-ink: auto; text-emphasis: none rgb(0, 0, 0); text-emphasis-position: auto; text-indent: 0px; text-justify: auto; text-orientation: mixed; text-overflow: clip; text-rendering: auto; text-shadow: none; text-transform: none; text-underline-offset: auto; text-underline-position: auto;Line truncated
}</style></head><body style="margin:0"><div class="dc-card" style="accent-color: auto; place-content: normal; place-items: normal; place-self: auto; vertical-align: baseline; anchor-name: none; anchor-scope: none; animation-composition: replace; appearance: none; aspect-ratio: auto; backdrop-filter: none; backface-visibility: visible; background: rgb(255, 255, 255); background-blend-mode: normal; block-size: 320px; border-block: 0px rgb(0, 0, 0); border-bottom: 0px rgb(0, 0, 0); border-collapse: separate; border-end-end-radius: 2px; border-end-start-radius: 2px; border-image: none; border-inline: 0px rgb(0, 0, 0); border-left: 0px rgb(0, 0, 0); border-right: 0px rgb(0, 0, 0); border-spacing: 0px; border-start-end-radius: 2px; border-start-start-radius: 2px; border-top: 0px rgb(0, 0, 0); border-radius: 0px; bottom: auto; box-decoration-break: slice; box-shadow: none; box-sizing: content-box; break-after: auto; break-before: auto; break-inside: auto; caption-side: top; caret-color: rgb(0, 0, 0); clear: none; clip: auto; clip-path: none; clip-rule: nonzero; color: rgb(0, 0, 0); color-interpolation: srgb; color-interpolation-filters: linearrgb; color-scheme: normal; columns: auto; column-fill: balance; gap: normal; column-rule: 3px none rgb(0, 0, 0); column-span: none; contain: none; contain-intrinsic-block-size: none; contain-intrinsic-height: none; contain-intrinsic-inline-size: none; contain-intrinsic-width: none; container: none; content: normal; content-visibility: visible; counter-increment: none; counter-reset: none; counter-set: none; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: block; dominant-baseline: auto; empty-cells: show; fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; filter: none; flex: 0 1 auto; flex-flow: row; float: none; flood-color: rgb(0, 0, 0); flood-opacity: 1; font: 16px -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, system-ui, sans-serif; font-palette: normal; font-synthesis: weight style small-caps position; forced-color-adjust: auto; grid: none; grid-area: auto; height: 320px; hyphenate-character: auto; hyphenate-limit-chars: auto; hyphens: manual; image-orientation: from-image; image-rendering: auto; ime-mode: auto; inline-size: 260px; inset-block: auto; inset-inline: auto; isolation: isolate; left: auto; letter-spacing: normal; lighting-color: rgb(255, 255, 255); line-break: auto; list-style: outside; margin-block: 0px; margin-bottom: 0px; margin-inline: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; marker: none; mask: none; mask-type: luminance; math-depth: 0; math-shift: normal; math-style: normal; max-block-size: none; max-height: none; max-inline-size: none; max-width: none; min-block-size: 0px; min-height: 0px; min-inline-size: 0px; min-width: 0px; mix-blend-mode: normal; object-fit: fill; object-position: 50% 50%; offset: normal; opacity: 1; order: 0; outline: rgb(0, 0, 0) 3px; outline-offset: 0px; overflow-anchor: auto; overflow-block: hidden; overflow-clip-margin: 0px; overflow-inline: hidden; overflow-wrap: normal; overflow: hidden; overscroll-behavior-block: auto; overscroll-behavior-inline: auto; overscroll-behavior: auto; padding-block: 0px; padding-bottom: 0px; padding-inline: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; page: auto; paint-order: normal; perspective: none; perspective-origin: 130px 160px; pointer-events: auto; position: static; position-anchor: normal; position-area: none; position-try: none; position-visibility: anchors-visible; print-color-adjust: economy; quotes: auto; r: 0px; resize: none; right: auto; rotate: none; ruby-align: space-around; ruby-position: alternate; rx: auto; ry: auto; scale: none; scroll-behavior: auto; scroll-margin-block: 0px; scroll-margin-bottom: 0px; scroll-margin-inline: 0px; scroll-margin-left: 0px; scroll-margin-right: 0px; scroll-margin-top: 0px; scroll-padding-block: auto; scroll-padding-bottom: auto; scroll-padding-inline: auto; scroll-padding-left: auto; scroll-padding-right: auto; scroll-padding-top: auto; scroll-snap-align: none; scroll-snap-stop: normal; scroll-snap-type: none; scrollbar-color: auto; scrollbar-gutter: auto; scrollbar-width: auto; shape-image-threshold: 0; shape-margin: 0px; shape-outside: none; shape-rendering: auto; stop-color: rgb(0, 0, 0); stop-opacity: 1; stroke: none; stroke-dasharray: none; stroke-dashoffset: 0px; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 1px; tab-size: 8; table-layout: auto; text-align: start; text-align-last: auto; text-anchor: start; text-autospace: no-autospace; text-combine-upright: none; text-decoration: rgb(0, 0, 0); text-decoration-inset: 0px; text-decoration-skip-ink: auto; text-emphasis: none rgb(0, 0, 0); text-emphasis-position: auto; text-indent: 0px; text-justify: auto; text-orientation: mixed; text-overflow: clip; text-rendering: auto; text-shadow: none; text-transform: none; text-underline-offset: auto; text-underline-position: auto;Line truncated
+1 -1
View File
@@ -221,4 +221,4 @@
font-display: swap;
src: url("data:font/woff2;base64,d09GMgABAAAAAFcwABMAAAAAzqQAAFbDAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoMkG/p0HIlOP0hWQVKDKQZgP1NUQVRYJx4AhFovRBEICoGBMOZ3C4RIADDoWAE2AiQDiQwEIAWEbgeLChvXvQeYZx0G6A7Q0RWtqjobUbsd37GEeKCAG0M3bBwANMmI7P//hAQ1ROY/tUcgoZvOiQgRZYq0tQqtrtq0OPuUj1ppo9lTWkHmsaqsgMwUGIipKXJ0KjzWWJ80IoTP6rPTjtqOPehg2PrRVv9O597vba1taam0nOjkldFY2WftKiv9hRBCyGUufEOI8zZfPXycK8k4QmDpPJb+rFeBATgChFh44jhUamELwaUnvcN3N4+vyxNzh/+7rgJjl8eIqFgn2pcoqn36kdWPVjKD2Aj7Vkam8T8C48lH+kg3AlD/PO+2P/c9BHyCgxAQEQkJHIiIiohITly4kWi4No4sbSwrbYxpZWPb2DSWbfWbjV82/8/VXsO/m9WLiJKEJIRgIWgppVS2e2qSQHdP/Mv9S8w6s+4jdm3HKQz/zvl/qAEdu/fCxFin1onzRIzvJ/Yp+knGnttVZS63QCmyUnPSeJOT5CRN0jRNE6Rssc7eE5TCKRKMQn4IAok4YTWC4XngsP27jWMO6wjiQBMO4wU0jTJ/8+n0a1ijkTl2gL4Dyx/y4f3N+gjb6t6Os1Q111515QJCiK3YkgU04IdrvTPJpjAvKZORwLocOORPskAbu2dL4L4qsFLtEG7aMeloSF6q1Gi3UoVhDRIhJESAgAe8Qk3n0pl0P7MX8XWi3YvJXkRBtUkleIAE3aaTaucJhF80mUPrnBD6QtnqQmoPYiAOCLgpJe9NC+zAS3tT3JKlRASEjXQuF+MJISoVs7WT23oxCX0CHje+fePoIg7992ufdu/2ktyzEOBRAZxabyYv+P5JWL2MCuBUFKAwIFTiJPCoIAv5ZYyJ8D5/b2ra/gcQuqXgACjuOi4UF45w6kU4tpQr185F9/fvgrsfiwUWuLQASB0IktbywJMB8CSBpAKW4N2AuPMMKTlccsx0yp+8BDriJKdAOcZYVO7UlC7LEEvnoqvtU/u1Ktov4pFSL7QbKqFTb97fYT8qsuzu7SKmITOEUKnFJP2IeLJQuXfutW0vGWSI8qNylbK5u//yCxkgKnRAfXMTDlDoycn5CU3w+Pt3ehMsrs6IC3kUY+T0s74b1jcv/nu30KGUMoiIG4KEEETEPY7b33o1Xse3/53TruMQEZGXpMeY+l80441tl9/a1BhCqDkQkeCJSKnNX1ARc+7sCZZ24VFTQHo01K0c8CYbYr4srQ+hwE0DtgDZCeRoTFTZjHQ0NBSyuUOoWrV5oVuPXn1hQkjwnCOC4SBD9RjG9rL/EpPrm4tgq8Ht/RvYF2O7YSpOSKgQAgPiiAPh4kJ4hBCRQRB3UogPBcSfP0QpEFKhAjLffMhfwbR/C3enKXqoS088KS/0OXrnvT75nL+KkO+ZRCI//ISHWQGLU7iUEARclWja3dGRJ8kNERKBwsKlr3ewER0QoOK2Ck5SDNfgBShKMgqnUZykWV6UldLGQhgTypwXsn0vbvv+8Np9e0U/LMX1pNnt9YfGbw6QdLRQEGAZd4mw40/UG8D29mtGV4AtEqB3DQgMftsN1RXwqZ8CcO+47XstMItk96K9gzYu1YGKzL7fAMEmudlqtIGhH/hyQhy2cF3NdPib9fnX8M4yr/9zsUodLfzwzqrcAYO0omoPv3QwQ0q3WvI+UPQeu9S1WrD+w4WHkDundQzR0qPnDZ6BD8M74aXwIngY7oTr4VI4e/tP7D+Q/vcaVGhU3XJh7SQaz6Zv4U3VZE3UuM2+kXHA/lHvqr+e1t26CahP1fHaB+q9taYW1YyaWNVVXJllqMSKLHUtKjmodcUrRlHRn7zJ/3kA8o20xJKDIB/MBpDXZkfmgAz5PxtSG3NyY0piIqOOZwRhhQaQgleeu68VeB54JnPsApqx6d5r2lI+XG269xq7NAyep+lGLJ0NZAAt+8pywB6ghQyw1weB5ne65/VLUoBXTs+xRwBHDYDjNmjxSfdemhUW8CxL914jljDAIwFLcwBaZOk+jKr6AzxWAJYBtAE092vewdfnO+BhgPlvgBYr1A3mn4CrBUsPA83tdM9b5nvgUYLFZ4AWZboXd81XwXMQ7P+fgKYW7PluoIUF5q8FWgLA+tcDTe1Y9qwHWvTpXqyYj4BHBRZfD7T4ppti/ih4NGDxO0CLEux5q5GwZ/bcM+Xq37NlOtxhcNrcnvLwrig5l2Xx1+UEGVj8i9x7e/UD+OdrkN6/9CYkAXsebidi5LYUCvZfV6R+vre9NBgsfS4gpX/RghL8ix+FBO9mmhjxfhgl+ffoRnvXz3/nxvP+tBrHOxqp/Hs8r69l3WJuH4lXcpF747GvRMyuuCulgD1enHqTkFdiP6Xj5WU5tIOG+Ik1F8zqPbg4Ph4sTfTE3j/nI7V/9/8hdffuH94S4YmEjIKKYIOGzpYdew4YHLGwcXDxuOATGEj4gvviuPPgyYuUDzlfCv6nd80K8331HR34OPBe4M3AKyEzgMeBe4GbgSuB84FTgaOBQ+G6yjJPBprO9AgyfvMuAIgOvCdQh/Dbf2IArfJbPwurgqHjJlrvN9bQbsHtx6njq8D90W85/voTcC148HhUL8NvXYuKzpfKw9XI0F8gtb7r9ucIGBW2lxYatg7Pw8DxvzczmHTWeyfwbNq7weM9q9RT99lmdkLTCt4wf4+x+7IPY2k2Z1B+3Ok8i+mz9eeko4YFVD/r4XLV6zO0zLPYLWR9nECdwPHli1d6K03r7zmi4yf1NAJB2Gs5RHSURJIrWAm5Vx09bC8H1l12xFWQhuSNGGtIFPrTZudS/bpyGEY7tu35VBPt7/5oxniNeJbxY8/AyNBXnZnmaEBVTd4r4S93wFi6z9oLnm6+zN1jqHbEro6vZify7f7IOMOfv+lH0xGrc5rsKSuhMWv0XGJvQedK5bir63RSksvbHE1ridEkK1lmEyX+pbfCU/ZkRY0Z8GUZr+UcEr10oTJR4a4YdvRZnyWd4RLvGotG6UZtWnh5/+Hd8Q+93VZKKVS+v3DZnPLpxGHx/mqQS77pauKrzV/+vuw2Y3H3/ymWunlXGbOK8Gu7xSLfk+RCBl7f3FvuSz/pDs+S+jLLUc3cPDxKfpe/ai7HndNamf90om7lRCU2l8dfKZJN7fA3vi0Xdd2o9eXUvA8inxVj4V1pA3IKY1Hp+EjKkFawmx1cGonbHofvdcOyJit3mh1pIUQ/Xn/r/uKQA5t3v5uZvB7+MDhH/O0vjdxCaKEsbeEUdySaI63C6rLs/YdN4+uPSWUsc4/XzjrLuHqiqDMyHdRc9LRnW9nx/ig0658PXI7Yf+gw7sV3G/It5khwiMooyN6qC36nNA88Ex8HpaO83Z2X4pnl3bx7kML3ZUL+sRq7cPnpmpT4P49RTbor6MB8G6AEo+RfsRJNmv+Juf8gxMzXx9Pdu5F25SW/mZjvxBF3hhRyC6ctPc+WcV6eA38I2xq0EBWfhpU7pIbt6lH1H7gg9vHlLpM2WiSqVU3DSUq6UpzYkDTAuiPsJ8VYL33wyoFUSraC8SCVIJSEUcxEN8frWGLipkXoN2ZXb2HagyAX2FEY4+0pmbH8glhNyVjlS2WRkFyugsVmVXbKZnQCH+3bRUcwEuKVMpyANuZgLDmPm/JRYvxHDrNqNXWwKoRd/0Xyjrj6gWHoThJBcC+RivhoFz7e4RNQs3h5ttK8XICqFoDIXulgxDq6eyDVUfW6j9bz4tikBXIisApWPfzGR2LAHkqseSZjAZ/NfZn5jgkvb1LPZm9z9plKc53GRqO9cgzj1GDnDWut9qAIS5fwSRx3Gp3Ezsfwiw8bGUbXVq1nTrGSWh6rFTOGimtnsyDOub61Y6VLzsZkF4/gVvnWSnXCDssc5/H7K04nGId+Fo1hwLLf2o4ZhntrqfR97r8p0OQdCKkfmkeGRh4kt0BrETQv8IiUXM5pQ1fXVoFCD7Hzda+CZyXruZojoNlRRz0qU3Tokv8PAiV3SyvX04/TS3kL3oejyA874S+OuJuaqO8SBcaGrIF0jJJbYvg/D+M0Nr7gfcxnvRn16GWO3BAHiMmyiL2knqqlx9fzuxv0wyHike/H8/h4bne1Q5QhOi5ztAZiA5YZ1ayn+qFJ8X+siHT+RH54CShJmNRn+Wn/fynCJFwkbsBa3gR8+PLgJ4BUIBUfauF8RYqkppcumIlJlCw5dIqNFmuMBkYzbVRgu+0ma9Vmik6dprrrrmkeeGC6Z56p90KfBl98Mcc338z1V/fS0BEd5ocRhgVxjauFwPIBcNAAmA2ATQfAIQNg7wEwHQB7DoAVA2DbAXD0BpywAcdswHgArB8AWwyApQEwHwCHDYCdB1BSkZCQWbFCQUaGoaCgIBAwdHQsDAwYJiYMCwsNGxsTBweJEydeuLjYnDmT4uGx58IFBx+fFVeurAkIIW7cEEREkEEGsSYmYcOdB2uevFBJSSHevFHJyCA+fFDJydH58kVQUMD58WPLnz+uAAG8KSnhAgVypqIiECSImJqap2DBPGhoiGhpuRtsMIlQodyECTNIhAhCUaII6aVgSJXOiYmJkyw5HBWrQFapEmaMseyMVwczVT2SBg3szTSflQWWQpZZzsYKKzlYZRWq1TZCNtkGbyWsIWDBgAGvEF7T6lsuvsjGV1F9k43vovo7vTLYUeDPGK44dgNg0kcNjgBWQagiEmv5IuocNmjI6HJiKzG7sK8ckDGEDJACU4ijHLBEYIuLI4yT/HDlwln+eLLlsp89G3kgsuYmD4NMnYegNNwr4jEVaQYYgI15w4cWrilEoJhUcqOWXLB8aOQlRJ60khksb6FyFyapcElE3i4ZO3z44Xy+HK4Ck0JhqeIcMtJoLine truncated
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}</style></head><body style="margin:0"><div class="dc-card" style="accent-color: auto; place-content: normal; place-items: normal; place-self: auto; vertical-align: baseline; anchor-name: none; anchor-scope: none; animation-composition: replace; appearance: none; aspect-ratio: auto; backdrop-filter: none; backface-visibility: visible; background: rgb(255, 255, 255); background-blend-mode: normal; block-size: 150px; border-block: 0px rgb(0, 0, 0); border-bottom: 0px rgb(0, 0, 0); border-collapse: separate; border-end-end-radius: 2px; border-end-start-radius: 2px; border-image: none; border-inline: 0px rgb(0, 0, 0); border-left: 0px rgb(0, 0, 0); border-right: 0px rgb(0, 0, 0); border-spacing: 0px; border-start-end-radius: 2px; border-start-start-radius: 2px; border-top: 0px rgb(0, 0, 0); border-radius: 0px; bottom: auto; box-decoration-break: slice; box-shadow: none; box-sizing: content-box; break-after: auto; break-before: auto; break-inside: auto; caption-side: top; caret-color: rgb(0, 0, 0); clear: none; clip: auto; clip-path: none; clip-rule: nonzero; color: rgb(0, 0, 0); color-interpolation: srgb; color-interpolation-filters: linearrgb; color-scheme: normal; columns: auto; column-fill: balance; gap: normal; column-rule: 3px none rgb(0, 0, 0); column-span: none; contain: none; contain-intrinsic-block-size: none; contain-intrinsic-height: none; contain-intrinsic-inline-size: none; contain-intrinsic-width: none; container: none; content: normal; content-visibility: visible; counter-increment: none; counter-reset: none; counter-set: none; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: block; dominant-baseline: auto; empty-cells: show; fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; filter: none; flex: 0 1 auto; flex-flow: row; float: none; flood-color: rgb(0, 0, 0); flood-opacity: 1; font: 16px -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, system-ui, sans-serif; font-palette: normal; font-synthesis: weight style small-caps position; forced-color-adjust: auto; grid: none; grid-area: auto; height: 150px; hyphenate-character: auto; hyphenate-limit-chars: auto; hyphens: manual; image-orientation: from-image; image-rendering: auto; ime-mode: auto; inline-size: 480px; inset-block: auto; inset-inline: auto; isolation: isolate; left: auto; letter-spacing: normal; lighting-color: rgb(255, 255, 255); line-break: auto; list-style: outside; margin-block: 0px; margin-bottom: 0px; margin-inline: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; marker: none; mask: none; mask-type: luminance; math-depth: 0; math-shift: normal; math-style: normal; max-block-size: none; max-height: none; max-inline-size: none; max-width: none; min-block-size: 0px; min-height: 0px; min-inline-size: 0px; min-width: 0px; mix-blend-mode: normal; object-fit: fill; object-position: 50% 50%; offset: normal; opacity: 1; order: 0; outline: rgb(0, 0, 0) 3px; outline-offset: 0px; overflow-anchor: auto; overflow-block: hidden; overflow-clip-margin: 0px; overflow-inline: hidden; overflow-wrap: normal; overflow: hidden; overscroll-behavior-block: auto; overscroll-behavior-inline: auto; overscroll-behavior: auto; padding-block: 0px; padding-bottom: 0px; padding-inline: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; page: auto; paint-order: normal; perspective: none; perspective-origin: 240px 75px; pointer-events: auto; position: static; position-anchor: normal; position-area: none; position-try: none; position-visibility: anchors-visible; print-color-adjust: economy; quotes: auto; r: 0px; resize: none; right: auto; rotate: none; ruby-align: space-around; ruby-position: alternate; rx: auto; ry: auto; scale: none; scroll-behavior: auto; scroll-margin-block: 0px; scroll-margin-bottom: 0px; scroll-margin-inline: 0px; scroll-margin-left: 0px; scroll-margin-right: 0px; scroll-margin-top: 0px; scroll-padding-block: auto; scroll-padding-bottom: auto; scroll-padding-inline: auto; scroll-padding-left: auto; scroll-padding-right: auto; scroll-padding-top: auto; scroll-snap-align: none; scroll-snap-stop: normal; scroll-snap-type: none; scrollbar-color: auto; scrollbar-gutter: auto; scrollbar-width: auto; shape-image-threshold: 0; shape-margin: 0px; shape-outside: none; shape-rendering: auto; stop-color: rgb(0, 0, 0); stop-opacity: 1; stroke: none; stroke-dasharray: none; stroke-dashoffset: 0px; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 1px; tab-size: 8; table-layout: auto; text-align: start; text-align-last: auto; text-anchor: start; text-autospace: no-autospace; text-combine-upright: none; text-decoration: rgb(0, 0, 0); text-decoration-inset: 0px; text-decoration-skip-ink: auto; text-emphasis: none rgb(0, 0, 0); text-emphasis-position: auto; text-indent: 0px; text-justify: auto; text-orientation: mixed; text-overflow: clip; text-rendering: auto; text-shadow: none; text-transform: none; text-underline-offset: auto; text-underline-position: auto; Line truncated
}</style></head><body style="margin:0"><div class="dc-card" style="accent-color: auto; place-content: normal; place-items: normal; place-self: auto; vertical-align: baseline; anchor-name: none; anchor-scope: none; animation-composition: replace; appearance: none; aspect-ratio: auto; backdrop-filter: none; backface-visibility: visible; background: rgb(255, 255, 255); background-blend-mode: normal; block-size: 150px; border-block: 0px rgb(0, 0, 0); border-bottom: 0px rgb(0, 0, 0); border-collapse: separate; border-end-end-radius: 2px; border-end-start-radius: 2px; border-image: none; border-inline: 0px rgb(0, 0, 0); border-left: 0px rgb(0, 0, 0); border-right: 0px rgb(0, 0, 0); border-spacing: 0px; border-start-end-radius: 2px; border-start-start-radius: 2px; border-top: 0px rgb(0, 0, 0); border-radius: 0px; bottom: auto; box-decoration-break: slice; box-shadow: none; box-sizing: content-box; break-after: auto; break-before: auto; break-inside: auto; caption-side: top; caret-color: rgb(0, 0, 0); clear: none; clip: auto; clip-path: none; clip-rule: nonzero; color: rgb(0, 0, 0); color-interpolation: srgb; color-interpolation-filters: linearrgb; color-scheme: normal; columns: auto; column-fill: balance; gap: normal; column-rule: 3px none rgb(0, 0, 0); column-span: none; contain: none; contain-intrinsic-block-size: none; contain-intrinsic-height: none; contain-intrinsic-inline-size: none; contain-intrinsic-width: none; container: none; content: normal; content-visibility: visible; counter-increment: none; counter-reset: none; counter-set: none; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: block; dominant-baseline: auto; empty-cells: show; fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; filter: none; flex: 0 1 auto; flex-flow: row; float: none; flood-color: rgb(0, 0, 0); flood-opacity: 1; font: 16px -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, system-ui, sans-serif; font-palette: normal; font-synthesis: weight style small-caps position; forced-color-adjust: auto; grid: none; grid-area: auto; height: 150px; hyphenate-character: auto; hyphenate-limit-chars: auto; hyphens: manual; image-orientation: from-image; image-rendering: auto; ime-mode: auto; inline-size: 480px; inset-block: auto; inset-inline: auto; isolation: isolate; left: auto; letter-spacing: normal; lighting-color: rgb(255, 255, 255); line-break: auto; list-style: outside; margin-block: 0px; margin-bottom: 0px; margin-inline: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; marker: none; mask: none; mask-type: luminance; math-depth: 0; math-shift: normal; math-style: normal; max-block-size: none; max-height: none; max-inline-size: none; max-width: none; min-block-size: 0px; min-height: 0px; min-inline-size: 0px; min-width: 0px; mix-blend-mode: normal; object-fit: fill; object-position: 50% 50%; offset: normal; opacity: 1; order: 0; outline: rgb(0, 0, 0) 3px; outline-offset: 0px; overflow-anchor: auto; overflow-block: hidden; overflow-clip-margin: 0px; overflow-inline: hidden; overflow-wrap: normal; overflow: hidden; overscroll-behavior-block: auto; overscroll-behavior-inline: auto; overscroll-behavior: auto; padding-block: 0px; padding-bottom: 0px; padding-inline: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; page: auto; paint-order: normal; perspective: none; perspective-origin: 240px 75px; pointer-events: auto; position: static; position-anchor: normal; position-area: none; position-try: none; position-visibility: anchors-visible; print-color-adjust: economy; quotes: auto; r: 0px; resize: none; right: auto; rotate: none; ruby-align: space-around; ruby-position: alternate; rx: auto; ry: auto; scale: none; scroll-behavior: auto; scroll-margin-block: 0px; scroll-margin-bottom: 0px; scroll-margin-inline: 0px; scroll-margin-left: 0px; scroll-margin-right: 0px; scroll-margin-top: 0px; scroll-padding-block: auto; scroll-padding-bottom: auto; scroll-padding-inline: auto; scroll-padding-left: auto; scroll-padding-right: auto; scroll-padding-top: auto; scroll-snap-align: none; scroll-snap-stop: normal; scroll-snap-type: none; scrollbar-color: auto; scrollbar-gutter: auto; scrollbar-width: auto; shape-image-threshold: 0; shape-margin: 0px; shape-outside: none; shape-rendering: auto; stop-color: rgb(0, 0, 0); stop-opacity: 1; stroke: none; stroke-dasharray: none; stroke-dashoffset: 0px; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 1px; tab-size: 8; table-layout: auto; text-align: start; text-align-last: auto; text-anchor: start; text-autospace: no-autospace; text-combine-upright: none; text-decoration: rgb(0, 0, 0); text-decoration-inset: 0px; text-decoration-skip-ink: auto; text-emphasis: none rgb(0, 0, 0); text-emphasis-position: auto; text-indent: 0px; text-justify: auto; text-orientation: mixed; text-overflow: clip; text-rendering: auto; text-shadow: none; text-transform: none; text-underline-offset: auto; text-underline-position: auto; Line truncated