Add SEO baseline + rebuild www/ (mobile toc/prefs polish, responsive viewport)

SEO: robots.txt, sitemap.xml, canonical, OG/Twitter tags, JSON-LD
(ProfessionalService) on / and /folders/, plus a reused screenshot as
og-image.png. prepare-www.mjs now copies robots/sitemap/og-image into
the build. Also carries pending mobile layout tweaks (toc/prefs bar,
viewport-fit=cover) that were already staged in the working tree.
This commit is contained in:
2026-07-08 10:58:46 -04:00
parent 6a571de240
commit 6949bee6e6
22 changed files with 507 additions and 94 deletions
+30 -4
View File
@@ -7,7 +7,7 @@ Homepage design concepts for Levkin Inc.
| **Spec** | `/spec/` | Company story as RFC-style documentation |
| **Folder** | `/stack-folder/` | Manila folders L0L7, sticky tab stack, site previews |
Open `/` to pick a direction.
**Production** (`levkin.ca`): `/` → spec, `/folders/` → stack. Dev picker at `/` only when running `npm run dev` (not deployed).
## Develop
@@ -16,7 +16,7 @@ npm install
npm run dev
```
Vite default: `http://localhost:5173`
Dev server: `http://localhost:5175` (5173 is often taken by other apps)
| Page | URL |
|------|-----|
@@ -28,9 +28,22 @@ Vite default: `http://localhost:5173`
```bash
npm run build
# dist/
# dist/ — Vite multi-page output
npm run build:www
# www/ — production layout for nginx (commit www/ before deploy)
```
### Deploy (pve10 LXC 220)
```bash
npm run build:www
git add www/ && git commit -m "Rebuild www" && git push origin main
ssh root@10.0.10.10 'pct exec 220 -- bash -c "cd /var/www/levkin && git pull origin main"'
```
Requires DNS: `levkin.ca` and `www.levkin.ca` A → your home IP (same as other `*.levkin.ca` sites). Caddy on VM 106 proxies to `10.0.10.60:80`.
## Folder stack (`/stack-folder/`)
Eight layers with labeled tabs. Scroll pins earlier tabs on a shared rail; L7 joins last. Scroll stops when all tabs align.
@@ -52,9 +65,22 @@ Writes PNGs used in `stack-folder/previews/` (spec, auto, caseware, iliadobkin,
```bash
npm run dev
STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
STACK_URL=http://localhost:5175/stack-folder/ npm run test:folder
```
Playwright runs **desktop** (tab alignment at max scroll) and **mobile** (bottom tab rail, no horizontal overflow, L3 jump).
### Test on a phone
Dev server listens on your LAN (`host: true` in Vite config):
```bash
npm run dev
# open http://<your-lan-ip>:5175/stack-folder/ on the phone (same WiFi)
```
Production: [levkin.ca/folders/](https://levkin.ca/folders/) — on viewports ≤720px the side rail becomes a **bottom tab bar** (L0L7) with safe-area padding for iOS.
## Related sites
- [auto.levkin.ca](https://auto.levkin.ca)
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://levkin.ca/sitemap.xml
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://levkin.ca/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://levkin.ca/folders/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+1 -1
View File
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const out = join(root, 'stack-folder/previews');
const base = process.env.PREVIEW_BASE || 'http://localhost:5177';
const base = process.env.PREVIEW_BASE || 'http://localhost:5175';
mkdirSync(out, { recursive: true });
+3
View File
@@ -37,6 +37,9 @@ await mkdir(join(www, 'folders'), { recursive: true });
await cp(join(dist, 'assets'), join(www, 'assets'), { recursive: true });
await cp(join(root, 'public', 'favicon.svg'), join(www, 'favicon.svg'));
await cp(join(root, 'public', 'robots.txt'), join(www, 'robots.txt'));
await cp(join(root, 'public', 'sitemap.xml'), join(www, 'sitemap.xml'));
await cp(join(root, 'public', 'og-image.png'), join(www, 'og-image.png'));
const specHtml = await readFile(join(dist, 'spec', 'index.html'), 'utf8');
await writeFile(
+80 -20
View File
@@ -1,14 +1,15 @@
/**
* Tab rail alignment tests for /stack-folder/.
* Run: STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
* Tab rail alignment tests for /stack-folder/ (desktop + mobile).
* Run: STACK_URL=http://localhost:5175/stack-folder/ npm run test:folder
*/
import { chromium } from 'playwright';
import { chromium, devices } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
const URL = process.env.STACK_URL || 'http://localhost:5175/stack-folder/';
const VERBOSE = process.env.VERBOSE === '1';
async function runViewport(name, contextOptions) {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
const page = await browser.newPage(contextOptions);
await page.goto(URL, { waitUntil: 'networkidle' });
async function tabTops() {
@@ -29,36 +30,95 @@ async function tabTops() {
});
}
if (VERBOSE) console.log('initial', await tabTops());
if (VERBOSE) console.log(`${name} initial`, await tabTops());
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(600);
if (VERBOSE) console.log('max scroll', await tabTops());
if (VERBOSE) console.log(`${name} max scroll`, await tabTops());
await page.click('[data-goto="7"]');
await page.waitForTimeout(800);
if (VERBOSE) console.log('goto L7', await tabTops());
const railVisible = await page.evaluate(() => {
const rail = document.querySelector('.tab-rail');
if (!rail) return false;
return getComputedStyle(rail).display !== 'none';
});
const fail = await page.evaluate(() => {
if (name === 'mobile' && !railVisible) {
await browser.close();
return [`${name}: mobile tab rail not visible`];
}
const foldIssues = await page.evaluate(({ isMobile }) => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const min = Math.min(...tops);
const max = Math.max(...tops);
const issues = [];
const spreadLimit = isMobile ? 40 : 30;
if (document.documentElement.scrollWidth > window.innerWidth + 1) {
issues.push('horizontal overflow');
}
if (max - min > spreadLimit) {
issues.push(`tab row spread ${Math.round(max - min)}px (want ≤${spreadLimit})`);
}
if (!document.querySelector('.mount')?.classList.contains('is-folded')) {
issues.push('mount not folded at max scroll');
}
return issues;
}, { isMobile: name === 'mobile' });
if (foldIssues.length) {
await browser.close();
return foldIssues.map((issue) => `${name}: ${issue}`);
}
if (name === 'mobile') {
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(200);
await page.locator('.tab-rail .rail-tab[data-goto="3"]').tap();
await page.waitForTimeout(800);
if (VERBOSE) console.log(`${name} rail goto L3`, await tabTops());
} else {
await page.click('[data-goto="7"]');
await page.waitForTimeout(800);
if (VERBOSE) console.log(`${name} goto L7`, await tabTops());
}
const fail = await page.evaluate(({ isMobile }) => {
const issues = [];
if (isMobile) {
const rail = document.querySelector('.tab-rail .rail-tab[data-goto="3"]');
const railBox = rail?.getBoundingClientRect();
if (!railBox || railBox.height < 40) issues.push('rail tap target too small');
if (window.scrollY < 100) issues.push('rail L3 tap did not scroll');
} else {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
const issues = [];
if (max - min > 30) issues.push(`tab row spread ${Math.round(max - min)}px (want ≤30)`);
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30)
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30) {
issues.push('L7 not aligned with L0');
if (!document.querySelector('.mount')?.classList.contains('is-folded'))
issues.push('mount not folded at max scroll');
}
}
return issues;
});
}, { isMobile: name === 'mobile' });
await browser.close();
return fail.map((issue) => `${name}: ${issue}`);
}
if (fail.length) {
console.error('FAIL:', fail.join('; '));
const desktop = { viewport: { width: 1400, height: 900 } };
const mobile = { ...devices['iPhone 13'], hasTouch: true };
const results = [
...(await runViewport('desktop', desktop)),
...(await runViewport('mobile', mobile)),
];
if (results.length) {
console.error('FAIL:', results.join('; '));
process.exit(1);
}
console.log('PASS: stack-folder tab rail');
console.log('PASS: stack-folder (desktop + mobile)');
+33 -2
View File
@@ -2,10 +2,37 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Specification</title>
<meta name="description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:title" content="Levkin — Company Specification" />
<meta property="og:description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta property="og:image" content="https://levkin.ca/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Levkin — Company Specification" />
<meta name="twitter:description" content="Levkin Inc.: software development and automation for production systems." />
<meta name="twitter:image" content="https://levkin.ca/og-image.png" />
<link rel="canonical" href="https://levkin.ca/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": "Levkin Inc.",
"url": "https://levkin.ca/",
"description": "Software development and automation for production systems.",
"email": "ilia@levkine.ca",
"areaServed": ["North America", "Europe"],
"sameAs": [
"https://auto.levkin.ca/",
"https://caseware.levkin.ca/",
"https://iliadobkin.com/"
]
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
@@ -24,20 +51,23 @@
<a class="skip-link" href="#main">Skip to specification</a>
<aside class="toc" aria-labelledby="toc-heading">
<div class="toc-head">
<a href="/folders/" class="back">← folders</a>
<p class="toc-doc-id">LK-SPEC-1.0</p>
</div>
<h2 id="toc-heading" class="toc-title">Table of contents</h2>
<nav aria-label="Specification sections">
<a href="#preamble"><span class="toc-num"></span> Preamble</a>
<a href="#scope"><span class="toc-num">1</span> Scope</a>
<a href="#services"><span class="toc-num">2</span> Services</a>
<a href="#working"><span class="toc-num">3</span> Terms of engagement</a>
<a href="#working"><span class="toc-num">3</span> <span class="toc-long">Terms of engagement</span><span class="toc-short">Terms</span></a>
<a href="#links"><span class="toc-num">4</span> References</a>
<a href="#contact"><span class="toc-num">5</span> Contact</a>
</nav>
<section class="prefs" aria-labelledby="prefs-heading">
<h2 id="prefs-heading" class="prefs-heading">Display</h2>
<div class="prefs-bar">
<fieldset class="theme-field">
<legend class="theme-legend">Theme</legend>
<div class="theme-options" role="radiogroup" aria-label="Color theme">
@@ -64,6 +94,7 @@
</div>
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
</div>
</div>
</section>
<p class="meta">Levkin Inc.<br />Effective May 2026</p>
+180 -14
View File
@@ -228,13 +228,17 @@ a:hover { color: var(--link-hover); }
text-align: right;
}
.toc-short {
display: none;
}
.toc nav a:hover,
.toc nav a:hover .toc-num { color: var(--link); }
.toc nav a.is-active {
font-weight: 600;
background: var(--nav-active);
outline: 1px solid var(--rule);
box-shadow: inset 0 0 0 1px var(--rule);
}
.toc nav a.is-active .toc-num { color: var(--ink); }
@@ -256,23 +260,41 @@ a:hover { color: var(--link-hover); }
.theme-field { border: 0; margin: 0 0 0.55rem; padding: 0; }
.theme-legend { font-size: 0.58rem; color: var(--muted); margin-bottom: 0.25rem; }
.theme-options { display: flex; gap: 0.15rem; }
.theme-option { flex: 1; cursor: pointer; }
.theme-option input { position: absolute; opacity: 0; width: 0; height: 0; }
.theme-option { flex: 1; cursor: pointer; position: relative; }
.theme-option input {
position: absolute;
inset: 0;
opacity: 0;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
}
.theme-option span {
.theme-option span,
.font-btn {
display: flex;
align-items: center;
justify-content: center;
min-height: 2.25rem;
min-width: 2.25rem;
border: 1px solid var(--rule);
background: var(--sheet);
color: var(--ink);
}
.theme-option span {
width: 100%;
}
.theme-option input:checked + span {
font-weight: 600;
background: var(--nav-active);
outline: 1px solid var(--rule-strong);
box-shadow: inset 0 0 0 1px var(--rule-strong);
}
.prefs-bar {
display: contents;
}
.font-controls {
@@ -288,11 +310,6 @@ a:hover { color: var(--link-hover); }
.font-btn {
font-family: var(--mono);
font-size: 0.68rem;
min-width: 2.25rem;
min-height: 2.25rem;
border: 1px solid var(--rule);
background: var(--sheet);
color: var(--ink);
cursor: pointer;
}
@@ -617,19 +634,168 @@ a:hover { color: var(--link-hover); }
.toc {
position: static;
height: auto;
padding: 0.65rem 0.75rem 0.55rem;
border-right: none;
border-bottom: 1px solid var(--rule);
}
.toc-head {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.35rem 0.65rem;
margin-bottom: 0.45rem;
}
.back {
margin-bottom: 0;
}
.toc-doc-id {
margin-bottom: 0;
letter-spacing: 0.08em;
}
.toc-title {
display: none;
}
.toc-short {
display: inline;
}
.toc-long {
display: none;
}
.toc nav {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
flex-wrap: nowrap;
gap: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
border: 1px solid var(--rule);
background: var(--sheet);
}
.toc nav::-webkit-scrollbar {
display: none;
}
.toc nav a {
min-height: auto;
padding: 0.3rem 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.25rem;
flex: 0 0 auto;
min-height: 2.25rem;
padding: 0.35rem 0.55rem;
border: none;
border-right: 1px solid var(--rule);
border-radius: 0;
background: transparent;
white-space: nowrap;
font-size: 0.58rem;
}
.toc nav a:last-child {
border-right: none;
}
.toc nav a.is-active {
box-shadow: none;
}
.toc-num {
text-align: left;
}
.prefs {
margin-top: 0;
padding-top: 0.45rem;
border-top: none;
}
.prefs-bar {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.35rem;
width: 100%;
}
.prefs-heading,
.theme-legend,
.font-label {
display: none;
}
.theme-field,
.font-controls {
display: flex;
min-width: 0;
margin: 0;
border: 1px solid var(--rule);
background: var(--sheet);
}
.theme-options {
display: flex;
flex: 1;
width: 100%;
gap: 0;
}
.theme-option {
flex: 1;
min-width: 0;
}
.theme-option span,
.font-btn {
width: 100%;
height: 2.25rem;
min-width: 0;
min-height: 2.25rem;
padding: 0 0.15rem;
border: none;
border-radius: 0;
font-size: 0.56rem;
box-shadow: none;
}
.theme-option:not(:first-child) span,
.font-btn + .font-btn {
border-left: 1px solid var(--rule);
}
.theme-option input:checked + span {
box-shadow: none;
}
.font-controls {
flex-wrap: nowrap;
gap: 0;
}
.font-buttons {
display: flex;
flex: 1;
min-width: 0;
gap: 0;
}
.font-btn {
flex: 1;
}
.font-scale-readout {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 2.35rem;
border-left: 1px solid var(--rule);
font-size: 0.55rem;
color: var(--muted);
}
.meta { display: none; }
+45 -2
View File
@@ -681,8 +681,51 @@ a.site-preview:hover {
}
@media (max-width: 720px) {
.tab-rail { display: none; }
.mount { padding-right: 1rem; }
.site-header {
font-size: 0.58rem;
line-height: 1.35;
padding: 0.45rem 0.65rem;
}
.tab-rail {
top: auto;
bottom: 0;
left: 0;
right: 0;
flex-direction: row;
flex-wrap: nowrap;
gap: 0.35rem;
padding: 0.45rem 0.5rem calc(0.45rem + env(safe-area-inset-bottom, 0px));
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
border: none;
border-top: 1px solid #4a4844;
}
.tab-rail::-webkit-scrollbar {
display: none;
}
.rail-tab {
flex: 0 0 auto;
min-height: 2.75rem;
min-width: 2.5rem;
padding: 0.4rem 0.55rem;
font-size: 0.62rem;
}
.mount {
padding-right: 1rem;
padding-bottom: calc(3.75rem + env(safe-area-inset-bottom, 0px));
--folder-body-h: min(28rem, calc(100dvh - var(--stack-nav) - var(--stack-tab-h) - 5.5rem));
}
.tab {
min-height: 2.75rem;
padding-top: 0.5rem;
}
:root { --tab-offset: 1.75rem; }
}
+8 -1
View File
@@ -2,9 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Files</title>
<meta name="description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:title" content="Levkin — Company Files" />
<meta property="og:description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/folders/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta name="twitter:card" content="summary" />
<link rel="canonical" href="https://levkin.ca/folders/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+5
View File
@@ -2,6 +2,11 @@ import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
server: {
host: true,
port: 5175,
strictPort: true,
},
build: {
rollupOptions: {
input: {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -3
View File
@@ -2,16 +2,23 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Files</title>
<meta name="description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:title" content="Levkin — Company Files" />
<meta property="og:description" content="Levkin Inc. — company specification, filed by section." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/folders/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta name="twitter:card" content="summary" />
<link rel="canonical" href="https://levkin.ca/folders/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/stackFolder-4cxThntN.js"></script>
<script type="module" crossorigin src="/assets/stackFolder-_Qi-mHWa.js"></script>
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="stylesheet" crossorigin href="/assets/stackFolder-oqVQaMMI.css">
<link rel="stylesheet" crossorigin href="/assets/stackFolder-DTsVZwGE.css">
<script defer src="https://stats.levkin.ca/script.js" data-website-id="2a3bc7b2-b827-4770-8b31-c94016667734"></script>
</head>
<body>
+35 -4
View File
@@ -2,10 +2,37 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Levkin — Company Specification</title>
<meta name="description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:title" content="Levkin — Company Specification" />
<meta property="og:description" content="Levkin Inc.: software development and automation for production systems." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://levkin.ca/" />
<meta property="og:site_name" content="Levkin Inc." />
<meta property="og:image" content="https://levkin.ca/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Levkin — Company Specification" />
<meta name="twitter:description" content="Levkin Inc.: software development and automation for production systems." />
<meta name="twitter:image" content="https://levkin.ca/og-image.png" />
<link rel="canonical" href="https://levkin.ca/" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": "Levkin Inc.",
"url": "https://levkin.ca/",
"description": "Software development and automation for production systems.",
"email": "ilia@levkine.ca",
"areaServed": ["North America", "Europe"],
"sameAs": [
"https://auto.levkin.ca/",
"https://caseware.levkin.ca/",
"https://iliadobkin.com/"
]
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
@@ -18,29 +45,32 @@
} catch (e) { /* ignore */ }
})();
</script>
<script type="module" crossorigin src="/assets/spec-BY4A7Wkv.js"></script>
<script type="module" crossorigin src="/assets/spec-B2lX7K2V.js"></script>
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="stylesheet" crossorigin href="/assets/spec-BoGenpAF.css">
<link rel="stylesheet" crossorigin href="/assets/spec-Cx4F7IDd.css">
<script defer src="https://stats.levkin.ca/script.js" data-website-id="2a3bc7b2-b827-4770-8b31-c94016667734"></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to specification</a>
<aside class="toc" aria-labelledby="toc-heading">
<div class="toc-head">
<a href="/folders/" class="back">← folders</a>
<p class="toc-doc-id">LK-SPEC-1.0</p>
</div>
<h2 id="toc-heading" class="toc-title">Table of contents</h2>
<nav aria-label="Specification sections">
<a href="#preamble"><span class="toc-num"></span> Preamble</a>
<a href="#scope"><span class="toc-num">1</span> Scope</a>
<a href="#services"><span class="toc-num">2</span> Services</a>
<a href="#working"><span class="toc-num">3</span> Terms of engagement</a>
<a href="#working"><span class="toc-num">3</span> <span class="toc-long">Terms of engagement</span><span class="toc-short">Terms</span></a>
<a href="#links"><span class="toc-num">4</span> References</a>
<a href="#contact"><span class="toc-num">5</span> Contact</a>
</nav>
<section class="prefs" aria-labelledby="prefs-heading">
<h2 id="prefs-heading" class="prefs-heading">Display</h2>
<div class="prefs-bar">
<fieldset class="theme-field">
<legend class="theme-legend">Theme</legend>
<div class="theme-options" role="radiogroup" aria-label="Color theme">
@@ -67,6 +97,7 @@
</div>
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
</div>
</div>
</section>
<p class="meta">Levkin Inc.<br />Effective May 2026</p>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://levkin.ca/sitemap.xml
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://levkin.ca/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://levkin.ca/folders/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>