frontend/index.html
Rama main · réplica de lectura del repositorio original.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="format-detection" content="telephone=no" />
<meta name="color-scheme" content="dark light" />
<!-- Preload the faces painted above the fold: Manrope carries the body,
Bricolage the headline. -->
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/fonts/skins/manrope-400-73389ca9.ce093b341d.woff2" />
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/fonts/bricolage-grotesque-latin-700-normal.woff2" />
<title>PrivaTools — Free, Open-Source Privacy-First File Tools</title>
<meta name="description" content="Free, open-source file tools — PDF, image, video, and developer utilities. Browser-only when possible; isolated, self-hostable processing when needed. No account required for tools.">
<meta name="author" content="PrivaTools" />
<meta name="theme-color" content="#151619" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#edf3f9" media="(prefers-color-scheme: light)" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="PrivaTools" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="robots" content="index,follow,max-image-preview:large" />
<meta property="og:title" content="PrivaTools — Free, Open-Source Privacy-First File Tools">
<meta property="og:description" content="Free, open-source file tools — PDF, image, video, and developer utilities. Browser-only when possible; isolated backend processing when needed.">
<meta property="og:type" content="website" />
<meta property="og:url" content="https://privatools.me" />
<meta property="og:site_name" content="PrivaTools" />
<meta property="og:image" content="https://privatools.me/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="PrivaTools — Free, Open-Source Privacy-First File Tools">
<meta name="twitter:description" content="Free, open-source file tools. Browser-only when possible; isolated backend processing when needed.">
<meta name="twitter:image" content="https://privatools.me/og-image.png">
<link rel="icon" type="image/png" sizes="96x96" href="/brand/privatools-icon-96.png" />
<link rel="icon" type="image/svg+xml" href="/brand/privatools-favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="PrivaTools Journal" href="/feed.xml" />
<link rel="manifest" href="/manifest.json" />
<link rel="search" type="application/opensearchdescription+xml" title="PrivaTools" href="/opensearch.xml" />
<link rel="apple-touch-icon" sizes="180x180" href="/brand/privatools-icon-180.png" />
<style>
:root { --prepaint-bg:#edf3f9; --prepaint-ink:#23394f; --prepaint-accent:#285cc7; --prepaint-line:#ccd9e7; }
html[data-experience="play"] { --prepaint-bg:#fae7eb; --prepaint-ink:#472732; --prepaint-accent:#a32749; --prepaint-line:#c9a4ae; }
html[data-theme="dark"] { --prepaint-bg:#151619; --prepaint-ink:#f3f4f7; --prepaint-accent:#91b5ff; --prepaint-line:#383d46; }
html[data-experience="play"][data-theme="dark"] { --prepaint-bg:#181818; --prepaint-ink:#f7f4ef; --prepaint-accent:#ff9677; --prepaint-line:#42413f; }
html { background:var(--prepaint-bg); }
#prepaint-brand {
position: fixed;
z-index: 95;
top: 18px;
left: 16px;
pointer-events: none;
font-family: "Manrope", system-ui, sans-serif;
font-size: 20px;
line-height: 1;
font-weight: 700;
letter-spacing: -0.03em;
color: var(--prepaint-ink);
}
html[data-experience="play"] #prepaint-brand { font-family:"Bricolage Grotesque", "Manrope", system-ui, sans-serif; }
#prepaint-brand em { font-style: normal; color: var(--prepaint-accent); }
/* The prepaint brand exists only to paint something before hydration.
SkinAppHost sets .app-ready on mount and it retires. */
.app-ready #prepaint-brand { display: none; }
#prepaint-shell { background:var(--prepaint-bg); color:var(--prepaint-ink); }
/* The skip link is styled here, not with utility classes: Tailwind does
not scan this file, and the link has to work before the app loads.
Hidden until focused, then a solid action-colour tab over the header. */
#prepaint-skip {
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
#prepaint-skip:focus {
position: fixed; z-index: 1000; top: 12px; left: 12px;
width: auto; height: auto; margin: 0; padding: 12px 18px;
overflow: visible; clip: auto; clip-path: none;
border-radius: 10px; background: var(--prepaint-accent); color: var(--prepaint-bg);
font: 650 15px/1.2 "Manrope", system-ui, sans-serif; text-decoration: none;
outline: 2px solid var(--prepaint-ink); outline-offset: 3px;
box-shadow: 0 10px 28px rgba(0, 0, 0, .2);
}
html[data-experience="play"] #prepaint-skip:focus { border-radius: 999px; font-family: "Outfit", "Manrope", system-ui, sans-serif; }
#prepaint-shell header { border-bottom:1px solid var(--prepaint-line); }
/* Device preference still paints the initial frame when JS is unavailable. */
@media (prefers-color-scheme: dark) {
html:not([data-theme]) { --prepaint-bg:#151619; --prepaint-ink:#f3f4f7; --prepaint-accent:#91b5ff; --prepaint-line:#383d46; }
}
@media (min-width: 640px) {
#prepaint-brand { left: 24px; }
}
</style>
<script id="experience-prepaint">
/* Keep these storage keys and colors in step with lib/experience.ts.
Resolve both independent choices before the first visible frame. */
(function () {
try {
var d = document.documentElement;
function read(key) { try { return window.localStorage.getItem(key); } catch (e) { return null; } }
function appearance(value) { return value === 'midnight' ? 'dark' : value === 'system' || value === 'light' || value === 'dark' ? value : null; }
var experience = read('privatools.experience') === 'play' ? 'play' : 'air';
var pref = appearance(read('privatools.appearance')) || appearance(read('privatools.daylight.theme')) || 'system';
var theme = pref === 'system' ? (!window.matchMedia || window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark') : pref;
d.setAttribute('data-experience', experience);
d.setAttribute('data-appearance', pref);
d.setAttribute('data-theme', theme);
d.style.colorScheme = theme;
var colors = {air:{light:'#edf3f9',dark:'#151619'},play:{light:'#fae7eb',dark:'#181818'}};
document.querySelectorAll('meta[name="theme-color"]').forEach(function(meta) { meta.content = colors[experience][theme]; });
} catch (e) { /* private mode — the CSS media fallback covers it */ }
})();
</script>
</head>
<body>
<!-- Air and Play share the mounted tools and account state. The visitor's
chosen experience and appearance are resolved before hydration. -->
<!-- The only skip link. SkinAppHost retargets it at the app's <main> once
it mounts; its styles are in the <style> above. -->
<a id="prepaint-skip" href="#main-content">Skip to main content</a>
<div id="prepaint-brand" aria-hidden="true">PrivaTools<em>.</em></div>
<div id="root">
<div id="prepaint-shell" style="min-height:100vh;font-family:Manrope,system-ui,sans-serif">
<header style="height:56px"></header>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
<!-- Service worker registration lives in src/lib/sw-register.ts so it
runs in production only and handles updates correctly (skipWaiting +
controllerchange reload). Don't re-add an inline registration here. -->
<!-- First-party pageviews and privacy controls are wired once by analyticsBeacon.ts. -->
</body>
</html>