The palette lived as ~620 hex literals in inline styles plus a few hundred more in
the stylesheets, so there was no single thing to change. It is now 113 custom
properties in src/styles/theme.css, in two blocks.
src/scripts/theme-boot.js decides the theme before first paint — blocking, in <head>,
above the stylesheet, because a deferred script paints dark first and corrects itself.
It stores the mode ('system' | 'light' | 'dark'), never the colour it resolved to, and
stamps data-theme so an explicit choice can beat the media query. The switcher in the
header is a view onto it.
A filled accent stays the brand colour in both themes — the ink on it is near-black
either way — while an accent used as text darkens to clear 4.5:1 on white. A colour
reaches a fill by four routes (a style property, a constant, a helper argument, an SVG
source string), and tests/theme-switching.test.mjs covers all four.
The dark theme is unchanged: every colour declaration the previous build produced comes
out of this one identically once the properties are resolved.
Also: the roadmap drops its status chips on mobile, and Roadmap.jsx no longer splits a
colour with parseInt at runtime, which a var() reference cannot survive.
241 lines
22 KiB
HTML
241 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<!-- Generated by scripts/build-i18n.js from templates/index.template.html and
|
|
locales/*.json. Edits made here are overwritten by the next build. -->
|
|
<html lang="{{HTML_LANG}}" dir="{{HTML_DIR}}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<!-- viewport-fit=cover is what makes env(safe-area-inset-*) resolve to anything
|
|
other than 0 on notched iPhones; the composer already pads for it.
|
|
interactive-widget=resizes-content is honoured by Chrome/Android; iOS
|
|
ignores it, which is why the shell also tracks visualViewport in JS. -->
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
|
|
<meta http-equiv="Content-Security-Policy"
|
|
content="default-src 'self';
|
|
script-src 'self';
|
|
style-src 'self' 'unsafe-inline';
|
|
font-src 'self' data:;
|
|
connect-src 'self' stun: stuns: turn: turns:;
|
|
img-src 'self' data:;
|
|
media-src 'self' blob:;
|
|
object-src 'none';
|
|
frame-src 'none';
|
|
worker-src 'self';
|
|
manifest-src 'self';
|
|
form-action 'self';
|
|
base-uri 'none';
|
|
upgrade-insecure-requests;">
|
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
|
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
|
|
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
|
|
|
<!-- PWA Manifest -->
|
|
<link rel="manifest" href="{{MANIFEST}}">
|
|
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=BUILD_VERSION">
|
|
|
|
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
|
|
from localStorage while the parser is still in <head>, so the page paints in the
|
|
chosen theme on the first frame instead of flashing dark and correcting itself.
|
|
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
|
|
<script src="/src/scripts/theme-boot.js?v=BUILD_VERSION"></script>
|
|
|
|
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
|
|
not replayed, so the listener has to exist before Chrome decides the page
|
|
is installable. Deferred/module scripts are already too late. -->
|
|
<script src="/src/scripts/pwa-install-capture.js?v=BUILD_VERSION"></script>
|
|
|
|
<!-- PWA Meta Tags -->
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="format-detection" content="telephone=no">
|
|
<!-- "default", not "black-translucent", now that there are two themes.
|
|
black-translucent draws the clock and battery in white and lets the page run under
|
|
them; on a light page that is white on near-white, and the meta is read once at
|
|
launch so no script can correct it. With "default" iOS tints the status bar from
|
|
<meta name="theme-color"> below and picks the glyph colour to contrast with it, so
|
|
it follows the theme on its own. The trade is that the page no longer extends under
|
|
the bar, which makes env(safe-area-inset-top) 0 in standalone — src/styles/pwa.css
|
|
already derives --sb-safe-top from that env(), so it collapses to 0 and the header
|
|
sits where it does in a browser tab. -->
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="SecureBit">
|
|
<meta name="application-name" content="SecureBit">
|
|
<meta name="msapplication-TileColor" content="#ff6b35">
|
|
<meta name="msapplication-config" content="/browserconfig.xml">
|
|
|
|
<!-- iOS Splash Screens - Полная конфигурация с генератора -->
|
|
<!-- Landscape -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_17_Pro_Max__iPhone_16_Pro_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_17_Pro__iPhone_17__iPhone_16_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 420px) and (device-height: 912px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_Air_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_16e__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_11__iPhone_XR_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png">
|
|
|
|
<!-- iPad Landscape -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/13__iPad_Pro_M4_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/12.9__iPad_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/11__iPad_Pro_M4_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/10.9__iPad_Air_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/10.5__iPad_Air_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/10.2__iPad_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/logo/splash/splash_screens/8.3__iPad_Mini_landscape.png">
|
|
|
|
<!-- Portrait -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_17_Pro_Max__iPhone_16_Pro_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_17_Pro__iPhone_17__iPhone_16_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 420px) and (device-height: 912px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_Air_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_16e__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_11__iPhone_XR_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png">
|
|
|
|
<!-- iPad Portrait -->
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/13__iPad_Pro_M4_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/12.9__iPad_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/11__iPad_Pro_M4_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/10.9__iPad_Air_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/10.5__iPad_Air_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/10.2__iPad_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png">
|
|
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
|
|
|
<!-- Apple Touch Icons -->
|
|
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=BUILD_VERSION">
|
|
<link rel="apple-touch-icon" sizes="57x57" href="/logo/icon-57x57.png">
|
|
<link rel="apple-touch-icon" sizes="60x60" href="/logo/icon-60x60.png">
|
|
<link rel="apple-touch-icon" sizes="72x72" href="/logo/icon-72x72.png">
|
|
<link rel="apple-touch-icon" sizes="76x76" href="/logo/icon-76x76.png">
|
|
<link rel="apple-touch-icon" sizes="114x114" href="/logo/icon-114x114.png">
|
|
<link rel="apple-touch-icon" sizes="120x120" href="/logo/icon-120x120.png">
|
|
<link rel="apple-touch-icon" sizes="144x144" href="/logo/icon-144x144.png">
|
|
<link rel="apple-touch-icon" sizes="152x152" href="/logo/icon-152x152.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=BUILD_VERSION">
|
|
|
|
<!-- Microsoft Tiles -->
|
|
<meta name="msapplication-TileColor" content="#ff6b35">
|
|
<meta name="msapplication-TileImage" content="./logo/icon-144x144.png">
|
|
<meta name="msapplication-config" content="/browserconfig.xml">
|
|
|
|
<!-- Theme colors.
|
|
Two tags with media queries, which is how the browser answers before any script has
|
|
run and how a client with no JavaScript answers permanently. They are the page's
|
|
own ground (--sb-bg in src/styles/theme.css), not the brand orange: this paints the
|
|
Android address bar and the iOS status bar, and a strip in a different colour from
|
|
the page reads as a rendering fault rather than as branding.
|
|
Once theme-boot.js runs it drops the media attributes and writes the resolved
|
|
colour into both, because a media query cannot express "light on a dark system",
|
|
which is exactly what someone who picked a theme by hand has asked for. -->
|
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f0f11">
|
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f6f7f8">
|
|
<meta name="msapplication-navbutton-color" content="#0f0f11">
|
|
|
|
<!-- Security Headers for PWA - CSP is already defined above -->
|
|
|
|
|
|
<!-- SEO -->
|
|
<title>{{TITLE}}</title>
|
|
<meta name="description" content="{{DESCRIPTION}}">
|
|
<meta name="author" content="{{AUTHOR}}">
|
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
|
<link rel="canonical" href="{{CANONICAL}}">
|
|
{{HREFLANG_LINKS}}
|
|
|
|
<!-- Open Graph -->
|
|
<meta property="og:site_name" content="{{SITE_NAME}}">
|
|
<meta property="og:title" content="{{OG_TITLE}}">
|
|
<meta property="og:description" content="{{OG_DESCRIPTION}}">
|
|
<meta property="og:url" content="{{CANONICAL}}">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:locale" content="{{OG_LOCALE}}">
|
|
{{OG_LOCALE_ALTERNATE}}
|
|
<meta property="og:image" content="{{SOCIAL_CARD}}">
|
|
<meta property="og:image:secure_url" content="{{SOCIAL_CARD}}">
|
|
<meta property="og:image:type" content="image/png">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:image:alt" content="{{OG_IMAGE_ALT}}">
|
|
|
|
<!-- Twitter Card -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="{{TWITTER_TITLE}}">
|
|
<meta name="twitter:description" content="{{TWITTER_DESCRIPTION}}">
|
|
<meta name="twitter:image" content="{{SOCIAL_CARD}}">
|
|
<meta name="twitter:image:alt" content="{{TWITTER_IMAGE_ALT}}">
|
|
|
|
<!-- Structured data (schema.org) -->
|
|
<script type="application/ld+json">
|
|
{{JSONLD}}
|
|
</script>
|
|
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
|
both execute in document order after parsing, so React still runs before the
|
|
app modules below, but the parser / first paint is no longer blocked. -->
|
|
<script defer src="/config/ice-servers.js?v=BUILD_VERSION"></script>
|
|
<script defer src="/libs/react/react.production.min.js"></script>
|
|
<script defer src="/libs/react-dom/react-dom.production.min.js"></script>
|
|
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
|
|
it never executes the snippet. Loaded in manual mode (no auto-highlight).
|
|
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
|
|
<script defer src="/libs/prism/prism.js"></script>
|
|
<!-- One render-blocking stylesheet, built by scripts/build-css.js from the Tailwind
|
|
output, the Inter @font-face declarations and the five hand-written sheets, in
|
|
that order — they argue with each other on source order, and concatenation
|
|
preserves it exactly. There used to be eight separate <link> tags here and at
|
|
the end of <body>: 21 KB of CSS in total, but eight round trips before the
|
|
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
|
|
connection. Bytes were never the problem; requests were. -->
|
|
<link rel="stylesheet" href="/assets/app.css?v=BUILD_VERSION">
|
|
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=BUILD_VERSION">
|
|
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
|
|
answers for every weight, so there is one to preload rather than the two of five
|
|
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
|
|
fa-brands are loaded on demand by their CSS, rarely on the first screen. -->
|
|
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
|
|
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
|
<script defer src="/src/scripts/load-async-css.js?v=BUILD_VERSION"></script>
|
|
<noscript>
|
|
<link rel="stylesheet" href="/libs/prism/prism.css">
|
|
</noscript>
|
|
<script defer src="/src/scripts/fa-check.js?v=BUILD_VERSION"></script>
|
|
<!-- This locale's dictionary, registered before anything can ask for a string.
|
|
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
|
|
falls back to it); every other page loads its own here instead of all thirteen
|
|
riding along inside dist/app.js. Absent on the default locale's own page. -->
|
|
{{LOCALE_DICT}}
|
|
<!-- Update Manager - система принудительного обновления -->
|
|
<script defer src="/src/utils/updateManager.js?v=BUILD_VERSION"></script>
|
|
<script type="module" src="/src/components/UpdateChecker.jsx?v=BUILD_VERSION"></script>
|
|
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
|
|
app has mounted and the browser is idle, so the 142 KB it weighs is off the
|
|
first load. src/components/QRScanner.js used to be here too and registered
|
|
window.QRScanner, which nothing has ever read. -->
|
|
</head>
|
|
<body>
|
|
<div id="root">
|
|
{{PRERENDER}}
|
|
</div>
|
|
<script type="module" src="/dist/app-boot.js?v=BUILD_VERSION"></script>
|
|
<script type="module" src="/dist/app.js?v=BUILD_VERSION"></script>
|
|
|
|
<script defer src="/src/scripts/pwa-register.js?v=BUILD_VERSION"></script>
|
|
<script src="/src/pwa/install-prompt.js?v=BUILD_VERSION" type="module"></script>
|
|
<script src="/src/pwa/pwa-manager.js?v=BUILD_VERSION" type="module"></script>
|
|
<script defer src="/src/scripts/pwa-offline-test.js?v=BUILD_VERSION"></script>
|
|
</body>
|
|
</html> |