This commit is contained in:
2026-08-10 12:01:03 +07:00
commit 265b1202ad
95 changed files with 27718 additions and 0 deletions

69
.design/DESIGN.md Normal file
View File

@@ -0,0 +1,69 @@
---
name: "Malenia"
category: Brands
surface: web
colors:
cream: "#faf7f2"
warm-white: "#f5f0e8"
ink: "#1a1610"
ink-soft: "#3d3428"
gold: "#c9a84c"
---
# Malenia
> Category: Brands
> Surface: web
*Свобода. Практика. Простота.*
A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.
## Color Palette
| Role | Name | Hex | Usage |
| --- | --- | --- | --- |
| background | Cream | `#faf7f2` | Main page background |
| surface | Warm White | `#f5f0e8` | Secondary surfaces |
| foreground | Ink | `#1a1610` | Primary text and primary buttons |
| muted | Ink Soft | `#3d3428` | Secondary text |
| accent | Gold | `#c9a84c` | Highlights, emblems, and key accents |
## Typography
- **Display:** Cormorant Garamond — weights 400, 700 — fallbacks: serif
- **Body:** Tenor Sans — weights 400, 700 — fallbacks: sans-serif
## Voice & Tone
- **Adjectives:** Sophisticated, Quiet, Essential, Free
- **Tone:** Restrained, archival, sophisticated, and minimal.
### Messaging pillars
- Network freedom
- Practical simplicity
- Aesthetic restraint
### Vocabulary
- **Use:** (none yet)
- **Avoid:** (none yet)
## Imagery
- **Style:** Geometric, symbolic, and minimalist with a focus on symmetry and gold-on-cream palettes.
- **Subjects:** Diamonds, Rings, Symmetrical emblems
- **Treatment:** Fine-line vector art, subtle noise textures, and centered compositions.
- **Avoid:** (none yet)
## Layout
- **Radius:** 8px
- **Border weight:** 1px
- **Spacing:** 8px baseline grid
### Posture rules
- Extreme whitespace and centered alignment
- Strict horizontal rules for structural division
- Custom cursor interactions (dot and ring)
- Subtle grain/noise overlay for organic feel
- Fade-up animations for sequential content reveal

106
.design/brand.json Normal file
View File

@@ -0,0 +1,106 @@
{
"name": "Malenia",
"tagline": "Свобода. Практика. Простота.",
"description": "A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.",
"sourceUrl": "https://malenia.space/",
"logo": {
"primary": "https://malenia.space/favicon.ico",
"alternates": [],
"notes": "Logotype uses Cormorant Garamond with high letter-spacing (0.35em), uppercase, and a gold accent on the 'e'."
},
"colors": [
{
"role": "background",
"hex": "#faf7f2",
"oklch": "",
"name": "Cream",
"usage": "Main page background"
},
{
"role": "surface",
"hex": "#f5f0e8",
"oklch": "",
"name": "Warm White",
"usage": "Secondary surfaces"
},
{
"role": "foreground",
"hex": "#1a1610",
"oklch": "",
"name": "Ink",
"usage": "Primary text and primary buttons"
},
{
"role": "muted",
"hex": "#3d3428",
"oklch": "",
"name": "Ink Soft",
"usage": "Secondary text"
},
{
"role": "accent",
"hex": "#c9a84c",
"oklch": "",
"name": "Gold",
"usage": "Highlights, emblems, and key accents"
}
],
"typography": {
"display": {
"family": "Cormorant Garamond",
"fallbacks": [
"serif"
],
"weights": [],
"googleFontsUrl": "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap"
},
"body": {
"family": "Tenor Sans",
"fallbacks": [
"sans-serif"
],
"weights": [],
"googleFontsUrl": "https://fonts.googleapis.com/css2?family=Tenor+Sans&display=swap"
}
},
"voice": {
"adjectives": [
"Sophisticated",
"Quiet",
"Essential",
"Free"
],
"tone": "Restrained, archival, sophisticated, and minimal.",
"messagingPillars": [
"Network freedom",
"Practical simplicity",
"Aesthetic restraint"
],
"vocabulary": {
"use": [],
"avoid": []
}
},
"imagery": {
"style": "Geometric, symbolic, and minimalist with a focus on symmetry and gold-on-cream palettes.",
"subjects": [
"Diamonds",
"Rings",
"Symmetrical emblems"
],
"treatment": "Fine-line vector art, subtle noise textures, and centered compositions.",
"avoid": []
},
"layout": {
"radius": "8px",
"borderWeight": "1px",
"spacing": "8px baseline grid",
"postureRules": [
"Extreme whitespace and centered alignment",
"Strict horizontal rules for structural division",
"Custom cursor interactions (dot and ring)",
"Subtle grain/noise overlay for organic feel",
"Fade-up animations for sequential content reveal"
]
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

48
.design/fonts/fonts.css Normal file
View File

@@ -0,0 +1,48 @@
@font-face {
font-family: "Cormorant Garamond";
src: url("./cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("./cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("./cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("./cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("./cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("./tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}

View File

@@ -0,0 +1,65 @@
{
"format": "brand-fonts/1",
"files": [
{
"family": "Cormorant Garamond",
"weight": "300",
"style": "italic",
"file": "cormorant-garamond-300-italic.woff2",
"format": "woff2",
"sourceUrl": "https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtHmSqn7B6DxjY.woff2",
"bytes": 11556,
"unicodeRange": "U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB"
},
{
"family": "Cormorant Garamond",
"weight": "400",
"style": "italic",
"file": "cormorant-garamond-400-italic.woff2",
"format": "woff2",
"sourceUrl": "https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtHmSqn7B6DxjY.woff2",
"bytes": 11556,
"unicodeRange": "U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB"
},
{
"family": "Cormorant Garamond",
"weight": "300",
"style": "normal",
"file": "cormorant-garamond-300.woff2",
"format": "woff2",
"sourceUrl": "https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpntKky2F7i6C.woff2",
"bytes": 11264,
"unicodeRange": "U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB"
},
{
"family": "Cormorant Garamond",
"weight": "400",
"style": "normal",
"file": "cormorant-garamond-400.woff2",
"format": "woff2",
"sourceUrl": "https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpntKky2F7i6C.woff2",
"bytes": 11264,
"unicodeRange": "U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB"
},
{
"family": "Cormorant Garamond",
"weight": "600",
"style": "normal",
"file": "cormorant-garamond-600.woff2",
"format": "woff2",
"sourceUrl": "https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpntKky2F7i6C.woff2",
"bytes": 11264,
"unicodeRange": "U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB"
},
{
"family": "Tenor Sans",
"weight": "400",
"style": "normal",
"file": "tenor-sans-400.woff2",
"format": "woff2",
"sourceUrl": "https://fonts.gstatic.com/s/tenorsans/v21/bx6ANxqUneKx06UkIXISn3t4Ck-L2qM2VQ.woff2",
"bytes": 6832,
"unicodeRange": "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF"
}
]
}

Binary file not shown.

BIN
.design/logos/favicon-0.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,44 @@
# Brand material — https://malenia.space/
- site name: (unknown)
- title: Malenia — Свобода в сети
- description: (none)
## Measured colors (frequency-ranked from the site's actual CSS)
- `#c9a84c` ×2 — css-var:--gold selector::root; css-var:--border selector::root
- `#e8ca7a` ×1 — css-var:--gold-light selector::root
- `#8b6914` ×1 — css-var:--gold-dim selector::root
- `#f5f0e8` ×1 — css-var:--warm-white selector::root
- `#1a1610` ×1 — css-var:--ink selector::root
- `#3d3428` ×1 — css-var:--ink-soft selector::root
- `#7a6e5f` ×1 — css-var:--ink-muted selector::root
- `#faf7f2` ×1 (near-white/black) — css-var:--cream selector::root
## Measured fonts (frequency-ranked font-family declarations)
- Cormorant Garamond ×27
- Tenor Sans ×5
@font-face families: Cormorant Garamond, Tenor Sans
Google Fonts links: https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Tenor+Sans&display=swap
## Self-hosted webfonts (downloaded into ./fonts/)
- "Cormorant Garamond" 300 italic — fonts/cormorant-garamond-300-italic.woff2 (woff2, 11556 bytes)
- "Cormorant Garamond" 400 italic — fonts/cormorant-garamond-400-italic.woff2 (woff2, 11556 bytes)
- "Cormorant Garamond" 300 normal — fonts/cormorant-garamond-300.woff2 (woff2, 11264 bytes)
- "Cormorant Garamond" 400 normal — fonts/cormorant-garamond-400.woff2 (woff2, 11264 bytes)
- "Cormorant Garamond" 600 normal — fonts/cormorant-garamond-600.woff2 (woff2, 11264 bytes)
- "Tenor Sans" 400 normal — fonts/tenor-sans-400.woff2 (woff2, 6832 bytes)
These font FILES are already saved locally. In brand.json, keep each
typography `family` spelled EXACTLY as listed above so the self-hosted
@font-face declarations apply to every generated asset.
## Logo candidates (downloaded into ./logos/)
- logos/favicon-0.ico — favicon, 4286 bytes, from https://malenia.space/favicon.ico
## Copy harvested from the site (for voice & tone analysis)

View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Malenia — Свобода в сети</title>
<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=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Tenor+Sans&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<!-- Cursor -->
<div id="cursor-dot"></div>
<div id="cursor-ring"></div>
<div class="page">
<!-- Header -->
<header>
<a class="logo" href="#">Mal<span>e</span>nia</a>
<div class="header-rule"></div>
<span class="header-tag">Свобода. Практика. Простота.</span>
</header>
<!-- Hero -->
<main class="hero">
<!-- Emblem -->
<div class="emblem" aria-hidden="true">
<svg viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- outer ring -->
<circle cx="45" cy="45" r="42" stroke="#C9A84C" stroke-width=".8" stroke-dasharray="3 4"/>
<!-- inner ring -->
<circle cx="45" cy="45" r="32" stroke="#C9A84C" stroke-width=".5" opacity=".5"/>
<!-- diamond -->
<path d="M45 18 L72 45 L45 72 L18 45 Z" stroke="#C9A84C" stroke-width=".9" fill="none"/>
<!-- center dot -->
<circle cx="45" cy="45" r="3.5" fill="#C9A84C"/>
<!-- cardinal marks -->
<line x1="45" y1="3" x2="45" y2="12" stroke="#C9A84C" stroke-width=".8"/>
<line x1="45" y1="78" x2="45" y2="87" stroke="#C9A84C" stroke-width=".8"/>
<line x1="3" y1="45" x2="12" y2="45" stroke="#C9A84C" stroke-width=".8"/>
<line x1="78" y1="45" x2="87" y2="45" stroke="#C9A84C" stroke-width=".8"/>
</svg>
</div>
<!-- Quote -->
<div class="quote-block">
<p class="quote-text" id="quote-text"></p>
<p class="quote-author">malenia.space</p>
</div>
<!-- Divider -->
<div class="divider" aria-hidden="true">
<div class="divider-diamond"></div>
</div>
<!-- CTA Buttons -->
<div class="cta-group">
<a class="btn btn-primary" href="https://t.me/maleniavpn" target="_blank" rel="noopener">
<!-- Telegram icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.265 2.428a1.523 1.523 0 0 0-1.547-.232L1.91 9.9a1.523 1.523 0 0 0 .089 2.844l4.498 1.498 1.815 5.784a.761.761 0 0 0 1.295.3l2.617-2.818 4.898 3.674a1.523 1.523 0 0 0 2.389-.98l3.047-16.762a1.523 1.523 0 0 0-.293-1.012zM9.94 13.88l-.98 3.267-.98-3.143L18.5 6.04z"/>
</svg>
<span>Канал</span>
</a>
<a class="btn btn-secondary" href="https://t.me/maleniasupportbot" target="_blank" rel="noopener">
<!-- Bot icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-1 1.732V7h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h3V5.732A2 2 0 0 1 10 4a2 2 0 0 1 2-2zm-3 9a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm6 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/>
</svg>
<span>Поддержка</span>
</a>
</div>
</main>
<!-- Footer -->
<footer>
<span class="footer-copy">© 2026 Malenia</span>
<div class="footer-dot"></div>
<span class="footer-copy">malenia.space</span>
</footer>
</div>
<script type="text/od-defused-script" src="script.js"></script>
</body>
</html>

531
.design/prefetch/styles.css Normal file
View File

@@ -0,0 +1,531 @@
:root {
--gold: #C9A84C;
--gold-light: #E8CA7A;
--gold-dim: #8B6914;
--cream: #FAF7F2;
--warm-white: #F5F0E8;
--ink: #1A1610;
--ink-soft: #3D3428;
--ink-muted: #7A6E5F;
--border: rgba(201,168,76,0.25);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { cursor: none; }
body {
font-family: 'Tenor Sans', sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
overflow-x: hidden;
cursor: none;
}
/* ── Custom cursor ─────────────────────────────────── */
#cursor-dot,
#cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity .3s;
}
#cursor-dot {
width: 6px; height: 6px;
background: var(--gold);
border-radius: 50%;
}
#cursor-ring {
width: 32px; height: 32px;
border: 1px solid var(--gold);
border-radius: 50%;
transition: width .25s cubic-bezier(.4,0,.2,1),
height .25s cubic-bezier(.4,0,.2,1),
opacity .3s;
}
body.hovering #cursor-ring {
width: 48px; height: 48px;
border-color: var(--gold-light);
}
/* ── Noise overlay ─────────────────────────────────── */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: .035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 180px;
}
/* ── Layout ────────────────────────────────────────── */
.page {
position: relative;
z-index: 1;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
/* ── Header ────────────────────────────────────────── */
header {
padding: 2.5rem 4rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.logo {
font-family: 'Cormorant Garamond', serif;
font-weight: 300;
font-size: 1.5rem;
letter-spacing: .35em;
text-transform: uppercase;
color: var(--ink);
text-decoration: none;
}
.logo span { color: var(--gold); }
.header-rule {
flex: 1;
height: 1px;
background: var(--border);
margin: 0 2rem;
}
.header-tag {
font-size: .65rem;
letter-spacing: .25em;
text-transform: uppercase;
color: var(--ink-muted);
}
/* ── Hero ──────────────────────────────────────────── */
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6rem 2rem;
gap: 3.5rem;
}
.emblem {
position: relative;
width: 90px;
height: 90px;
opacity: 0;
animation: fadeUp .9s .1s forwards;
}
.emblem svg { width: 100%; height: 100%; }
.quote-block {
max-width: 640px;
opacity: 0;
animation: fadeUp .9s .25s forwards;
}
.quote-text {
font-family: 'Cormorant Garamond', serif;
font-size: clamp(2rem, 5vw, 3.4rem);
font-weight: 300;
line-height: 1.25;
color: var(--ink);
letter-spacing: .01em;
}
.quote-text em {
font-style: italic;
color: var(--gold-dim);
}
.quote-author {
margin-top: 1.25rem;
font-size: .7rem;
letter-spacing: .3em;
text-transform: uppercase;
color: var(--ink-muted);
}
/* ── Divider ───────────────────────────────────────── */
.divider {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 480px;
opacity: 0;
animation: fadeUp .9s .4s forwards;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.divider-diamond {
width: 6px; height: 6px;
background: var(--gold);
transform: rotate(45deg);
flex-shrink: 0;
}
/* ── Buttons ───────────────────────────────────────── */
.cta-group {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
justify-content: center;
opacity: 0;
animation: fadeUp .9s .55s forwards;
}
.btn {
position: relative;
display: inline-flex;
align-items: center;
gap: .75rem;
padding: .85rem 2rem;
font-family: 'Tenor Sans', sans-serif;
font-size: .78rem;
letter-spacing: .18em;
text-transform: uppercase;
text-decoration: none;
border-radius: 0;
cursor: none;
transition: color .25s, background .25s, border-color .25s;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
inset: 0;
background: var(--gold);
transform: scaleX(0);
transform-origin: left;
transition: transform .35s cubic-bezier(.4,0,.2,1);
z-index: 0;
}
.btn:hover::after { transform: scaleX(1); }
.btn > * { position: relative; z-index: 1; }
.btn-primary {
background: var(--ink);
color: var(--cream);
border: 1px solid var(--ink);
}
.btn-primary:hover { color: var(--ink); border-color: var(--gold); }
.btn-secondary {
background: transparent;
color: var(--ink);
border: 1px solid var(--gold);
}
.btn-secondary::after { background: var(--ink); }
.btn-secondary:hover { color: var(--cream); }
.btn-icon {
width: 18px; height: 18px;
fill: currentColor;
flex-shrink: 0;
}
/* ── Footer ────────────────────────────────────────── */
footer {
padding: 2rem 4rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-copy {
font-size: .65rem;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--ink-muted);
}
.footer-dot {
width: 4px; height: 4px;
background: var(--gold);
border-radius: 50%;
}
/* ── Animations ────────────────────────────────────── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Responsive ────────────────────────────────────── */
@media (max-width: 640px) {
header { padding: 1.75rem 1.5rem; }
.header-rule { display: none; }
.header-tag { display: none; }
.hero { padding: 4rem 1.5rem; gap: 2.5rem; }
footer { padding: 1.5rem; flex-direction: column; align-items: flex-start; }
.btn { padding: .8rem 1.5rem; font-size: .72rem; }
.cta-group { flex-direction: column; align-items: stretch; }
.btn { justify-content: center; }
}
/* cyrillic-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtFmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtMmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtHmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtGmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtImSqn7B6D.woff2) format('woff2');
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;
}
/* cyrillic-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtFmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtMmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtHmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtGmSqn7B6DxjY.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cormorant Garamond';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtImSqn7B6D.woff2) format('woff2');
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;
}
/* cyrillic-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpHtKky2F7i6C.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYrXtKky2F7i6C.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpntKky2F7i6C.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYp3tKky2F7i6C.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYqXtKky2F7g.woff2) format('woff2');
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;
}
/* cyrillic-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpHtKky2F7i6C.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYrXtKky2F7i6C.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpntKky2F7i6C.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYp3tKky2F7i6C.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYqXtKky2F7g.woff2) format('woff2');
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;
}
/* cyrillic-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpHtKky2F7i6C.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYrXtKky2F7i6C.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYpntKky2F7i6C.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYp3tKky2F7i6C.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cormorantgaramond/v21/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYqXtKky2F7g.woff2) format('woff2');
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;
}
/* cyrillic */
@font-face {
font-family: 'Tenor Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tenorsans/v21/bx6ANxqUneKx06UkIXISn3F4Ck-L2qM2VQ.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
font-family: 'Tenor Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tenorsans/v21/bx6ANxqUneKx06UkIXISn3t4Ck-L2qM2VQ.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Tenor Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tenorsans/v21/bx6ANxqUneKx06UkIXISn3V4Ck-L2qM.woff2) format('woff2');
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;
}

View File

@@ -0,0 +1,53 @@
# Malenia — brand system
Generated by the branding-agent engine from a single **seed** (20 fields).
Everything below is *derived* — no token here was hand-authored.
- **slug:** `malenia`
- **source:** https://malenia.space/
- **primary seed color:** `#c9a84c`
- **derived tokens per theme:** 218
- **themes:** default · dark · compact (same seed, different algorithm)
## Files
| Path | What it is |
| --- | --- |
| `seed.json` | Generated snapshot of the effective seed; persistent overrides live in `brand.json.seed`. |
| `tokens.default.json` | Full derived DesignTokens (light). |
| `tokens.dark.json` | Same seed, dark algorithm. |
| `tokens.compact.json` | Same seed, compact (dense) algorithm. |
| `variables.css` | `:root{}` + `.dark{}` CSS custom properties (`--brand-*`). |
| `variables.dark.css` | Standalone `:root{}` for the dark theme. |
| `theme.json` | antd `ConfigProvider` theme (token + algorithm). |
| `kit.html` | Themed component showcase (light). |
| `kit.dark.html` | Themed component showcase (dark). |
| `artifacts/landing.html` | Full landing page (hero → features → pricing → FAQ → CTA). |
| `artifacts/deck.html` | 9-slide 16:9 pitch deck with keyboard navigation. |
| `artifacts/poster.html` | 3:4 print-style poster. |
| `artifacts/email.html` | A marketing email. |
| `artifacts/newsletter.html` | Multi-story digest email. |
| `artifacts/form.html` | Split-screen contact / signup page. |
| `index.html` | Gallery linking everything + live component preview. |
## How to re-theme
The whole system is regenerated from `brand.json` and optional
`brand.json.seed` overrides. Persist authored overrides there, then run
`od brand finalize <brand-id>` — every downstream token, component and artifact
follows. Do not edit `system/seed.json` directly; finalize replaces it.
- **Change the brand color:** edit the accent role or `seed.colorPrimary` in `brand.json`.
The 10-step palette, all interaction states, the primary background/border and
every component that references `var(--brand-color-primary)` update together.
- **Light → dark:** the dark theme is the same seed run through the `"dark"`
algorithm — load `variables.dark.css` (or add the `.dark` class).
- **Density:** the `"compact"` algorithm tightens spacing & control heights
while keeping colors identical.
## Add a new artifact type
`renderArtifact(kind, brand, tokens)` already accepts every `AssetKind`
(`landing | deck | poster | email | newsletter | form`). Add the kind to the
build's artifact list to ship it in the bundle; kinds without a bespoke layout
fall back to a sensible card+button page.

View File

@@ -0,0 +1,782 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Malenia — Pitch deck</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
:root { --display-family: 'Cormorant Garamond', serif, var(--brand-font-family); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--brand-font-family);
font-size: var(--brand-font-size);
line-height: var(--brand-line-height);
color: var(--brand-color-text);
background: var(--brand-color-bg-layout);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--brand-color-link); text-decoration: none; }
h1, h2, h3, h4 {
margin: 0;
font-family: var(--display-family);
line-height: var(--brand-line-height-heading);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text);
overflow-wrap: break-word;
}
p { margin: 0; overflow-wrap: break-word; }
img, svg { max-width: 100%; display: block; }
.wrap { max-width: 1120px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
.kicker {
display: block;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: var(--brand-font-size-sm);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-primary);
margin-bottom: var(--brand-size-sm);
}
.clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
html, body { height: 100%; overflow: hidden; }
.deck { height: 100vh; overflow-y: auto; scroll-snap-type: y mandatory; scroll-behavior: smooth; }
.slide { height: 100vh; scroll-snap-align: start; scroll-snap-stop: always; display: flex; align-items: center; justify-content: center; padding: 24px; }
.frame {
position: relative; aspect-ratio: 16 / 9;
width: min(1280px, 94vw, calc((100vh - 48px) * 16 / 9));
container-type: size;
background: var(--brand-color-bg-container);
border: var(--brand-line-width) solid var(--brand-color-border-secondary);
border-radius: var(--brand-border-radius-lg);
box-shadow: 0 24px 64px -24px var(--brand-color-fill-secondary), 0 2px 8px var(--brand-color-fill-quaternary);
overflow: hidden; display: flex; flex-direction: column;
}
/* solid-accent slides (cover + traction + ask): restrained 3-page accent budget */
.frame--accent { background: var(--brand-color-primary); border-color: transparent; }
.frame--accent .s-kicker { color: rgba(255,255,255,0.72); }
.frame--accent .s-kicker::before { background: rgba(255,255,255,0.85); }
.frame--accent .s-title, .frame--accent .s-mega, .frame--accent .s-num { color: #fff; }
.frame--accent .s-lede, .frame--accent .s-eyebrow { color: rgba(255,255,255,0.85); }
.frame--accent .f-head, .frame--accent .f-foot { color: rgba(255,255,255,0.62); }
.frame--accent .f-head .rule, .frame--accent .f-foot .rule { background: rgba(255,255,255,0.2); }
/* surface slides (alternate bg per §3 视觉一致) */
.frame--surface { background: var(--brand-color-bg-layout); }
.f-head { display: flex; justify-content: space-between; align-items: center; gap: 2cqi; padding: 3cqi 5cqi 0; color: var(--brand-color-text-quaternary); font-size: 1.35cqi; }
.f-head .wm { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 1.7cqi; letter-spacing: -0.01em; color: inherit; }
.f-head .rule { flex: 1; height: var(--brand-line-width); background: var(--brand-color-border-secondary); }
.f-head .ix { font-variant-numeric: tabular-nums; letter-spacing: 0.12em; }
.f-body { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 0 5cqi; min-height: 0; overflow: hidden; }
/* shrink-to-fit layer: the runtime scales this down about its centre when
a slide's content would otherwise exceed the fixed frame (no-clip).
flex:none keeps its natural (unshrunk) height so the runtime can
measure the true overflow instead of a flex-shrunk box. */
.f-fit { flex: none; width: 100%; transform-origin: center center; will-change: transform; }
.f-foot { display: flex; justify-content: space-between; align-items: center; gap: 2cqi; padding: 0 5cqi 3cqi; color: var(--brand-color-text-quaternary); font-size: 1.25cqi; }
.f-foot .rule { flex: 1; height: var(--brand-line-width); background: var(--brand-color-border-secondary); }
.s-kicker { display: inline-flex; align-items: center; gap: 1.1cqi; text-transform: uppercase; letter-spacing: 0.18em; font-size: 1.3cqi; color: var(--brand-color-primary); font-weight: var(--brand-font-weight-strong); margin-bottom: 2cqi; }
.s-kicker::before { content: ""; width: 2.6cqi; height: 0.32cqi; border-radius: 1cqi; background: var(--brand-color-primary); }
.s-eyebrow { font-size: 1.7cqi; color: var(--brand-color-text-tertiary); margin: 0 0 1.6cqi; max-width: 70cqi; }
.s-title { font-family: var(--display-family); font-size: 4.6cqi; line-height: 1.1; letter-spacing: -0.02em; margin: 0; color: var(--brand-color-text); max-width: 82cqi; }
.s-mega { font-family: var(--display-family); font-size: 8cqi; line-height: 1.02; letter-spacing: -0.025em; margin: 0; color: var(--brand-color-text); }
.s-lede { font-size: 1.95cqi; line-height: 1.5; color: var(--brand-color-text-secondary); max-width: 66cqi; margin: 2.4cqi 0 0; }
.s-accent { color: var(--brand-color-primary); }
.frame--accent .s-accent { color: #fff; }
.s-grid { display: grid; gap: 2cqi; margin-top: 4cqi; }
.s-card { position: relative; background: var(--brand-color-bg-container); border: var(--brand-line-width) solid var(--brand-color-border-secondary); border-radius: 1.4cqi; padding: 2.6cqi; min-width: 0; }
.frame--surface .s-card { background: var(--brand-color-bg-container); }
.s-card h3 { font-size: 2cqi; line-height: 1.22; margin: 0 0 1cqi; letter-spacing: -0.01em; }
.s-card p { font-size: 1.55cqi; line-height: 1.5; color: var(--brand-color-text-secondary); margin: 0; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
.s-card .tick { display: inline-flex; align-items: center; justify-content: center; width: 3.4cqi; height: 3.4cqi; border-radius: 1cqi; background: var(--brand-color-primary-bg); color: var(--brand-color-primary); margin-bottom: 1.6cqi; }
.s-card .tick svg { width: 1.9cqi; height: 1.9cqi; }
.s-num { font-family: var(--display-family); font-size: 3.4cqi; font-weight: var(--brand-font-weight-strong); line-height: 1; color: var(--brand-color-primary); margin-bottom: 1.4cqi; letter-spacing: -0.02em; }
/* metric tiles (market / traction) */
.s-metric { min-width: 0; }
.s-metric .v { font-family: var(--display-family); font-size: 5cqi; font-weight: var(--brand-font-weight-strong); line-height: 1; letter-spacing: -0.03em; color: var(--brand-color-text); }
.frame--accent .s-metric .v { color: #fff; }
.s-metric .l { margin-top: 1.2cqi; font-size: 1.5cqi; line-height: 1.35; color: var(--brand-color-text-tertiary); }
.frame--accent .s-metric .l { color: rgba(255,255,255,0.78); }
/* market funnel bars — value labels live OUTSIDE the proportional track in
their own auto column, so a long label can never wrap/clip the bar. */
.s-bars { display: flex; flex-direction: column; gap: 1.8cqi; margin-top: 3.4cqi; max-width: 82cqi; }
.s-bar { display: grid; grid-template-columns: 12cqi 1fr auto; align-items: center; gap: 2cqi; }
.s-bar .tag { font-size: 1.5cqi; font-weight: var(--brand-font-weight-strong); color: var(--brand-color-text-secondary); letter-spacing: 0.04em; }
.s-bar .lane { min-width: 0; }
.s-bar .track { height: 4.2cqi; border-radius: 0.8cqi; }
.s-bar .val { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 1.9cqi; color: var(--brand-color-text); white-space: nowrap; }
.frame--accent .s-bar .val { color: #fff; }
/* competition matrix */
.s-matrix { display: grid; gap: 1.6cqi; margin-top: 3.4cqi; }
.s-matrix .row { display: grid; align-items: center; gap: 1.6cqi; padding: 1.8cqi 2.4cqi; border-radius: 1.2cqi; }
.s-matrix .row.head { color: var(--brand-color-text-tertiary); font-size: 1.4cqi; letter-spacing: 0.08em; text-transform: uppercase; padding-top: 0; padding-bottom: 0; }
.s-matrix .row.them { background: var(--brand-color-bg-container); border: var(--brand-line-width) solid var(--brand-color-border-secondary); }
.s-matrix .row.us { background: var(--brand-color-primary-bg); border: 1.5px solid var(--brand-color-primary-border); }
.s-matrix .cell { font-size: 1.6cqi; min-width: 0; }
.s-matrix .who { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 1.85cqi; letter-spacing: -0.01em; }
.s-matrix .us .who { color: var(--brand-color-primary); }
.s-matrix .mk { display: inline-flex; align-items: center; justify-content: center; width: 3cqi; height: 3cqi; border-radius: 50%; }
.s-matrix .mk svg { width: 1.7cqi; height: 1.7cqi; }
.s-matrix .yes { background: var(--brand-color-primary); color: #fff; }
.s-matrix .no { background: var(--brand-color-fill-tertiary); color: var(--brand-color-text-quaternary); }
/* team initials */
.s-team { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2cqi; margin-top: 4cqi; }
.s-avatar { display: inline-flex; align-items: center; justify-content: center; width: 7cqi; height: 7cqi; border-radius: 50%; font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 2.6cqi; background: var(--brand-color-primary-bg); color: var(--brand-color-primary); margin-bottom: 1.6cqi; letter-spacing: 0.02em; }
/* quote / why-now */
.s-quote { font-family: var(--display-family); font-size: 3.4cqi; line-height: 1.32; letter-spacing: -0.015em; color: var(--brand-color-text); max-width: 80cqi; margin: 0; }
.s-byline { display: flex; align-items: center; gap: 1.8cqi; margin-top: 3.6cqi; }
.s-byline .av { display: inline-flex; align-items: center; justify-content: center; width: 4.6cqi; height: 4.6cqi; border-radius: 50%; background: var(--brand-color-primary-bg); color: var(--brand-color-primary); font-weight: var(--brand-font-weight-strong); font-size: 1.7cqi; }
.s-byline .n { font-weight: var(--brand-font-weight-strong); font-size: 1.75cqi; color: var(--brand-color-text); }
.s-byline .r { font-size: 1.45cqi; color: var(--brand-color-text-tertiary); }
/* ask checklist */
.s-ask { display: flex; flex-direction: column; gap: 1.8cqi; margin-top: 3.6cqi; max-width: 70cqi; }
.s-ask .item { display: flex; align-items: flex-start; gap: 1.8cqi; font-size: 2cqi; line-height: 1.4; color: #fff; }
.s-ask .item .b { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 3.4cqi; height: 3.4cqi; border-radius: 0.9cqi; background: rgba(255,255,255,0.16); margin-top: 0.2cqi; }
.s-ask .item .b svg { width: 1.9cqi; height: 1.9cqi; }
.s-ask .item .k { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); }
.s-cta { display: inline-flex; align-items: center; gap: 1.4cqi; margin-top: 4cqi; height: 6cqi; padding: 0 3.4cqi; border-radius: 1.2cqi; background: #fff; color: var(--brand-color-primary); font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 2.1cqi; letter-spacing: -0.01em; }
.s-cta svg { width: 2cqi; height: 2cqi; }
/* product wireframe motif (div/SVG, no screenshots) */
.s-screen { margin-top: 3.6cqi; border-radius: 1.6cqi; border: var(--brand-line-width) solid var(--brand-color-border-secondary); background: var(--brand-color-bg-container); overflow: hidden; box-shadow: 0 12px 32px -16px var(--brand-color-fill-secondary); }
.s-screen .bar { display: flex; align-items: center; gap: 1cqi; padding: 1.4cqi 1.8cqi; border-bottom: var(--brand-line-width) solid var(--brand-color-border-secondary); }
.s-screen .bar i { width: 1.2cqi; height: 1.2cqi; border-radius: 50%; background: var(--brand-color-fill-tertiary); }
.s-screen .bar .url { margin-left: 1cqi; height: 2.4cqi; flex: 1; max-width: 36cqi; border-radius: 1cqi; background: var(--brand-color-fill-quaternary); }
.s-screen .canvas { display: grid; grid-template-columns: 16cqi 1fr; min-height: 22cqi; }
.s-screen .nav { border-right: var(--brand-line-width) solid var(--brand-color-border-secondary); padding: 2cqi 1.6cqi; display: flex; flex-direction: column; gap: 1.4cqi; }
.s-screen .nav span { height: 1.5cqi; border-radius: 1cqi; background: var(--brand-color-fill-quaternary); }
.s-screen .nav span:first-child { background: var(--brand-color-primary); width: 70%; }
.s-screen .main { padding: 2.4cqi; display: flex; flex-direction: column; gap: 1.6cqi; }
.s-screen .main .h { height: 3cqi; width: 44%; border-radius: 0.8cqi; background: var(--brand-color-fill-tertiary); }
.s-screen .tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.6cqi; }
.s-screen .tiles i { height: 9cqi; border-radius: 1.2cqi; background: var(--brand-color-fill-quaternary); border: var(--brand-line-width) solid var(--brand-color-border-secondary); }
.s-screen .tiles i:first-child { background: var(--brand-color-primary-bg); border-color: var(--brand-color-primary-border); }
/* solution: oversized signature line */
.s-pillars { display: flex; flex-wrap: wrap; gap: 1.2cqi; margin-top: 3.6cqi; }
.s-pill { display: inline-flex; align-items: center; gap: 1cqi; padding: 1.2cqi 2cqi; border-radius: 6cqi; border: var(--brand-line-width) solid var(--brand-color-border); font-size: 1.6cqi; font-weight: var(--brand-font-weight-strong); color: var(--brand-color-text-secondary); }
.s-pill .dot { width: 1cqi; height: 1cqi; border-radius: 50%; background: var(--brand-color-primary); }
/* pager UI */
.deck-ui { position: fixed; right: 24px; bottom: 20px; z-index: 30; display: flex; align-items: center; gap: 10px; }
.deck-ui button {
width: 38px; height: 38px; border-radius: 50%; cursor: pointer;
border: var(--brand-line-width) solid var(--brand-color-border);
background: var(--brand-color-bg-container); color: var(--brand-color-text-secondary);
font-size: 15px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
transition: border-color .15s ease, color .15s ease;
}
.deck-ui button:hover { border-color: var(--brand-color-primary); color: var(--brand-color-primary); }
.deck-counter { font-size: 12px; color: var(--brand-color-text-tertiary); font-variant-numeric: tabular-nums; min-width: 48px; text-align: center; letter-spacing: 0.04em; }
.deck-dots { position: fixed; left: 50%; transform: translateX(-50%); bottom: 26px; z-index: 30; display: flex; gap: 8px; }
.deck-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand-color-fill); cursor: pointer; transition: all .2s ease; }
.deck-dot.on { background: var(--brand-color-primary); transform: scale(1.3); }
</style>
</head>
<body>
<main class="deck">
<section class="slide">
<div class="frame frame--accent">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">SOPHISTICATED · QUIET · ESSENTIAL</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Sophisticated · Quiet · Essential</span>
<h1 class="s-mega">Malenia</h1>
<p class="s-lede" style="font-size:2.6cqi;color:rgba(255,255,255,0.92);">Свобода. Практика. Простота.</p>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">01 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame frame--surface">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">THE PROBLEM</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">The problem</span>
<h2 class="s-title">Getting this category right is still painfully hard.</h2>
<p class="s-lede">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
<div class="s-grid" style="grid-template-columns:repeat(3,1fr);">
<div class="s-card"><span class="tick"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span><h3>Tools don't talk</h3><p>Work scatters across apps that never share a source of truth, so context is lost on every handoff.</p></div>
<div class="s-card"><span class="tick"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span><h3>Every loop costs a day</h3><p>Iteration stalls in coordination — approvals, exports, re-dos — long before anything ships.</p></div>
<div class="s-card"><span class="tick"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span><h3>Quality is a coin flip</h3><p>Output depends on whoever happened to do the work, not on a standard you can rely on.</p></div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">02 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">THE SOLUTION</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">The solution</span>
<h2 class="s-title"><span class="s-accent">Malenia</span> is how teams get свобода. Практика. Простота</h2>
<div class="s-pillars">
<span class="s-pill"><span class="dot"></span>Network freedom</span>
<span class="s-pill"><span class="dot"></span>Practical simplicity</span>
<span class="s-pill"><span class="dot"></span>Aesthetic restraint</span>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">03 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">WHY NOW</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Why now</span>
<p class="s-quote">The shift toward <span class="s-accent">sophisticated</span>, accountable this category is happening now — and the teams that move first set the standard.</p>
<div class="s-byline">
<span class="av">M</span>
<div><div class="n">Malenia</div><div class="r">malenia.space</div></div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">04 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame frame--surface">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">MARKET</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Market</span>
<h2 class="s-title">The opportunity is large and still wide open.</h2>
<div class="s-bars">
<div class="s-bar"><span class="tag">TAM</span>
<div class="lane"><div class="track" style="width:100%;background:var(--brand-color-primary);"></div></div>
<span class="val">$48B total market</span></div>
<div class="s-bar"><span class="tag">SAM</span>
<div class="lane"><div class="track" style="width:64%;background:var(--brand-color-primary-hover);"></div></div>
<span class="val">$12B reachable today</span></div>
<div class="s-bar"><span class="tag">SOM</span>
<div class="lane"><div class="track" style="width:30%;background:var(--brand-color-primary-active);"></div></div>
<span class="val">$1.4B near-term wedge</span></div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">05 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">PRODUCT</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Product</span>
<h2 class="s-title">Malenia is real, shipping, and good to use today.</h2>
<div class="s-screen">
<div class="bar"><i></i><i></i><i></i><span class="url"></span></div>
<div class="canvas">
<div class="nav"><span></span><span></span><span></span><span></span><span></span></div>
<div class="main"><div class="h"></div><div class="tiles"><i></i><i></i><i></i></div></div>
</div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">06 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame frame--surface">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">BUSINESS MODEL</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Business model</span>
<h2 class="s-title">We make money the moment we create value.</h2>
<div class="s-grid" style="grid-template-columns:repeat(3,1fr);">
<div class="s-card">
<h3>Starter</h3>
<div style="font-family:var(--display-family);font-size:3.4cqi;font-weight:var(--brand-font-weight-strong);letter-spacing:-0.02em;color:var(--brand-color-text);margin:0.6cqi 0 1.4cqi;">$0<span style="font-size:1.3cqi;color:var(--brand-color-text-tertiary);font-weight:400;"> / mo</span></div>
<p style="-webkit-line-clamp:3;">Up to 3 projects · Network freedom · Community support</p>
</div>
<div class="s-card" style="border-color:var(--brand-color-primary);border-width:1.5px;">
<h3>Pro</h3>
<div style="font-family:var(--display-family);font-size:3.4cqi;font-weight:var(--brand-font-weight-strong);letter-spacing:-0.02em;color:var(--brand-color-text);margin:0.6cqi 0 1.4cqi;">$29<span style="font-size:1.3cqi;color:var(--brand-color-text-tertiary);font-weight:400;"> / mo</span></div>
<p style="-webkit-line-clamp:3;">Unlimited projects · Practical simplicity · Aesthetic restraint</p>
</div>
<div class="s-card">
<h3>Enterprise</h3>
<div style="font-family:var(--display-family);font-size:3.4cqi;font-weight:var(--brand-font-weight-strong);letter-spacing:-0.02em;color:var(--brand-color-text);margin:0.6cqi 0 1.4cqi;">Custom<span style="font-size:1.3cqi;color:var(--brand-color-text-tertiary);font-weight:400;"> </span></div>
<p style="-webkit-line-clamp:3;">SSO &amp; audit logs · Dedicated success manager · Custom contracts &amp; SLA</p>
</div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">07 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">COMPETITION &amp; MOAT</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Competition &amp; moat</span>
<h2 class="s-title">Only Malenia is built sophisticated from the ground up.</h2>
<div class="s-matrix" style="grid-template-columns:1fr;">
<div class="row head" style="grid-template-columns:18cqi repeat(3,1fr);">
<span></span><span class="cell">Built for teams</span><span class="cell">Fast to value</span><span class="cell">On-brand by default</span>
</div>
<div class="row them" style="grid-template-columns:18cqi repeat(3,1fr);">
<span class="cell who" style="color:var(--brand-color-text);">Fieldnote</span>
<span class="cell"><span class="mk no"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span></span><span class="cell"><span class="mk no"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span></span><span class="cell"><span class="mk yes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span></span>
</div>
<div class="row them" style="grid-template-columns:18cqi repeat(3,1fr);">
<span class="cell who" style="color:var(--brand-color-text);">Arcadia</span>
<span class="cell"><span class="mk yes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span></span><span class="cell"><span class="mk no"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span></span><span class="cell"><span class="mk no"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg></span></span>
</div>
<div class="row us" style="grid-template-columns:18cqi repeat(3,1fr);">
<span class="cell who">Malenia</span>
<span class="cell"><span class="mk yes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span></span><span class="cell"><span class="mk yes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span></span><span class="cell"><span class="mk yes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span></span>
</div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">08 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame frame--accent">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">TRACTION</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Traction</span>
<h2 class="s-title" style="font-size:5cqi;">People already want this — and the line is going up.</h2>
<div class="s-grid" style="grid-template-columns:repeat(4,1fr);margin-top:5cqi;">
<div class="s-metric"><div class="v">10k+</div><div class="l">Teams onboard</div></div>
<div class="s-metric"><div class="v">99.9%</div><div class="l">Uptime SLA</div></div>
<div class="s-metric"><div class="v">4.9/5</div><div class="l">Average rating</div></div>
<div class="s-metric"><div class="v">120+</div><div class="l">Integrations</div></div>
</div>
<p class="s-lede" style="color:rgba(255,255,255,0.78);font-size:1.5cqi;max-width:64cqi;margin-top:3.4cqi;">Illustrative figures — replace with your live numbers before pitching.</p>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">09 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame frame--surface">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">TEAM</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">Team</span>
<h2 class="s-title">The people behind Malenia have done this before.</h2>
<div class="s-team">
<div class="s-card" style="background:transparent;border:none;padding:0;">
<span class="s-avatar">MC</span>
<h3 style="font-size:1.9cqi;margin:0 0 0.4cqi;">Maya Chen</h3>
<p style="-webkit-line-clamp:2;">Head of Design, Northwind</p>
</div>
<div class="s-card" style="background:transparent;border:none;padding:0;">
<span class="s-avatar">JW</span>
<h3 style="font-size:1.9cqi;margin:0 0 0.4cqi;">Jonas Weber</h3>
<p style="-webkit-line-clamp:2;">Engineering Lead, Lumen Labs</p>
</div>
<div class="s-card" style="background:transparent;border:none;padding:0;">
<span class="s-avatar">PN</span>
<h3 style="font-size:1.9cqi;margin:0 0 0.4cqi;">Priya Natarajan</h3>
<p style="-webkit-line-clamp:2;">Founder, Fieldnote</p>
</div>
</div>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">10 / 11</span></div>
</div>
</section>
<section class="slide">
<div class="frame frame--accent">
<div class="f-head"><span class="wm">Malenia</span><span class="rule"></span><span class="ix">THE ASK</span></div>
<div class="f-body">
<div class="f-fit">
<span class="s-kicker">The ask</span>
<h2 class="s-mega" style="font-size:6.4cqi;">Let's build the future of this category together.</h2>
<div class="s-ask">
<div class="item"><span class="b"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span><span><span class="k">Partner with us</span> — back the team building the sophisticated standard for this market.</span></div>
<div class="item"><span class="b"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span><span><span class="k">Fuel the next phase</span> — scale the product, the go-to-market motion, and the team.</span></div>
<div class="item"><span class="b"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span><span><span class="k">Hit the milestone</span> — reach the next tier of customers and revenue within 12 months.</span></div>
</div>
<a class="s-cta">Talk to us &middot; malenia.space <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
</div>
</div>
<div class="f-foot"><span>malenia.space</span><span class="rule"></span><span class="ix">11 / 11</span></div>
</div>
</section>
</main>
<div class="deck-dots"><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span><span class="deck-dot"></span></div>
<div class="deck-ui">
<button class="deck-prev" aria-label="Previous slide">&larr;</button>
<span class="deck-counter">1 / 11</span>
<button class="deck-next" aria-label="Next slide">&rarr;</button>
</div>
<script>
(function () {
/* od-deck-fit: shrink-to-fit guarantee — keep every slide's content inside
the fixed 16:9 frame so brand copy of any length never clips. */
function fitFrame(body) {
var fit = body.querySelector('.f-fit');
if (!fit) return;
fit.style.transform = 'none';
var availH = body.clientHeight;
var availW = fit.clientWidth;
if (availH <= 0 || availW <= 0) return;
var naturalH = fit.offsetHeight;
var naturalW = fit.scrollWidth;
var scale = Math.min(1, availH / naturalH, availW / naturalW);
if (scale < 1) {
// small safety margin so descenders never touch the clip edge
fit.style.transform = 'scale(' + (scale * 0.98) + ')';
}
}
function fitAll() {
[].forEach.call(document.querySelectorAll('.f-body'), fitFrame);
}
// Fit synchronously rather than via requestAnimationFrame: rAF callbacks are
// throttled while the deck is offscreen/occluded, which would leave a slide
// unscaled (and clipped) until first paint. A trailing settle pass catches
// late reflow (web-font swap, image load).
var fitTimer;
function scheduleFit() {
fitAll();
if (fitTimer) clearTimeout(fitTimer);
fitTimer = setTimeout(fitAll, 150);
}
window.addEventListener('resize', scheduleFit);
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(fitAll).catch(function () {});
}
window.addEventListener('load', fitAll);
scheduleFit();
var deck = document.querySelector('.deck');
var slides = [].slice.call(document.querySelectorAll('.slide'));
var dots = [].slice.call(document.querySelectorAll('.deck-dot'));
var counter = document.querySelector('.deck-counter');
var idx = 0;
function render() {
if (counter) counter.textContent = (idx + 1) + ' / ' + slides.length;
dots.forEach(function (d, n) { d.classList.toggle('on', n === idx); });
}
function go(i) {
idx = Math.max(0, Math.min(slides.length - 1, i));
slides[idx].scrollIntoView({ behavior: 'smooth' });
render();
}
deck.addEventListener('scroll', function () {
requestAnimationFrame(function () {
var i = Math.round(deck.scrollTop / deck.clientHeight);
if (i !== idx && i >= 0 && i < slides.length) { idx = i; render(); }
});
});
window.addEventListener('keydown', function (e) {
if (['ArrowDown', 'ArrowRight', 'PageDown', ' '].indexOf(e.key) > -1) { e.preventDefault(); go(idx + 1); }
else if (['ArrowUp', 'ArrowLeft', 'PageUp'].indexOf(e.key) > -1) { e.preventDefault(); go(idx - 1); }
else if (e.key === 'Home') { e.preventDefault(); go(0); }
else if (e.key === 'End') { e.preventDefault(); go(slides.length - 1); }
});
dots.forEach(function (d, n) { d.addEventListener('click', function () { go(n); }); });
document.querySelector('.deck-prev').addEventListener('click', function () { go(idx - 1); });
document.querySelector('.deck-next').addEventListener('click', function () { go(idx + 1); });
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,439 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Свобода. Практика. Простота — from Malenia</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
:root { --display-family: 'Cormorant Garamond', serif, var(--brand-font-family); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--brand-font-family);
font-size: var(--brand-font-size);
line-height: var(--brand-line-height);
color: var(--brand-color-text);
background: var(--brand-color-bg-layout);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--brand-color-link); text-decoration: none; }
h1, h2, h3, h4 {
margin: 0;
font-family: var(--display-family);
line-height: var(--brand-line-height-heading);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text);
overflow-wrap: break-word;
}
p { margin: 0; overflow-wrap: break-word; }
img, svg { max-width: 100%; display: block; }
.wrap { max-width: 1120px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
.kicker {
display: block;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: var(--brand-font-size-sm);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-primary);
margin-bottom: var(--brand-size-sm);
}
.clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
img{border:0;outline:none;text-decoration:none;}table{border-spacing:0;}
</style>
</head>
<body>
<!-- hidden preheader: earns the open, complements the subject, never repeats it -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:var(--brand-color-bg-layout);opacity:0;">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</div>
<!-- whitespace eater so the body copy does not leak into the inbox preview -->
<div style="display:none;max-height:0;overflow:hidden;">&#847;&#8204;&#160;&#847;&#8204;&#160;&#847;&#8204;&#160;&#847;&#8204;&#160;&#847;&#8204;&#160;&#847;&#8204;&#160;&#847;&#8204;&#160;</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:var(--brand-color-bg-layout);margin:0;padding:0;">
<tr>
<td align="center" style="padding:var(--brand-size-xl) var(--brand-size-md);">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;">
<!-- Header: logo only, generous whitespace, no nav -->
<tr>
<td style="padding:var(--brand-size-lg) var(--brand-size-xl);text-align:center;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);">
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);letter-spacing:-0.01em;color:var(--brand-color-text);white-space:nowrap;">Malenia</span>
</td>
</tr>
<!-- Hero: inverted pyramid — conclusion first, then why it matters, then the one CTA -->
<tr>
<td style="padding:var(--brand-size-xxl) var(--brand-size-xl) var(--brand-size-lg);text-align:center;background:var(--brand-color-primary-bg);">
<p style="margin:0 0 var(--brand-size-sm);font-family:var(--brand-font-family);text-transform:uppercase;letter-spacing:0.16em;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-primary);">Sophisticated</p>
<h1 style="margin:0 0 var(--brand-size-md);font-family:var(--display-family);font-size:var(--brand-font-size-heading-2);line-height:1.15;letter-spacing:-0.015em;color:var(--brand-color-text);">Свобода. Практика. Простота.</h1>
<p style="margin:0 auto var(--brand-size-xl);max-width:46ch;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);line-height:1.55;color:var(--brand-color-text-secondary);">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
<!-- the one primary CTA, above the fold -->
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="var(--brand-color-primary)" style="border-radius:var(--brand-border-radius-lg);">
<a href="#" target="_blank" style="display:inline-block;min-width:200px;padding:16px var(--brand-size-xl);font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:#ffffff;text-decoration:none;text-align:center;border-radius:var(--brand-border-radius-lg);border:2px solid var(--brand-color-primary);">Get started with Malenia</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Body: scannable, human, one idea per block -->
<tr>
<td style="padding:var(--brand-size-xl) var(--brand-size-xl) var(--brand-size-sm);">
<p style="margin:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:16px;line-height:1.6;color:var(--brand-color-text);">Hi there,</p>
<p style="margin:0 0 var(--brand-size-lg);font-family:var(--brand-font-family);font-size:16px;line-height:1.6;color:var(--brand-color-text-secondary);">Here is what Malenia gives your team from the first day — no migration project, no setup tax:</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
<tr>
<td valign="top" style="width:40px;padding:var(--brand-size-sm) var(--brand-size-sm) var(--brand-size-sm) 0;"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;" aria-hidden="true"><circle cx="10" cy="10" r="9" stroke="var(--brand-color-primary)" stroke-width="1.5" opacity="0.45"/><path d="M6 10.2l2.6 2.6L14.2 7.4" stroke="var(--brand-color-primary)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></td>
<td valign="top" style="padding:var(--brand-size-sm) 0;">
<p style="margin:0 0 4px;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1.3;">Network freedom</p>
<p style="margin:0;font-family:var(--brand-font-family);font-size:16px;color:var(--brand-color-text-secondary);line-height:1.55;">Network freedom is built into Malenia from day one — not bolted on later.</p>
</td>
</tr>
<tr>
<td valign="top" style="width:40px;padding:var(--brand-size-sm) var(--brand-size-sm) var(--brand-size-sm) 0;"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;" aria-hidden="true"><circle cx="10" cy="10" r="9" stroke="var(--brand-color-primary)" stroke-width="1.5" opacity="0.45"/><path d="M6 10.2l2.6 2.6L14.2 7.4" stroke="var(--brand-color-primary)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></td>
<td valign="top" style="padding:var(--brand-size-sm) 0;">
<p style="margin:0 0 4px;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1.3;">Practical simplicity</p>
<p style="margin:0;font-family:var(--brand-font-family);font-size:16px;color:var(--brand-color-text-secondary);line-height:1.55;">Practical simplicity is built into Malenia from day one — not bolted on later.</p>
</td>
</tr>
<tr>
<td valign="top" style="width:40px;padding:var(--brand-size-sm) var(--brand-size-sm) var(--brand-size-sm) 0;"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;" aria-hidden="true"><circle cx="10" cy="10" r="9" stroke="var(--brand-color-primary)" stroke-width="1.5" opacity="0.45"/><path d="M6 10.2l2.6 2.6L14.2 7.4" stroke="var(--brand-color-primary)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></td>
<td valign="top" style="padding:var(--brand-size-sm) 0;">
<p style="margin:0 0 4px;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1.3;">Aesthetic restraint</p>
<p style="margin:0;font-family:var(--brand-font-family);font-size:16px;color:var(--brand-color-text-secondary);line-height:1.55;">Aesthetic restraint is built into Malenia from day one — not bolted on later.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- One named proof point: a person beats a paragraph of adjectives -->
<tr>
<td style="padding:0 var(--brand-size-xl) var(--brand-size-lg);">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="padding:var(--brand-size-lg);background:var(--brand-color-fill-quaternary);border-left:3px solid var(--brand-color-primary);border-radius:var(--brand-border-radius);">
<p style="margin:0 0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);line-height:1.55;color:var(--brand-color-text);">&ldquo;Malenia is the first tool here that actually feels Sophisticated. It was part of our daily flow inside a week.&rdquo;</p>
<p style="margin:0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);"><strong style="color:var(--brand-color-text);font-weight:var(--brand-font-weight-strong);">Maya Chen</strong> &middot; Head of Design, Northwind</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- CTA repeated once near the end — same single action -->
<tr>
<td style="padding:var(--brand-size-sm) var(--brand-size-xl) var(--brand-size-xl);text-align:center;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="var(--brand-color-primary)" style="border-radius:var(--brand-border-radius-lg);">
<a href="#" target="_blank" style="display:inline-block;min-width:200px;padding:16px var(--brand-size-xl);font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:#ffffff;text-decoration:none;text-align:center;border-radius:var(--brand-border-radius-lg);border:2px solid var(--brand-color-primary);">Get started with Malenia</a>
</td>
</tr>
</table>
<p style="margin:var(--brand-size-md) 0 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:1.5;">Questions? Just reply to this email — a real person reads every message.</p>
</td>
</tr>
<!-- Footer: working unsubscribe + physical address placeholder + brand -->
<tr>
<td style="padding:var(--brand-size-xl) var(--brand-size-xl);background:var(--brand-color-fill-quaternary);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);text-align:center;">
<p style="margin:0 0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:1.5;">You are receiving this because you signed up to hear from Malenia.</p>
<p style="margin:0 0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:1.5;"><a href="#" target="_blank" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">Update preferences</a> &nbsp;&middot;&nbsp; <a href="#" target="_blank" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">Unsubscribe</a></p>
<p style="margin:0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-quaternary);line-height:1.5;">Malenia &middot; 100 Market Street, Suite 200 &middot; malenia.space</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,442 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Malenia — Get started</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
:root { --display-family: 'Cormorant Garamond', serif, var(--brand-font-family); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--brand-font-family);
font-size: var(--brand-font-size);
line-height: var(--brand-line-height);
color: var(--brand-color-text);
background: var(--brand-color-bg-layout);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--brand-color-link); text-decoration: none; }
h1, h2, h3, h4 {
margin: 0;
font-family: var(--display-family);
line-height: var(--brand-line-height-heading);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text);
overflow-wrap: break-word;
}
p { margin: 0; overflow-wrap: break-word; }
img, svg { max-width: 100%; display: block; }
.wrap { max-width: 1120px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
.kicker {
display: block;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: var(--brand-font-size-sm);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-primary);
margin-bottom: var(--brand-size-sm);
}
.clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
.form-split { display: grid; grid-template-columns: minmax(320px, 4.6fr) minmax(380px, 5.4fr); min-height: 100vh; }
@media (max-width: 900px) { .form-split { grid-template-columns: 1fr; } .form-brand { min-height: 340px; } }
.form-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--brand-size); }
@media (max-width: 540px) { .form-grid-2 { grid-template-columns: 1fr; } }
.fld input:focus, .fld textarea:focus, .fld select:focus { border-color: var(--brand-color-primary) !important; box-shadow: 0 0 0 3px var(--brand-color-primary-bg); }
.fld input:invalid:not(:placeholder-shown) { border-color: var(--brand-color-error); }
.fld input:invalid:not(:placeholder-shown) ~ .fld-err { display: block; }
.fld-err { display: none; margin: var(--brand-size-xxs) 0 0; font-size: var(--brand-font-size-sm); color: var(--brand-color-error); }
.form-card { transition: box-shadow var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out); }
</style>
</head>
<body>
<div class="form-split">
<div class="form-brand" style="background:var(--brand-color-primary);color:#fff;padding:clamp(32px,5vw,64px);display:flex;flex-direction:column;justify-content:space-between;gap:var(--brand-size-xl);position:relative;overflow:hidden;">
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" aria-hidden="true" style="position:absolute;top:-40px;right:-40px;opacity:0.18;color:#fff;"><defs><pattern id="fdot" width="22" height="22" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1.6" fill="currentColor"/></pattern></defs><rect width="220" height="220" fill="url(#fdot)"/></svg>
<div style="position:absolute;bottom:-140px;left:-100px;width:320px;height:320px;border-radius:50%;background:var(--brand-color-primary-active);opacity:0.45;"></div>
<div style="position:relative;display:flex;align-items:center;gap:var(--brand-size-sm);">
<span style="display:inline-flex;width:30px;height:30px;border-radius:var(--brand-border-radius);background:rgba(255,255,255,0.16);align-items:center;justify-content:center;">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M1.5 7l3.5 3.5L12.5 3" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);letter-spacing:-0.01em;color:#fff;white-space:nowrap;">Malenia</span>
</div>
<div style="position:relative;max-width:42ch;">
<h1 style="color:#fff;font-size:clamp(28px,3.4vw,42px);letter-spacing:-0.02em;line-height:1.08;margin-bottom:var(--brand-size-md);">Свобода. Практика. Простота.</h1>
<p class="clamp-3" style="color:rgba(255,255,255,0.82);font-size:var(--brand-font-size-lg);line-height:1.55;margin-bottom:var(--brand-size-lg);">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
<ul style="list-style:none;margin:0;padding:0;"><li style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-xs) 0;color:rgba(255,255,255,0.92);font-size:var(--brand-font-size-lg);line-height:1.45;">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true" style="flex:0 0 auto;margin-top:3px;"><path d="M3.5 9.5L7 13l7.5-8" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Network freedom</span>
</li><li style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-xs) 0;color:rgba(255,255,255,0.92);font-size:var(--brand-font-size-lg);line-height:1.45;">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true" style="flex:0 0 auto;margin-top:3px;"><path d="M3.5 9.5L7 13l7.5-8" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Practical simplicity</span>
</li><li style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-xs) 0;color:rgba(255,255,255,0.92);font-size:var(--brand-font-size-lg);line-height:1.45;">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true" style="flex:0 0 auto;margin-top:3px;"><path d="M3.5 9.5L7 13l7.5-8" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Aesthetic restraint</span>
</li></ul>
</div>
<div style="position:relative;display:flex;align-items:center;gap:var(--brand-size-sm);color:rgba(255,255,255,0.7);font-size:var(--brand-font-size-sm);">
<span style="display:inline-flex;width:8px;height:8px;border-radius:50%;background:var(--brand-color-success);box-shadow:0 0 0 4px rgba(255,255,255,0.12);"></span>
<span>Restrained, archival, sophisticated, and minimal.</span>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:center;padding:clamp(24px,4vw,64px);background:var(--brand-color-bg-layout);">
<div class="form-card" style="width:100%;max-width:520px;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:clamp(28px,4vw,44px);box-shadow:0 24px 60px var(--brand-color-fill-quaternary), 0 2px 8px var(--brand-color-fill-quaternary);">
<header style="margin-bottom:var(--brand-size-lg);">
<h2 style="font-size:var(--brand-font-size-heading-3);letter-spacing:-0.015em;margin-bottom:var(--brand-size-xs);">Tell us where to start</h2>
<p style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size);">A couple of details and Malenia takes it from there — no account needed yet.</p>
</header>
<form novalidate onsubmit="this.querySelector('.form-body').style.display='none';var s=this.querySelector('.form-success');s.hidden=false;s.style.display='flex';return false;">
<div class="form-body" style="display:flex;flex-direction:column;gap:var(--brand-size-lg);">
<div style="display:flex;flex-direction:column;gap:var(--brand-size-lg);">
<div class="form-grid-2">
<div class="fld">
<label style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);"><span>First name</span></label>
<input type="text" placeholder="Ada" autocomplete="given-name" style="box-sizing:border-box;width:100%;font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;height:var(--brand-control-height-lg);padding:0 var(--brand-size);">
</div>
<div class="fld">
<label style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);"><span>Last name</span></label>
<input type="text" placeholder="Lovelace" autocomplete="family-name" style="box-sizing:border-box;width:100%;font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;height:var(--brand-control-height-lg);padding:0 var(--brand-size);">
</div>
</div>
<div class="fld">
<label style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);"><span>Work email</span></label>
<input type="email" placeholder="you@company.com" inputmode="email" autocomplete="email" style="box-sizing:border-box;width:100%;font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;height:var(--brand-control-height-lg);padding:0 var(--brand-size);">
<p class="fld-err">That doesn't look like an email yet — try name@company.com.</p>
<p style="margin:var(--brand-size-xxs) 0 0;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:1.4;">We'll send your next steps here.</p>
</div>
<div class="fld">
<label style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);"><span>What can we help with?</span></label>
<select style="box-sizing:border-box;width:100%;font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;height:var(--brand-control-height-lg);padding:0 var(--brand-size);-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;padding-right:var(--brand-size-xxl);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right var(--brand-size) center;"><option value="" disabled selected>Choose one…</option><option>Network freedom</option><option>Practical simplicity</option><option>Aesthetic restraint</option></select>
<p style="margin:var(--brand-size-xxs) 0 0;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:1.4;">Pick the closest — you can change direction later.</p>
</div>
<div class="fld">
<label style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);"><span>Anything we should know?</span><span style="font-weight:400;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-quaternary);">Optional</span></label>
<textarea rows="4" placeholder="A sentence or two about what you're trying to do with Malenia…" style="box-sizing:border-box;width:100%;font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;display:block;min-height:var(--brand-control-height-lg);padding:var(--brand-size-sm) var(--brand-size);line-height:var(--brand-line-height);resize:vertical;"></textarea>
</div>
<label class="cbx" style="display:flex;align-items:flex-start;gap:var(--brand-size-sm);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);cursor:pointer;">
<input type="checkbox" style="accent-color:var(--brand-color-primary);width:18px;height:18px;margin:2px 0 0;flex:0 0 auto;cursor:pointer;">
<span>Keep me posted on what's new at Malenia. No noise, easy to turn off.</span>
</label>
</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-sm);margin-top:var(--brand-size);">
<div style="display:flex;flex-direction:column;">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Get started with Malenia</a>
</div>
<p style="display:flex;align-items:center;justify-content:center;gap:var(--brand-size-xs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);text-align:center;">
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" aria-hidden="true" style="flex:0 0 auto;"><path d="M2 6V4.5a4.5 4.5 0 019 0V6m-10 0h11v7H1.5z" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>No spam, ever. Unsubscribe in one click — your data stays yours.</span>
</p>
</div>
</div>
<div class="form-success" hidden style="display:none;flex-direction:column;align-items:center;text-align:center;gap:var(--brand-size-md);padding:var(--brand-size-xl) 0;">
<span style="display:inline-flex;width:56px;height:56px;border-radius:50%;background:var(--brand-color-success-bg);align-items:center;justify-content:center;">
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" aria-hidden="true"><path d="M6 13.5l4.5 4.5L20 7" stroke="var(--brand-color-success)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<h2 style="font-size:var(--brand-font-size-heading-4);">You're in.</h2>
<p style="color:var(--brand-color-text-secondary);max-width:34ch;">Check your inbox for a note from Malenia — your next step is waiting there. We usually reply within one business day.</p>
</div>
</form>
<div style="margin-top:var(--brand-size-lg);padding-top:var(--brand-size-lg);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-sm);color:var(--brand-color-text-quaternary);font-size:var(--brand-font-size-sm);">
<span style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);"><span style="flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:var(--brand-border-radius-sm);background:var(--brand-color-primary-bg);"><svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2.5 6h7M6 2.5L9.5 6 6 9.5" stroke="var(--brand-color-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span style="color:var(--brand-color-text-tertiary);">Свобода. Практика. Простота</span></span>
<span>malenia.space</span>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,822 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Malenia — Свобода. Практика. Простота.</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
:root { --display-family: 'Cormorant Garamond', serif, var(--brand-font-family); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--brand-font-family);
font-size: var(--brand-font-size);
line-height: var(--brand-line-height);
color: var(--brand-color-text);
background: var(--brand-color-bg-layout);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--brand-color-link); text-decoration: none; }
h1, h2, h3, h4 {
margin: 0;
font-family: var(--display-family);
line-height: var(--brand-line-height-heading);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text);
overflow-wrap: break-word;
}
p { margin: 0; overflow-wrap: break-word; }
img, svg { max-width: 100%; display: block; }
.wrap { max-width: 1120px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
.kicker {
display: block;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: var(--brand-font-size-sm);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-primary);
margin-bottom: var(--brand-size-sm);
}
.clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
.nav-links { display: flex; gap: 28px; align-items: center; color: var(--brand-color-text-secondary); }
.nav-links a { color: var(--brand-color-text-secondary); transition: color .15s ease; }
.nav-links a:hover { color: var(--brand-color-text); }
@media (max-width: 820px) { .nav-links { display: none; } }
.section { padding: clamp(64px, 9vw, 104px) 0; }
.section-head { max-width: 680px; margin: 0 auto 48px; text-align: center; }
.pillars { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(16px, 2vw, 24px); }
@media (max-width: 880px) { .pillars { grid-template-columns: 1fr; } }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; align-items: stretch; }
.problem-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(14px, 1.8vw, 20px); }
@media (max-width: 880px) { .problem-grid { grid-template-columns: 1fr; } }
.zig { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(32px, 5vw, 72px); align-items: center; }
.zig + .zig { margin-top: clamp(48px, 7vw, 96px); }
@media (max-width: 880px) { .zig { grid-template-columns: 1fr; gap: 28px; } .zig .zig-visual { order: -1; } }
.footer-cols { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 32px; }
@media (max-width: 760px) { .footer-cols { grid-template-columns: 1fr 1fr; } }
.mock-grid { display: grid; grid-template-columns: 200px minmax(0, 1fr); min-height: 300px; }
@media (max-width: 720px) { .mock-grid { grid-template-columns: minmax(0, 1fr); } .mock-side { display: none; } }
.hero-trust { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; }
</style>
</head>
<body>
<header style="position:sticky;top:0;z-index:20;background:color-mix(in srgb, var(--brand-color-bg-container) 86%, transparent);backdrop-filter:blur(12px);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);">
<div class="wrap" style="display:flex;align-items:center;justify-content:space-between;height:64px;gap:var(--brand-size-lg);">
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);letter-spacing:-0.01em;color:var(--brand-color-text);white-space:nowrap;">Malenia</span>
<nav class="nav-links">
<a href="#how">How it works</a>
<a href="#features">Features</a>
<a href="#pricing">Pricing</a>
<a href="#faq">FAQ</a>
</nav>
<div style="display:flex;gap:var(--brand-size-sm);align-items:center;">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-text);border-color:transparent">Sign in</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Get started free</a>
</div>
</div>
</header>
<!-- 2 · HERO — the 5-piece value prop -->
<main>
<section class="wrap" style="padding-top:clamp(56px, 9vw, 108px);text-align:center;">
<div style="max-width:860px;margin:0 auto;">
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Sophisticated · Quiet · Essential</span>
<h1 style="font-size:clamp(36px, 5.6vw, 64px);letter-spacing:-0.025em;line-height:1.06;margin:var(--brand-size-lg) auto var(--brand-size-md);max-width:18ch;">Свобода. Практика. Простота.</h1>
<p style="font-size:var(--brand-font-size-xl);color:var(--brand-color-text-secondary);max-width:62ch;margin:0 auto var(--brand-size-xl);line-height:1.5;">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
<div style="display:flex;gap:var(--brand-size-sm);justify-content:center;flex-wrap:wrap;">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Get started free</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border)">See how it works</a>
</div>
<p style="margin-top:var(--brand-size-md);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">Free plan · No credit card · 2-minute setup</p>
</div>
<div style="max-width:980px;margin:clamp(40px, 6vw, 68px) auto 0;">
<div style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:14px;overflow:hidden;background:var(--brand-color-bg-container);box-shadow:0 32px 80px -24px var(--brand-color-fill-secondary);">
<div style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-fill-quaternary);">
<span style="width:10px;height:10px;border-radius:50%;background:var(--brand-color-error);opacity:.55;"></span>
<span style="width:10px;height:10px;border-radius:50%;background:var(--brand-color-warning);opacity:.55;"></span>
<span style="width:10px;height:10px;border-radius:50%;background:var(--brand-color-success);opacity:.55;"></span>
<span style="flex:1;max-width:380px;margin-left:12px;height:22px;border-radius:11px;background:var(--brand-color-fill-tertiary);display:flex;align-items:center;padding:0 12px;font-size:11px;color:var(--brand-color-text-quaternary);overflow:hidden;white-space:nowrap;">malenia.space</span>
</div>
<div class="mock-grid">
<div class="mock-side" style="border-right:var(--brand-line-width) solid var(--brand-color-border-secondary);padding:16px 12px;display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--brand-border-radius-sm);background:var(--brand-color-primary-bg);">
<div style="width:14px;height:14px;border-radius:4px;background:var(--brand-color-primary);"></div>
<div style="width:64%;height:8px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div>
</div><div style="display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--brand-border-radius-sm);">
<div style="width:14px;height:14px;border-radius:4px;background:var(--brand-color-fill);"></div>
<div style="width:52%;height:8px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div>
</div><div style="display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--brand-border-radius-sm);">
<div style="width:14px;height:14px;border-radius:4px;background:var(--brand-color-fill);"></div>
<div style="width:52%;height:8px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div>
</div><div style="display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--brand-border-radius-sm);">
<div style="width:14px;height:14px;border-radius:4px;background:var(--brand-color-fill);"></div>
<div style="width:52%;height:8px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div>
</div>
</div>
<div style="padding:20px;display:flex;flex-direction:column;gap:16px;min-width:0;">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;">
<div style="width:180px;height:14px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div>
<div style="width:96px;height:28px;border-radius:var(--brand-border-radius);background:var(--brand-color-primary);"></div>
</div>
<div style="display:grid;grid-template-columns:1.4fr 1fr;gap:16px;flex:1;">
<div style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:16px;display:flex;align-items:flex-end;gap:8px;min-height:160px;"><div style="flex:1;height:42%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-4);"></div><div style="flex:1;height:68%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-5);"></div><div style="flex:1;height:54%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-6);"></div><div style="flex:1;height:82%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-7);"></div><div style="flex:1;height:64%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-4);"></div><div style="flex:1;height:96%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-5);"></div><div style="flex:1;height:72%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-6);"></div><div style="flex:1;height:58%;border-radius:4px 4px 0 0;background:var(--brand-primary-palette-7);"></div></div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:14px;display:flex;flex-direction:column;gap:8px;"><div style="width:40%;height:8px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div><div style="width:70%;height:14px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div></div>
<div style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:14px;display:flex;flex-direction:column;gap:8px;"><div style="width:46%;height:8px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div><div style="width:60%;height:14px;border-radius:5px;background:var(--brand-color-fill-secondary);"></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 3 · SOCIAL PROOF — hugs the hero -->
<section class="wrap" style="padding:clamp(40px,6vw,64px) 0 8px;">
<div class="hero-trust" style="margin-bottom:var(--brand-size-lg);">
<span style="color:var(--brand-color-warning);letter-spacing:2px;">★★★★★</span>
<span style="font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary);">Rated 4.9/5 by 12,000+ teams</span>
</div>
<p style="text-align:center;text-transform:uppercase;letter-spacing:0.16em;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-quaternary);margin-bottom:var(--brand-size-lg);">Trusted by teams at</p>
<div style="display:flex;justify-content:center;align-items:center;gap:clamp(24px,5vw,56px);flex-wrap:wrap;opacity:0.9;">
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text-quaternary);letter-spacing:0.02em;white-space:nowrap;">Northwind</span>
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text-quaternary);letter-spacing:0.02em;white-space:nowrap;">Lumen Labs</span>
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text-quaternary);letter-spacing:0.02em;white-space:nowrap;">Fieldnote</span>
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text-quaternary);letter-spacing:0.02em;white-space:nowrap;">Arcadia</span>
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text-quaternary);letter-spacing:0.02em;white-space:nowrap;">Helios</span>
</div>
</section>
<!-- 4 · PROBLEM — PAS (P + A) -->
<section class="section">
<div class="wrap">
<div class="section-head">
<span class="kicker">The problem</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">You're not slow. Your setup is.</h2>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);">Sound familiar? The longer it goes unfixed, the more it quietly taxes everything Malenia is supposed to make easy.</p>
</div>
<div class="problem-grid">
<div style="background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);">
<div style="display:flex;align-items:flex-start;gap:var(--brand-size-sm);">
<span style="flex:0 0 auto;width:8px;height:8px;border-radius:50%;margin-top:9px;background:var(--brand-color-error);"></span>
<div>
<h3 style="font-size:var(--brand-font-size-lg);margin-bottom:var(--brand-size-xxs);">Every deliverable starts from scratch</h3>
<p class="clamp-3" style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size);">So the same work gets redone, the backlog grows, and shipping slips by days.</p>
</div>
</div>
</div>
<div style="background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);">
<div style="display:flex;align-items:flex-start;gap:var(--brand-size-sm);">
<span style="flex:0 0 auto;width:8px;height:8px;border-radius:50%;margin-top:9px;background:var(--brand-color-error);"></span>
<div>
<h3 style="font-size:var(--brand-font-size-lg);margin-bottom:var(--brand-size-xxs);">The tools don't talk to each other</h3>
<p class="clamp-3" style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size);">Context lives in five tabs, hand-offs break, and nobody trusts the source of truth.</p>
</div>
</div>
</div>
<div style="background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);">
<div style="display:flex;align-items:flex-start;gap:var(--brand-size-sm);">
<span style="flex:0 0 auto;width:8px;height:8px;border-radius:50%;margin-top:9px;background:var(--brand-color-error);"></span>
<div>
<h3 style="font-size:var(--brand-font-size-lg);margin-bottom:var(--brand-size-xxs);">Quality drifts as you scale</h3>
<p class="clamp-3" style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size);">What looked sharp at ten people looks inconsistent at fifty — and customers notice.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 5 · VALUE PILLARS — PAS (S), FAB so each shows the benefit -->
<section id="value" class="section" style="background:var(--brand-color-bg-container);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);">
<div class="wrap">
<div class="section-head">
<span class="kicker">Why Malenia</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Here's how that changes</h2>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);">Three ways Malenia turns the daily grind into something you actually look forward to.</p>
</div>
<div class="pillars">
<div style="background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);box-shadow:0 1px 2px var(--brand-color-fill-quaternary);display:flex;flex-direction:column;">
<div style="display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:var(--brand-color-primary-bg);color:var(--brand-color-primary);margin-bottom:var(--brand-size-md);"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" style="display:block;"><path d="M5 12l4 4 10-10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3 class="clamp-2" style="font-size:var(--brand-font-size-heading-4);margin-bottom:var(--brand-size-xs);">Network freedom</h3>
<p class="clamp-3" style="color:var(--brand-color-text-secondary);">Sophisticated by design — and built into every surface Malenia ships.</p>
</div>
<div style="background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);box-shadow:0 1px 2px var(--brand-color-fill-quaternary);display:flex;flex-direction:column;">
<div style="display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:var(--brand-color-primary-bg);color:var(--brand-color-primary);margin-bottom:var(--brand-size-md);"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" style="display:block;"><path d="M12 3v18M3 12h18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3 class="clamp-2" style="font-size:var(--brand-font-size-heading-4);margin-bottom:var(--brand-size-xs);">Practical simplicity</h3>
<p class="clamp-3" style="color:var(--brand-color-text-secondary);">Quiet by design — and built into every surface Malenia ships.</p>
</div>
<div style="background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);box-shadow:0 1px 2px var(--brand-color-fill-quaternary);display:flex;flex-direction:column;">
<div style="display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:var(--brand-color-primary-bg);color:var(--brand-color-primary);margin-bottom:var(--brand-size-md);"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" style="display:block;"><circle cx="11" cy="11" r="6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 20l-4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3 class="clamp-2" style="font-size:var(--brand-font-size-heading-4);margin-bottom:var(--brand-size-xs);">Aesthetic restraint</h3>
<p class="clamp-3" style="color:var(--brand-color-text-secondary);">Essential by design — and built into every surface Malenia ships.</p>
</div>
</div>
</div>
</section>
<!-- 6 · HOW IT WORKS — 3 steps -->
<section id="how" class="section">
<div class="wrap">
<div class="section-head">
<span class="kicker">How it works</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Up and running in three steps</h2>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);">No migration project. No six-week rollout.</p>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:clamp(24px,4vw,48px);">
<div style="display:flex;flex-direction:column;gap:var(--brand-size-md);">
<div style="aspect-ratio:124/108;border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);background:var(--brand-color-bg-layout);padding:var(--brand-size-lg);"><svg viewBox="0 0 124 108" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><rect x="14" y="40" width="92" height="9" rx="4.5" fill="var(--brand-color-border-secondary)"/><rect x="14" y="58" width="58" height="9" rx="4.5" fill="var(--brand-color-border-secondary)"/><rect x="14" y="80" width="40" height="14" rx="7" fill="var(--brand-color-primary)"/></svg></div>
<div>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--brand-color-primary);color:#fff;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);">1</span>
<h3 style="font-size:var(--brand-font-size-heading-4);">Connect</h3>
</div>
<p style="color:var(--brand-color-text-secondary);max-width:34ch;">Point Malenia at what you already have — no rebuild required.</p>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-md);">
<div style="aspect-ratio:124/108;border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);background:var(--brand-color-bg-layout);padding:var(--brand-size-lg);"><svg viewBox="0 0 124 108" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><rect x="14" y="38" width="44" height="38" rx="6" fill="none" stroke="var(--brand-color-border-secondary)" stroke-width="2"/><rect x="66" y="38" width="44" height="38" rx="6" fill="none" stroke="var(--brand-color-primary)" stroke-width="2"/><rect x="14" y="84" width="96" height="9" rx="4.5" fill="var(--brand-color-border-secondary)"/></svg></div>
<div>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--brand-color-primary);color:#fff;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);">2</span>
<h3 style="font-size:var(--brand-font-size-heading-4);">Make it yours</h3>
</div>
<p style="color:var(--brand-color-text-secondary);max-width:34ch;">Adjust the defaults until they fit the way your team works.</p>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-md);">
<div style="aspect-ratio:124/108;border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);background:var(--brand-color-bg-layout);padding:var(--brand-size-lg);"><svg viewBox="0 0 124 108" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><path d="M16 70l20 16 28-44" fill="none" stroke="var(--brand-color-primary)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><rect x="72" y="44" width="38" height="9" rx="4.5" fill="var(--brand-color-border-secondary)"/><rect x="72" y="64" width="26" height="9" rx="4.5" fill="var(--brand-color-border-secondary)"/></svg></div>
<div>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--brand-color-primary);color:#fff;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);">3</span>
<h3 style="font-size:var(--brand-font-size-heading-4);">Launch</h3>
</div>
<p style="color:var(--brand-color-text-secondary);max-width:34ch;">Go live with confidence, then iterate from real feedback.</p>
</div>
</div>
</div>
</div>
</section>
<!-- recurring CTA (~every 1.5 screens) — calm, the loud one is reserved for module 11 -->
<section class="wrap" style="padding:0 0 clamp(56px,8vw,88px);">
<div style="border:var(--brand-line-width) solid var(--brand-color-primary-border);background:var(--brand-color-primary-bg);border-radius:clamp(14px,2vw,20px);padding:clamp(24px,4vw,36px) clamp(24px,5vw,48px);display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-lg);flex-wrap:wrap;">
<div style="min-width:0;">
<h3 style="font-size:clamp(20px,2.4vw,26px);letter-spacing:-0.01em;">See it on your own work in minutes</h3>
<p style="color:var(--brand-color-text-secondary);margin-top:var(--brand-size-xxs);">Free plan · No credit card · 2-minute setup</p>
</div>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Get started free</a>
</div>
</section>
<!-- 7 · FEATURES — zig-zag deep dive -->
<section id="features" class="section" style="background:var(--brand-color-bg-container);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);">
<div class="wrap">
<div class="section-head">
<span class="kicker">A closer look</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Built for the way real teams work</h2>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);">Every capability earns its place — and translates to something you can feel.</p>
</div>
<div class="zig"><div>
<span class="kicker">Capability 01</span>
<h3 style="font-size:clamp(22px,2.6vw,30px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Secure from day one</h3>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);max-width:46ch;">SSO, audit logs and granular permissions are built in, not bolted on.</p>
<div style="margin-top:var(--brand-size-lg);">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Learn more</a>
</div>
</div><div class="zig-visual" style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);background:var(--brand-color-bg-container);box-shadow:0 8px 28px -16px var(--brand-color-fill-secondary);padding:var(--brand-size-xl);min-height:240px;display:flex;flex-direction:column;gap:var(--brand-size-md);">
<div style="display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:11px;background:var(--brand-color-primary-bg);color:var(--brand-color-primary);"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" style="display:block;"><path d="M4 13l5 5L20 6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 7l3 3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div style="height:12px;width:70%;border-radius:6px;background:var(--brand-color-fill-secondary);"></div>
<div style="height:12px;width:52%;border-radius:6px;background:var(--brand-color-fill-tertiary);"></div>
<div style="margin-top:auto;display:flex;gap:var(--brand-size-sm);">
<div style="flex:1;height:64px;border-radius:var(--brand-border-radius);background:var(--brand-color-primary-bg);"></div>
<div style="flex:1;height:64px;border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid var(--brand-color-border-secondary);"></div>
</div>
</div></div>
<div class="zig"><div class="zig-visual" style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);background:var(--brand-color-bg-container);box-shadow:0 8px 28px -16px var(--brand-color-fill-secondary);padding:var(--brand-size-xl);min-height:240px;display:flex;flex-direction:column;gap:var(--brand-size-md);">
<div style="display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:11px;background:var(--brand-color-primary-bg);color:var(--brand-color-primary);"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" style="display:block;"><rect x="4" y="4" width="16" height="16" rx="3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 12h6M12 9v6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div style="height:12px;width:70%;border-radius:6px;background:var(--brand-color-fill-secondary);"></div>
<div style="height:12px;width:52%;border-radius:6px;background:var(--brand-color-fill-tertiary);"></div>
<div style="margin-top:auto;display:flex;gap:var(--brand-size-sm);">
<div style="flex:1;height:64px;border-radius:var(--brand-border-radius);background:var(--brand-color-primary-bg);"></div>
<div style="flex:1;height:64px;border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid var(--brand-color-border-secondary);"></div>
</div>
</div><div>
<span class="kicker">Capability 02</span>
<h3 style="font-size:clamp(22px,2.6vw,30px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Works where you work</h3>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);max-width:46ch;">Integrates with the tools your team already relies on every day.</p>
<div style="margin-top:var(--brand-size-lg);">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Learn more</a>
</div>
</div></div>
<div class="zig"><div>
<span class="kicker">Capability 03</span>
<h3 style="font-size:clamp(22px,2.6vw,30px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Insight you can act on</h3>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);max-width:46ch;">Reporting that reads like a story, not a spreadsheet.</p>
<div style="margin-top:var(--brand-size-lg);">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Learn more</a>
</div>
</div><div class="zig-visual" style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);background:var(--brand-color-bg-container);box-shadow:0 8px 28px -16px var(--brand-color-fill-secondary);padding:var(--brand-size-xl);min-height:240px;display:flex;flex-direction:column;gap:var(--brand-size-md);">
<div style="display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:11px;background:var(--brand-color-primary-bg);color:var(--brand-color-primary);"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" style="display:block;"><path d="M12 2l3 6 7 .9-5 4.7 1.3 6.9L12 18l-6.3 3.5L7 14.6 2 9.9 9 9z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div style="height:12px;width:70%;border-radius:6px;background:var(--brand-color-fill-secondary);"></div>
<div style="height:12px;width:52%;border-radius:6px;background:var(--brand-color-fill-tertiary);"></div>
<div style="margin-top:auto;display:flex;gap:var(--brand-size-sm);">
<div style="flex:1;height:64px;border-radius:var(--brand-border-radius);background:var(--brand-color-primary-bg);"></div>
<div style="flex:1;height:64px;border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid var(--brand-color-border-secondary);"></div>
</div>
</div></div>
</div>
</section>
<!-- 8 · PROOF — stat band + testimonials -->
<section style="background:var(--brand-color-primary);padding:clamp(48px,6vw,64px) 0;">
<div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--brand-size-xl);">
<div style="text-align:center;">
<div style="font-family:var(--display-family);font-size:clamp(30px,3.6vw,46px);font-weight:var(--brand-font-weight-strong);line-height:1.05;color:#fff;letter-spacing:-0.02em;">10k+</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:rgba(255,255,255,0.78);">Teams onboard</div>
</div>
<div style="text-align:center;">
<div style="font-family:var(--display-family);font-size:clamp(30px,3.6vw,46px);font-weight:var(--brand-font-weight-strong);line-height:1.05;color:#fff;letter-spacing:-0.02em;">99.9%</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:rgba(255,255,255,0.78);">Uptime SLA</div>
</div>
<div style="text-align:center;">
<div style="font-family:var(--display-family);font-size:clamp(30px,3.6vw,46px);font-weight:var(--brand-font-weight-strong);line-height:1.05;color:#fff;letter-spacing:-0.02em;">4.9/5</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:rgba(255,255,255,0.78);">Average rating</div>
</div>
<div style="text-align:center;">
<div style="font-family:var(--display-family);font-size:clamp(30px,3.6vw,46px);font-weight:var(--brand-font-weight-strong);line-height:1.05;color:#fff;letter-spacing:-0.02em;">120+</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:rgba(255,255,255,0.78);">Integrations</div>
</div>
</div>
</section>
<section class="section">
<div class="wrap">
<div class="section-head">
<span class="kicker">Proof</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Loved by the teams who use it daily</h2>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);">Real results, in their words — not ours.</p>
</div>
<div class="grid-3">
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1"><p style="margin:0 0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);line-height:1.6;color:var(--brand-color-text)">&ldquo;Malenia is the first tool in this space that actually feels Sophisticated. It became part of our daily flow within a week.&rdquo;</p>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">MC</span>
<div style="min-width:0;">
<div style="font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);font-size:var(--brand-font-size);">Maya Chen</div>
<div style="font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">Head of Design, Northwind</div>
</div>
</div></div></div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1"><p style="margin:0 0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);line-height:1.6;color:var(--brand-color-text)">&ldquo;We were skeptical, then we shipped our first project — genuinely Quiet — in a single afternoon. The team never looked back.&rdquo;</p>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">JW</span>
<div style="min-width:0;">
<div style="font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);font-size:var(--brand-font-size);">Jonas Weber</div>
<div style="font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">Engineering Lead, Lumen Labs</div>
</div>
</div></div></div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1"><p style="margin:0 0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);line-height:1.6;color:var(--brand-color-text)">&ldquo;The quality bar out of the box is what sold us. Свобода. Практика. Простота is not just a slogan here.&rdquo;</p>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">PN</span>
<div style="min-width:0;">
<div style="font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);font-size:var(--brand-font-size);">Priya Natarajan</div>
<div style="font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">Founder, Fieldnote</div>
</div>
</div></div></div>
</div>
</div>
</section>
<!-- 9 · OBJECTION — FAQ -->
<section id="faq" class="section" style="background:var(--brand-color-bg-container);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);">
<div class="wrap" style="max-width:780px;">
<div class="section-head">
<span class="kicker">Before you ask</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Your questions, answered</h2>
</div>
<div><details class="kit-acc" open style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">What is Malenia?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</div>
</details>
<details class="kit-acc" style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">How long does it take to get started?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">Most teams are up and running in under an hour. There is nothing to install and no migration to plan — connect, customize, launch.</div>
</details>
<details class="kit-acc" style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">Can I try it before committing?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">Yes. Start on the free plan, invite your team, and upgrade only when you need more. No credit card required.</div>
</details>
<details class="kit-acc" style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">How does pricing work?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">Plans scale with your team. Start free, move to Pro when you need unlimited usage, and talk to us about Enterprise for custom needs.</div>
</details>
<details class="kit-acc" style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">What about security and privacy?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">Your data stays yours. Access is encrypted in transit and at rest, and granular permissions keep the right things with the right people.</div>
</details></div>
</div>
</section>
<!-- 10 · PRICING — middle tier highlighted -->
<section id="pricing" class="section">
<div class="wrap">
<div class="section-head">
<span class="kicker">Pricing</span>
<h2 style="font-size:clamp(26px, 3.2vw, 38px);letter-spacing:-0.015em;margin-bottom:var(--brand-size-sm);">Plans that scale with you</h2>
<p style="color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);">Start free. Upgrade only when it earns it.</p>
</div>
<div class="grid-3" style="max-width:1000px;margin:0 auto;">
<div style="display:flex;flex-direction:column;position:relative;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);font-family:var(--brand-font-family);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text);">Starter</span>
</div>
<p style="margin:0 0 var(--brand-size-md);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">For trying things out.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-xxs);margin-bottom:var(--brand-size-lg);">
<span style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1;letter-spacing:-0.01em;">$0</span>
<span style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size-sm);">/ mo</span>
</div>
<ul style="list-style:none;margin:0 0 var(--brand-size-xl);padding:0;flex:1;"><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Up to 3 projects</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Network freedom</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Community support</span></li></ul>
<div style="display:flex;flex-direction:column;"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border)">Start for free</a></div>
</div>
<div style="display:flex;flex-direction:column;position:relative;background:var(--brand-color-bg-container);border:2px solid var(--brand-color-primary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);font-family:var(--brand-font-family);box-shadow:0 12px 32px var(--brand-color-primary-bg)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text);">Pro</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Most popular</span>
</div>
<p style="margin:0 0 var(--brand-size-md);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">For growing teams.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-xxs);margin-bottom:var(--brand-size-lg);">
<span style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1;letter-spacing:-0.01em;">$29</span>
<span style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size-sm);">/ mo</span>
</div>
<ul style="list-style:none;margin:0 0 var(--brand-size-xl);padding:0;flex:1;"><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Unlimited projects</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Practical simplicity</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Aesthetic restraint</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Priority support</span></li></ul>
<div style="display:flex;flex-direction:column;"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Start free trial</a></div>
</div>
<div style="display:flex;flex-direction:column;position:relative;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);font-family:var(--brand-font-family);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text);">Enterprise</span>
</div>
<p style="margin:0 0 var(--brand-size-md);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">For organizations at scale.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-xxs);margin-bottom:var(--brand-size-lg);">
<span style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1;letter-spacing:-0.01em;">Custom</span>
</div>
<ul style="list-style:none;margin:0 0 var(--brand-size-xl);padding:0;flex:1;"><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>SSO &amp; audit logs</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Dedicated success manager</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Custom contracts &amp; SLA</span></li></ul>
<div style="display:flex;flex-direction:column;"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border)">Talk to sales</a></div>
</div>
</div>
<p style="text-align:center;margin-top:var(--brand-size-lg);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">All prices in USD · Cancel anytime · No credit card to start.</p>
</div>
</section>
</main>
<!-- 11 · FINAL CTA — the page's strongest accent -->
<section class="wrap" style="padding-bottom:clamp(64px,9vw,104px);">
<div style="background:var(--brand-color-primary);border-radius:clamp(18px,2.4vw,28px);padding:clamp(48px,7vw,84px) clamp(24px,5vw,64px);text-align:center;position:relative;overflow:hidden;">
<div style="position:absolute;top:-100px;right:-90px;width:320px;height:320px;border-radius:50%;background:var(--brand-color-primary-hover);opacity:0.5;"></div>
<div style="position:absolute;bottom:-120px;left:-80px;width:260px;height:260px;border-radius:50%;background:var(--brand-color-primary-active);opacity:0.45;"></div>
<div style="position:relative;">
<h2 style="color:#fff;font-size:clamp(28px,4vw,46px);letter-spacing:-0.02em;line-height:1.08;margin-bottom:var(--brand-size-md);max-width:20ch;margin-left:auto;margin-right:auto;">Start building with Malenia today</h2>
<p class="clamp-2" style="color:rgba(255,255,255,0.88);font-size:var(--brand-font-size-lg);max-width:540px;margin:0 auto var(--brand-size-xl);">Свобода. Практика. Простота.</p>
<div style="display:flex;gap:var(--brand-size-sm);justify-content:center;flex-wrap:wrap;">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);background:#fff;color:var(--brand-color-primary);border:none;">Get started free</a>
<a href="#" style="display:inline-flex;align-items:center;height:var(--brand-control-height-lg);color:rgba(255,255,255,0.92);font-weight:var(--brand-font-weight-strong);padding:0 var(--brand-size-md);">Talk to us &rarr;</a>
</div>
<p style="margin-top:var(--brand-size-md);font-size:var(--brand-font-size-sm);color:rgba(255,255,255,0.72);">2-minute setup · Cancel anytime</p>
</div>
</div>
</section>
<!-- 12 · FOOTER — grey, small, restrained -->
<footer style="background:var(--brand-color-bg-container);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);padding:clamp(48px,6vw,64px) 0 32px;">
<div class="wrap">
<div class="footer-cols">
<div>
<span style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);letter-spacing:-0.01em;color:var(--brand-color-text);white-space:nowrap;">Malenia</span>
<p class="clamp-2" style="color:var(--brand-color-text-tertiary);max-width:36ch;margin-top:var(--brand-size-sm);font-size:var(--brand-font-size-sm);">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
</div>
<div>
<div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-sm);text-transform:uppercase;letter-spacing:0.08em;color:var(--brand-color-text-tertiary);margin-bottom:var(--brand-size-md);">Product</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-xs);">
<a href="#" style="color:var(--brand-color-text-secondary);">How it works</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Features</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Pricing</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Changelog</a>
</div>
</div>
<div>
<div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-sm);text-transform:uppercase;letter-spacing:0.08em;color:var(--brand-color-text-tertiary);margin-bottom:var(--brand-size-md);">Company</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-xs);">
<a href="#" style="color:var(--brand-color-text-secondary);">About</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Blog</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Careers</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Contact</a>
</div>
</div>
<div>
<div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-sm);text-transform:uppercase;letter-spacing:0.08em;color:var(--brand-color-text-tertiary);margin-bottom:var(--brand-size-md);">Resources</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-xs);">
<a href="#" style="color:var(--brand-color-text-secondary);">Docs</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Support</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Status</a>
<a href="#" style="color:var(--brand-color-text-secondary);">Privacy</a>
</div>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--brand-size-lg);flex-wrap:wrap;margin-top:48px;padding-top:var(--brand-size-lg);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">
<span>&copy; Malenia &middot; malenia.space</span>
<span>Sophisticated · Quiet · Essential</span>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,427 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Malenia — Newsletter</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
:root { --display-family: 'Cormorant Garamond', serif, var(--brand-font-family); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--brand-font-family);
font-size: var(--brand-font-size);
line-height: var(--brand-line-height);
color: var(--brand-color-text);
background: var(--brand-color-bg-layout);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--brand-color-link); text-decoration: none; }
h1, h2, h3, h4 {
margin: 0;
font-family: var(--display-family);
line-height: var(--brand-line-height-heading);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text);
overflow-wrap: break-word;
}
p { margin: 0; overflow-wrap: break-word; }
img, svg { max-width: 100%; display: block; }
.wrap { max-width: 1120px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
.kicker {
display: block;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: var(--brand-font-size-sm);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-primary);
margin-bottom: var(--brand-size-sm);
}
.clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
</style>
</head>
<body>
<div style="padding:var(--brand-size-xl) var(--brand-size-md);">
<div style="max-width:600px;margin:0 auto;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;">
<!-- preheader: shown in the inbox preview, hidden in body -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:var(--brand-color-bg-container);">Свобода. Практика. Простота.</div>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
<!-- masthead -->
<tr><td style="padding:var(--brand-size-xl) var(--brand-size-xxl) var(--brand-size-lg);text-align:center;border-bottom:var(--brand-line-width) solid var(--brand-color-border);">
<div style="display:inline-block;width:32px;height:var(--brand-line-width);background:var(--brand-color-primary);margin-bottom:var(--brand-size-md);"></div>
<div style="font-family:var(--display-family);font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-heading-3);letter-spacing:-0.02em;color:var(--brand-color-text);">Malenia</div>
<div style="margin-top:var(--brand-size-xs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);text-transform:uppercase;letter-spacing:0.2em;">Sophisticated reads · Issue 01</div>
</td></tr>
<!-- lede: inverted pyramid — core line + why-relevant, above the fold -->
<tr><td style="padding:var(--brand-size-xl) var(--brand-size-xxl) var(--brand-size-md);">
<p style="margin:0 0 var(--brand-size-xs);text-transform:uppercase;letter-spacing:0.16em;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-primary);">In this issue</p>
<h1 style="font-size:var(--brand-font-size-heading-2);line-height:1.18;letter-spacing:-0.015em;margin:0 0 var(--brand-size-sm);color:var(--brand-color-text);">Network freedom</h1>
<p style="margin:0;color:var(--brand-color-text-secondary);font-size:var(--brand-font-size-lg);line-height:1.6;">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
</td></tr>
<!-- primary CTA, surfaced above the fold (one job) -->
<tr><td style="padding:0 var(--brand-size-xxl) var(--brand-size-xl);text-align:center;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;">
<tr><td style="border-radius:var(--brand-border-radius);"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Read this issue</a></td></tr>
</table>
</td></tr>
<tr><td><div style="height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin:0 var(--brand-size-xxl);"></div></td></tr>
<!-- numbered stories -->
<tr><td style="padding:var(--brand-size-xl) var(--brand-size-xxl) var(--brand-size-lg);">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;">
<tr>
<td valign="top" style="width:54px;padding-right:var(--brand-size-md);">
<div style="font-family:var(--display-family);font-size:var(--brand-font-size-heading-3);font-weight:var(--brand-font-weight-strong);line-height:1;color:var(--brand-color-primary);letter-spacing:-0.02em;">01</div>
<div style="margin-top:var(--brand-size-xs);width:24px;height:var(--brand-line-width);background:var(--brand-color-primary-border);"></div>
</td>
<td valign="top">
<h2 style="font-size:var(--brand-font-size-heading-4);line-height:1.25;letter-spacing:-0.01em;margin:0 0 var(--brand-size-xs);color:var(--brand-color-text);">Network freedom</h2>
<p style="margin:0 0 var(--brand-size-md);color:var(--brand-color-text-secondary);font-size:var(--brand-font-size);line-height:var(--brand-line-height);">Sophisticated by design — and built into every surface Malenia ships.</p>
<a href="#" style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-link);text-decoration:none;">Read the full story &rarr;</a>
</td>
</tr>
</table>
</td></tr>
<tr><td><div style="height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin:0 var(--brand-size-xxl);"></div></td></tr>
<tr><td style="padding:var(--brand-size-xl) var(--brand-size-xxl) var(--brand-size-lg);">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;">
<tr>
<td valign="top" style="width:54px;padding-right:var(--brand-size-md);">
<div style="font-family:var(--display-family);font-size:var(--brand-font-size-heading-3);font-weight:var(--brand-font-weight-strong);line-height:1;color:var(--brand-color-primary);letter-spacing:-0.02em;">02</div>
<div style="margin-top:var(--brand-size-xs);width:24px;height:var(--brand-line-width);background:var(--brand-color-primary-border);"></div>
</td>
<td valign="top">
<h2 style="font-size:var(--brand-font-size-heading-4);line-height:1.25;letter-spacing:-0.01em;margin:0 0 var(--brand-size-xs);color:var(--brand-color-text);">Practical simplicity</h2>
<p style="margin:0 0 var(--brand-size-md);color:var(--brand-color-text-secondary);font-size:var(--brand-font-size);line-height:var(--brand-line-height);">Quiet by design — and built into every surface Malenia ships.</p>
<a href="#" style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-link);text-decoration:none;">See how it works &rarr;</a>
</td>
</tr>
</table>
</td></tr>
<tr><td><div style="height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin:0 var(--brand-size-xxl);"></div></td></tr>
<tr><td style="padding:var(--brand-size-xl) var(--brand-size-xxl) var(--brand-size-lg);">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;">
<tr>
<td valign="top" style="width:54px;padding-right:var(--brand-size-md);">
<div style="font-family:var(--display-family);font-size:var(--brand-font-size-heading-3);font-weight:var(--brand-font-weight-strong);line-height:1;color:var(--brand-color-primary);letter-spacing:-0.02em;">03</div>
<div style="margin-top:var(--brand-size-xs);width:24px;height:var(--brand-line-width);background:var(--brand-color-primary-border);"></div>
</td>
<td valign="top">
<h2 style="font-size:var(--brand-font-size-heading-4);line-height:1.25;letter-spacing:-0.01em;margin:0 0 var(--brand-size-xs);color:var(--brand-color-text);">Aesthetic restraint</h2>
<p style="margin:0 0 var(--brand-size-md);color:var(--brand-color-text-secondary);font-size:var(--brand-font-size);line-height:var(--brand-line-height);">Essential by design — and built into every surface Malenia ships.</p>
<a href="#" style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-link);text-decoration:none;">Get the details &rarr;</a>
</td>
</tr>
</table>
</td></tr>
<tr><td><div style="height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin:0 var(--brand-size-xxl);"></div></td></tr>
<!-- closing CTA: same action, echoed once (email.md) -->
<tr><td style="padding:var(--brand-size-xl) var(--brand-size-xxl) var(--brand-size-xxl);text-align:center;">
<p style="margin:0 0 var(--brand-size-md);color:var(--brand-color-text-secondary);font-size:var(--brand-font-size);">That's the issue. Malenia lands in your inbox next week.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;">
<tr><td style="border-radius:var(--brand-border-radius);"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Read this issue</a></td></tr>
</table>
</td></tr>
</table>
<div style="padding:var(--brand-size-lg) var(--brand-size-xl);background:var(--brand-color-fill-quaternary);color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size-sm);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);text-align:center;">
<p style="margin-bottom:var(--brand-size-xs);">You're receiving this because you follow Malenia.</p>
<p><a href="#" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">View in browser</a> &nbsp;&middot;&nbsp; <a href="#" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">Preferences</a> &nbsp;&middot;&nbsp; <a href="#" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">Unsubscribe</a></p>
<p style="margin-top:var(--brand-size-xs);">&copy; Malenia &middot; malenia.space</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,434 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Malenia — Poster</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
:root { --display-family: 'Cormorant Garamond', serif, var(--brand-font-family); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--brand-font-family);
font-size: var(--brand-font-size);
line-height: var(--brand-line-height);
color: var(--brand-color-text);
background: var(--brand-color-bg-layout);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--brand-color-link); text-decoration: none; }
h1, h2, h3, h4 {
margin: 0;
font-family: var(--display-family);
line-height: var(--brand-line-height-heading);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text);
overflow-wrap: break-word;
}
p { margin: 0; overflow-wrap: break-word; }
img, svg { max-width: 100%; display: block; }
.wrap { max-width: 1120px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
.kicker {
display: block;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: var(--brand-font-size-sm);
font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-primary);
margin-bottom: var(--brand-size-sm);
}
.clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
.poster-stage { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: clamp(20px, 4vw, 48px); background: var(--brand-color-bg-layout); }
.poster {
position: relative;
aspect-ratio: 3 / 4;
width: min(720px, 92vw, calc((100vh - 96px) * 3 / 4));
container-type: inline-size;
background: var(--brand-color-bg-elevated);
color: var(--brand-color-text);
border: var(--brand-line-width) solid var(--brand-color-border-secondary);
border-radius: 1.4cqi;
overflow: hidden;
box-shadow: 0 36px 90px var(--brand-color-fill-secondary), 0 4px 12px var(--brand-color-fill-quaternary);
display: grid;
grid-template-rows: auto 1fr auto;
padding: 9cqi;
}
/* hairline frame inset — quiet print-mark, not a banner */
.poster::after {
content: "";
position: absolute; inset: 4cqi;
border: var(--brand-line-width) solid var(--brand-color-border-secondary);
border-radius: 0.8cqi;
pointer-events: none;
}
.poster > * { position: relative; z-index: 1; }
/* ── three type-size roles ────────────────────────────────────────── */
.p-focal {
font-family: var(--display-family);
font-weight: var(--brand-font-weight-strong);
line-height: 1.02;
letter-spacing: -0.025em;
margin: 0;
text-wrap: balance;
color: var(--brand-color-text);
}
.p-focal .accent { color: var(--brand-color-primary); }
.p-sub {
font-size: 3cqi; line-height: 1.42; margin: 0;
color: var(--brand-color-text-secondary); max-width: 56cqi;
}
.p-meta {
font-size: 1.9cqi; line-height: 1.4; letter-spacing: 0.02em;
color: var(--brand-color-text-tertiary);
}
.p-kicker {
text-transform: uppercase; letter-spacing: 0.28em;
font-size: 1.8cqi; font-weight: var(--brand-font-weight-strong);
color: var(--brand-color-text-tertiary); margin: 0;
}
/* the single accent mark that sits on the focal */
.p-rule { width: 12cqi; height: 0.7cqi; background: var(--brand-color-primary); border-radius: 0.4cqi; }
/* header band: tiny wordmark + edition tag, like a print masthead */
.p-head { display: flex; align-items: center; justify-content: space-between; gap: 3cqi; }
.p-wordmark { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 2.4cqi; letter-spacing: -0.01em; color: var(--brand-color-text); }
/* signature footer: logo small & confident, like a signature */
.p-sign { display: flex; align-items: flex-end; justify-content: space-between; gap: 3cqi; }
.p-sign-mark { display: flex; align-items: center; gap: 1.6cqi; }
.p-sign-dot { width: 2.6cqi; height: 2.6cqi; border-radius: 50%; background: var(--brand-color-primary); flex: 0 0 auto; }
.p-sign-name { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 2.8cqi; letter-spacing: -0.01em; color: var(--brand-color-text); }
/* ── Event 5W grid ────────────────────────────────────────────────── */
.p-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 4cqi 5cqi; margin: 0; }
.p-fact { display: flex; flex-direction: column; gap: 1cqi; min-width: 0; }
.p-fact-k { text-transform: uppercase; letter-spacing: 0.2em; font-size: 1.6cqi; font-weight: var(--brand-font-weight-strong); color: var(--brand-color-text-tertiary); }
.p-fact-v { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 3.4cqi; line-height: 1.12; color: var(--brand-color-text); letter-spacing: -0.01em; }
/* When + Where are the most-looked-up facts → give them more weight */
.p-fact--lead .p-fact-v { font-size: 4cqi; }
/* CTA block — one action, scan / short link, with a line-drawn code mark */
.p-cta { display: flex; align-items: center; gap: 3.4cqi; padding-top: 4cqi; border-top: var(--brand-line-width) solid var(--brand-color-border-secondary); }
.p-cta-copy { min-width: 0; }
.p-cta-action { font-family: var(--display-family); font-weight: var(--brand-font-weight-strong); font-size: 3cqi; color: var(--brand-color-text); margin: 0 0 0.6cqi; letter-spacing: -0.01em; }
.p-cta-link { font-size: 2cqi; color: var(--brand-color-primary); font-weight: var(--brand-font-weight-strong); letter-spacing: 0.01em; }
.p-code { flex: 0 0 auto; width: 17cqi; height: 17cqi; border: var(--brand-line-width) solid var(--brand-color-border); border-radius: 1cqi; padding: 1.8cqi; background: var(--brand-color-bg-container); }
</style>
</head>
<body>
<div class="poster-stage">
<div class="poster">
<header class="p-head">
<span class="p-wordmark">Malenia</span>
<span class="p-meta">malenia.space</span>
</header>
<div style="display:flex;flex-direction:column;justify-content:center;gap:4cqi;padding:2cqi 0;">
<p class="p-kicker">SOPHISTICATED · QUIET · ESSENTIAL</p>
<h1 class="p-focal" style="font-size:12.5cqi;"><span class="accent">Свобода.</span> Практика. Простота.</h1>
<div class="p-rule"></div>
<p class="p-sub">A minimalist, high-aesthetic brand focused on network freedom and simplicity, characterized by a luxury-leaning, archival visual language.</p>
</div>
<footer class="p-sign">
<span class="p-sign-mark">
<span class="p-sign-dot"></span>
<span class="p-sign-name">Malenia</span>
</span>
<span class="p-meta" style="text-align:right;max-width:40cqi;">Sophisticated · Quiet</span>
</footer>
</div>
</div>
</body>
</html>

3769
.design/system/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,587 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Malenia — component kit (dark)</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root{
--brand-color-primary: #b9a566;
--brand-color-primary-bg: #292519;
--brand-color-primary-bg-hover: #413922;
--brand-color-primary-border: #4e4731;
--brand-color-primary-border-hover: #6b613f;
--brand-color-primary-hover: #cec391;
--brand-color-primary-active: #928352;
--brand-color-primary-text: #f5f3e6;
--brand-color-success: #64b537;
--brand-color-success-bg: #192913;
--brand-color-success-border: #304d20;
--brand-color-success-hover: #87ca5c;
--brand-color-success-active: #518f2f;
--brand-color-warning: #dcaa37;
--brand-color-warning-bg: #312512;
--brand-color-warning-border: #5b4920;
--brand-color-warning-hover: #e8c35e;
--brand-color-warning-active: #ad872f;
--brand-color-error: #dc6966;
--brand-color-error-bg: #32191a;
--brand-color-error-border: #5b3231;
--brand-color-error-hover: #e89590;
--brand-color-error-active: #ad5553;
--brand-color-info: #b9a566;
--brand-color-info-bg: #292519;
--brand-color-info-border: #4e4731;
--brand-color-link: #b9a566;
--brand-color-link-hover: #cec391;
--brand-color-link-active: #928352;
--brand-color-text: #dcdcdc;
--brand-color-text-secondary: #adadad;
--brand-color-text-tertiary: #7e7e7e;
--brand-color-text-quaternary: #4f4f4f;
--brand-color-fill: #3e3e3e;
--brand-color-fill-secondary: #303030;
--brand-color-fill-tertiary: #272727;
--brand-color-fill-quaternary: #1d1d1d;
--brand-color-bg-layout: #1d1d1d;
--brand-color-bg-container: #141414;
--brand-color-bg-elevated: #272727;
--brand-color-border: #3e3e3e;
--brand-color-border-secondary: #303030;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #292519;
--brand-primary-palette-2: #413922;
--brand-primary-palette-3: #4e4731;
--brand-primary-palette-4: #6b613f;
--brand-primary-palette-5: #928352;
--brand-primary-palette-6: #b9a566;
--brand-primary-palette-7: #cec391;
--brand-primary-palette-8: #e5dfc5;
--brand-primary-palette-9: #f5f3e6;
--brand-primary-palette-10: #faf9ec;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
font-family:var(--brand-font-family);
font-size:var(--brand-font-size);
line-height:var(--brand-line-height);
color:var(--brand-color-text);
background:var(--brand-color-bg-layout);
-webkit-font-smoothing:antialiased;
}
h1,h2,h3{font-family:'Cormorant Garamond', serif, var(--brand-font-family)}
a{color:inherit}
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
.wrap{max-width:960px;margin:0 auto;padding:var(--brand-size-xxl) var(--brand-size-lg)}
.panel{
background:var(--brand-color-bg-container);
border:var(--brand-line-width) solid var(--brand-color-border-secondary);
border-radius:var(--brand-border-radius-lg);
padding:var(--brand-size-xl);
margin-bottom:var(--brand-size-xl);
}
</style>
</head>
<body>
<div class="wrap">
<header style="margin-bottom:var(--brand-size-xxl)">
<div style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);padding:var(--brand-size-xxs) var(--brand-size-sm);border-radius:var(--brand-border-radius);background:var(--brand-color-primary-bg);color:var(--brand-color-primary);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong)">Malenia · dark theme</div>
<h1 style="margin:var(--brand-size) 0 var(--brand-size-xxs);font-size:var(--brand-font-size-heading-1);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Malenia — component kit (dark)</h1>
<p style="margin:0;max-width:60ch;font-size:var(--brand-font-size-lg);color:var(--brand-color-text-secondary)">Every component below is colored only by <code style="font-family:var(--brand-font-family-code)">var(--brand-*)</code> custom properties derived from one seed.</p>
</header>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Buttons</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Five types across three sizes.</p>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Primary</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border)">Default</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border);border-style:dashed">Dashed</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-text);border-color:transparent">Text</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Link</a>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size)">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Small</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Medium</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Large</a>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Form controls</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Inputs, selects, textareas, checkboxes and switches share control height, radius and focus ring.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:var(--brand-size-lg);max-width:720px">
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Email</label><input type="text" placeholder="you@example.com" style="box-sizing:border-box;width:100%;height:var(--brand-control-height);padding:0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;transition:border-color var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out)"></div>
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Project name</label><input type="text" value="Malenia" style="box-sizing:border-box;width:100%;height:var(--brand-control-height);padding:0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;transition:border-color var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out)"></div>
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Team size</label><select style="box-sizing:border-box;width:100%;height:var(--brand-control-height);padding:0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none"><option>Just me</option><option selected>2–10</option><option>11–50</option><option>50+</option></select></div>
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Message</label><textarea rows="3" placeholder="Tell us what you're building…" style="box-sizing:border-box;display:block;width:100%;padding:var(--brand-size-xs) var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);resize:vertical;outline:none"></textarea></div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size-lg);gap:var(--brand-size-lg)">
<label style="display:inline-flex;align-items:flex-start;gap:var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);cursor:pointer"><input type="checkbox" checked style="accent-color:var(--brand-color-primary);width:16px;height:16px;margin:3px 0 0;flex:0 0 auto"><span>Email me product updates</span></label>
<label style="display:inline-flex;align-items:flex-start;gap:var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);cursor:pointer"><input type="checkbox" style="accent-color:var(--brand-color-primary);width:16px;height:16px;margin:3px 0 0;flex:0 0 auto"><span>I agree to the terms</span></label>
<span style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);"><span style="display:inline-flex;align-items:center;flex:0 0 auto;width:44px;height:24px;padding:2px;border-radius:12px;box-sizing:border-box;transition:background var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);background:var(--brand-color-primary);justify-content:flex-end"><span style="width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);"></span></span><span style="font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);">Notifications</span></span>
<span style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);"><span style="display:inline-flex;align-items:center;flex:0 0 auto;width:44px;height:24px;padding:2px;border-radius:12px;box-sizing:border-box;transition:background var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);background:var(--brand-color-fill);justify-content:flex-start"><span style="width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);"></span></span><span style="font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);">Public profile</span></span>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Navigation</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Tabs, breadcrumbs, pagination and steps for app shells and flows.</p>
<div style="display:flex;gap:var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)"><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-primary);font-weight:var(--brand-font-weight-strong);border-bottom:2px solid var(--brand-color-primary)">Overview</span><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-text-secondary);border-bottom:2px solid transparent">Analytics</span><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-text-secondary);border-bottom:2px solid transparent">Reports</span><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-text-secondary);border-bottom:2px solid transparent">Settings</span></div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size-lg);justify-content:space-between;gap:var(--brand-size-lg)">
<nav style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size);"><span style="color:var(--brand-color-text-tertiary);">Malenia</span><span style="color:var(--brand-color-text-quaternary);">/</span><span style="color:var(--brand-color-text-tertiary);">Projects</span><span style="color:var(--brand-color-text-quaternary);">/</span><span style="color:var(--brand-color-text);">Brand system</span></nav>
<div style="display:inline-flex;gap:var(--brand-size-xxs);align-items:center;"><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">&lsaquo;</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">1</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-primary);background:var(--brand-color-primary);color:#fff">2</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">3</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">4</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">5</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">&rsaquo;</span></div>
</div>
<div style="margin-top:var(--brand-size-xl);">
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;flex-wrap:wrap;font-family:var(--brand-font-family);"><div style="flex:0 1 auto;min-width:140px;display:flex;gap:var(--brand-size-sm);align-items:flex-start"><span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:28px;height:28px;border-radius:50%;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-primary);background:var(--brand-color-primary);color:#fff">&#10003;</span><div style="min-width:0;padding-top:4px;"><div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-text);line-height:1.3">Connect</div><div style="margin-top:2px;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:var(--brand-line-height)">Point at a source</div></div></div><div style="flex:1 1 var(--brand-size-xl);min-width:var(--brand-size-md);height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin-top:14px"></div><div style="flex:0 1 auto;min-width:140px;display:flex;gap:var(--brand-size-sm);align-items:flex-start"><span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:28px;height:28px;border-radius:50%;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-primary);background:var(--brand-color-primary);color:#fff">2</span><div style="min-width:0;padding-top:4px;"><div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-text);line-height:1.3">Derive</div><div style="margin-top:2px;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:var(--brand-line-height)">Seed to tokens</div></div></div><div style="flex:1 1 var(--brand-size-xl);min-width:var(--brand-size-md);height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin-top:14px"></div><div style="flex:0 1 auto;min-width:140px;display:flex;gap:var(--brand-size-sm);align-items:flex-start"><span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:28px;height:28px;border-radius:50%;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border);background:var(--brand-color-bg-container);color:var(--brand-color-text-tertiary)">3</span><div style="min-width:0;padding-top:4px;"><div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-text);line-height:1.3">Ship</div><div style="margin-top:2px;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:var(--brand-line-height)">Export everywhere</div></div></div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Cards</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Elevated content surfaces with optional header &amp; footer.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--brand-size-lg)">
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)">Overview</div><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">Cards group related content on an elevated surface. Radius, border and shadow all read from the derived tokens.</div><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-fill-quaternary)"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Learn more</a></div></div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)">Usage</div><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">Drop the same component into a dark or compact theme and it re-skins automatically — no per-variant markup.</div></div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">A bodyonly card. <span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">live</span> <span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">beta</span></div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Data display</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Tables, stats, avatars and a no-JS accordion for structured content.</p>
<div style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;background:var(--brand-color-bg-container)"><table style="width:100%;border-collapse:collapse;font-family:var(--brand-font-family);"><thead><tr><th style="text-align:left;padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text-secondary);background:var(--brand-color-fill-quaternary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);white-space:nowrap">Token</th><th style="text-align:left;padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text-secondary);background:var(--brand-color-fill-quaternary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);white-space:nowrap">Role</th><th style="text-align:left;padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text-secondary);background:var(--brand-color-fill-quaternary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);white-space:nowrap">Value</th></tr></thead><tbody><tr><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">colorPrimary</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">Brand accent</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">seed</td></tr><tr><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">fontSize</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">Body type base</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">derived</td></tr><tr><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text)">borderRadius</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text)">Corner rounding</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text)">derived</td></tr></tbody></table></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--brand-size-lg);margin-top:var(--brand-size-xl)">
<div style="font-family:var(--brand-font-family);min-width:0">
<div style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:var(--brand-color-text);letter-spacing:-0.01em">10k+</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Teams onboard</div>
</div>
<div style="font-family:var(--brand-font-family);min-width:0">
<div style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:var(--brand-color-text);letter-spacing:-0.01em">99.9%</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Uptime SLA</div>
</div>
<div style="font-family:var(--brand-font-family);min-width:0">
<div style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:var(--brand-color-text);letter-spacing:-0.01em">4.9/5</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Average rating</div>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size-xl)">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height-lg);height:var(--brand-control-height-lg);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">M</span>
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">AL</span>
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">TB</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">design</span> <span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-success);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">ops</span>
</div>
<div style="margin-top:var(--brand-size-xl);">
<div><details class="kit-acc" open style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">What is a seed token?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">The ~20-field input every other token is derived from. Change one field and the whole system follows.</div>
</details>
<details class="kit-acc" style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">Can I theme dark mode separately?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">No need — dark is the same seed run through a different algorithm, so it can never drift from the light theme.</div>
</details></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Marketing blocks</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Testimonials and pricing compose the primitives — landing pages are these in the right order.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--brand-size-lg)">
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1"><p style="margin:0 0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);line-height:1.6;color:var(--brand-color-text)">&ldquo;We replaced three hand-maintained style guides with one generated system.&rdquo;</p>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">AL</span>
<div style="min-width:0;">
<div style="font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);font-size:var(--brand-font-size);">Ada Lovelace</div>
<div style="font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">Design lead</div>
</div>
</div></div></div>
<div style="display:flex;flex-direction:column;position:relative;background:var(--brand-color-bg-container);border:2px solid var(--brand-color-primary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);font-family:var(--brand-font-family);box-shadow:0 12px 32px var(--brand-color-primary-bg)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text);">Pro</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Most popular</span>
</div>
<p style="margin:0 0 var(--brand-size-md);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">For growing teams.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-xxs);margin-bottom:var(--brand-size-lg);">
<span style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1;letter-spacing:-0.01em;">$29</span>
<span style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size-sm);">/ mo</span>
</div>
<ul style="list-style:none;margin:0 0 var(--brand-size-xl);padding:0;flex:1;"><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Unlimited brands</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Dark &amp; compact themes</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Priority support</span></li></ul>
<div style="display:flex;flex-direction:column;"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Start free trial</a></div>
</div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)">Composable</div><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">Marketing blocks (testimonials, pricing, stats) compose the same primitives — so a landing page is just kit pieces in the right order.</div><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-fill-quaternary)"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">View artifacts</a></div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Tags</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Compact labels keyed to functional colors.</p>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center">
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Primary</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-success);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Success</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-warning);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Warning</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-error);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Error</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-info);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Info</span>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Alerts &amp; progress</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Semantic feedback states.</p>
<div style="display:flex;flex-direction:column;gap:var(--brand-size)">
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-success-bg);border:var(--brand-line-width) solid var(--brand-color-success-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-success);font-size:var(--brand-font-size-lg);line-height:1">&#10003;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:var(--brand-line-height)">Brand system generated</div><div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);line-height:var(--brand-line-height)">All design tokens were derived from a single seed color.</div></div></div>
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-info-bg);border:var(--brand-line-width) solid var(--brand-color-info-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-info);font-size:var(--brand-font-size-lg);line-height:1">&#8505;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:inherit;color:var(--brand-color-text);line-height:var(--brand-line-height)">Tokens map 1:1 onto CSS custom properties.</div></div></div>
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-warning-bg);border:var(--brand-line-width) solid var(--brand-color-warning-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-warning);font-size:var(--brand-font-size-lg);line-height:1">&#9888;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:var(--brand-line-height)">Wireframe mode is off</div><div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);line-height:var(--brand-line-height)">Filled surfaces are in use for this theme.</div></div></div>
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-error-bg);border:var(--brand-line-width) solid var(--brand-color-error-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-error);font-size:var(--brand-font-size-lg);line-height:1">&#10005;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:inherit;color:var(--brand-color-text);line-height:var(--brand-line-height)">Could not reach the source URL.</div></div></div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-sm);margin-top:var(--brand-size-xl);max-width:480px">
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);font-family:var(--brand-font-family);">
<div style="flex:1;height:8px;border-radius:4px;background:var(--brand-color-fill-secondary);overflow:hidden"><div style="width:72%;height:100%;border-radius:4px;background:var(--brand-color-primary);transition:width var(--brand-motion-duration-slow) var(--brand-motion-ease-out)"></div></div>
<span style="flex:0 0 auto;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">72%</span>
</div>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);font-family:var(--brand-font-family);">
<div style="flex:1;height:8px;border-radius:4px;background:var(--brand-color-fill-secondary);overflow:hidden"><div style="width:100%;height:100%;border-radius:4px;background:var(--brand-color-success);transition:width var(--brand-motion-duration-slow) var(--brand-motion-ease-out)"></div></div>
<span style="flex:0 0 auto;font-size:var(--brand-font-size-sm);color:var(--brand-color-success)">&#10003;</span>
</div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Color</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">The full derived palette and neutrals.</p>
<div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Primary palette</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#292519"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">50</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#413922"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">200</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#4e4731"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">300</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#6b613f"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">400</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#928352"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">500</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#b9a566"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">600</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#cec391"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">700</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#e5dfc5"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">800</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#f5f3e6"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">900</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#faf9ec"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">1050</span>
</div></div>
</div><div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Functional</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#b9a566"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">primary</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#64b537"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">success</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#dcaa37"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">warning</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#dc6966"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">error</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#b9a566"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">info</span>
</div></div>
</div><div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Neutral text</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#dcdcdc"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#adadad"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text/2</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#7e7e7e"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text/3</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#4f4f4f"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text/4</span>
</div></div>
</div><div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Surfaces &amp; borders</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#1d1d1d"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">layout</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#141414"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">container</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#272727"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">elevated</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#3e3e3e"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">border</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#303030"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">border/2</span>
</div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Type scale</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">A modular ramp derived from the base font size.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 1 · 38px</span>
<span style="font-family:var(--brand-font-family);font-size:38px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 2 · 32px</span>
<span style="font-family:var(--brand-font-family);font-size:32px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 3 · 26px</span>
<span style="font-family:var(--brand-font-family);font-size:26px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 4 · 22px</span>
<span style="font-family:var(--brand-font-family);font-size:22px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 5 · 18px</span>
<span style="font-family:var(--brand-font-family);font-size:18px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Large · 18px</span>
<span style="font-family:var(--brand-font-family);font-size:18px;font-weight:inherit;line-height:var(--brand-line-height);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Body · 14px</span>
<span style="font-family:var(--brand-font-family);font-size:14px;font-weight:inherit;line-height:var(--brand-line-height);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Small · 12px</span>
<span style="font-family:var(--brand-font-family);font-size:12px;font-weight:inherit;line-height:var(--brand-line-height);color:var(--brand-color-text)">The quick brown fox</span>
</div>
</section></div>
</div>
</body>
</html>

587
.design/system/kit.html Normal file
View File

@@ -0,0 +1,587 @@
<!doctype html>
<html lang="en">
<head>
<style data-brand-fonts>
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-300-italic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-400-italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-300.woff2") format("woff2");
font-weight: 300;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/cormorant-garamond-600.woff2") format("woff2");
font-weight: 600;
font-style: normal;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
font-display: swap;
}
@font-face {
font-family: "Tenor Sans";
src: url("../fonts/tenor-sans-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Malenia — component kit</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&amp;display=swap" />
<style>
:root{
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
font-family:var(--brand-font-family);
font-size:var(--brand-font-size);
line-height:var(--brand-line-height);
color:var(--brand-color-text);
background:var(--brand-color-bg-layout);
-webkit-font-smoothing:antialiased;
}
h1,h2,h3{font-family:'Cormorant Garamond', serif, var(--brand-font-family)}
a{color:inherit}
a[role="button"]{transition:filter .15s ease,transform .15s ease}
a[role="button"]:hover{filter:brightness(1.07)}
a[role="button"]:active{filter:brightness(.95);transform:translateY(0.5px)}
input:focus,textarea:focus,select:focus{border-color:var(--brand-color-primary)!important;box-shadow:0 0 0 3px var(--brand-color-primary-bg);outline:none}
details.kit-acc>summary{list-style:none;cursor:pointer}
details.kit-acc>summary::-webkit-details-marker{display:none}
details.kit-acc .kit-acc-icon{transition:transform .18s ease}
details.kit-acc[open] .kit-acc-icon{transform:rotate(45deg)}
.wrap{max-width:960px;margin:0 auto;padding:var(--brand-size-xxl) var(--brand-size-lg)}
.panel{
background:var(--brand-color-bg-container);
border:var(--brand-line-width) solid var(--brand-color-border-secondary);
border-radius:var(--brand-border-radius-lg);
padding:var(--brand-size-xl);
margin-bottom:var(--brand-size-xl);
}
</style>
</head>
<body>
<div class="wrap">
<header style="margin-bottom:var(--brand-size-xxl)">
<div style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);padding:var(--brand-size-xxs) var(--brand-size-sm);border-radius:var(--brand-border-radius);background:var(--brand-color-primary-bg);color:var(--brand-color-primary);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong)">Malenia · default theme</div>
<h1 style="margin:var(--brand-size) 0 var(--brand-size-xxs);font-size:var(--brand-font-size-heading-1);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Malenia — component kit</h1>
<p style="margin:0;max-width:60ch;font-size:var(--brand-font-size-lg);color:var(--brand-color-text-secondary)">Every component below is colored only by <code style="font-family:var(--brand-font-family-code)">var(--brand-*)</code> custom properties derived from one seed.</p>
</header>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Buttons</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Five types across three sizes.</p>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Primary</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border)">Default</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-bg-container);color:var(--brand-color-text);border-color:var(--brand-color-border);border-style:dashed">Dashed</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-text);border-color:transparent">Text</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Link</a>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size)">
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Small</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Medium</a>
<a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-lg);padding:0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Large</a>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Form controls</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Inputs, selects, textareas, checkboxes and switches share control height, radius and focus ring.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:var(--brand-size-lg);max-width:720px">
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Email</label><input type="text" placeholder="you@example.com" style="box-sizing:border-box;width:100%;height:var(--brand-control-height);padding:0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;transition:border-color var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out)"></div>
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Project name</label><input type="text" value="Malenia" style="box-sizing:border-box;width:100%;height:var(--brand-control-height);padding:0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none;transition:border-color var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out)"></div>
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Team size</label><select style="box-sizing:border-box;width:100%;height:var(--brand-control-height);padding:0 var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);outline:none"><option>Just me</option><option selected>2–10</option><option>11–50</option><option>50+</option></select></div>
<div><label style="display:block;margin-bottom:var(--brand-size-xxs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">Message</label><textarea rows="3" placeholder="Tell us what you're building…" style="box-sizing:border-box;display:block;width:100%;padding:var(--brand-size-xs) var(--brand-size-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text);background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border);border-radius:var(--brand-border-radius);resize:vertical;outline:none"></textarea></div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size-lg);gap:var(--brand-size-lg)">
<label style="display:inline-flex;align-items:flex-start;gap:var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);cursor:pointer"><input type="checkbox" checked style="accent-color:var(--brand-color-primary);width:16px;height:16px;margin:3px 0 0;flex:0 0 auto"><span>Email me product updates</span></label>
<label style="display:inline-flex;align-items:flex-start;gap:var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);cursor:pointer"><input type="checkbox" style="accent-color:var(--brand-color-primary);width:16px;height:16px;margin:3px 0 0;flex:0 0 auto"><span>I agree to the terms</span></label>
<span style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);"><span style="display:inline-flex;align-items:center;flex:0 0 auto;width:44px;height:24px;padding:2px;border-radius:12px;box-sizing:border-box;transition:background var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);background:var(--brand-color-primary);justify-content:flex-end"><span style="width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);"></span></span><span style="font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);">Notifications</span></span>
<span style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);"><span style="display:inline-flex;align-items:center;flex:0 0 auto;width:44px;height:24px;padding:2px;border-radius:12px;box-sizing:border-box;transition:background var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);background:var(--brand-color-fill);justify-content:flex-start"><span style="width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);"></span></span><span style="font-family:var(--brand-font-family);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);">Public profile</span></span>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Navigation</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Tabs, breadcrumbs, pagination and steps for app shells and flows.</p>
<div style="display:flex;gap:var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)"><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-primary);font-weight:var(--brand-font-weight-strong);border-bottom:2px solid var(--brand-color-primary)">Overview</span><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-text-secondary);border-bottom:2px solid transparent">Analytics</span><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-text-secondary);border-bottom:2px solid transparent">Reports</span><span style="padding:var(--brand-size-xs) 2px;margin-bottom:calc(-1 * var(--brand-line-width));font-family:var(--brand-font-family);font-size:var(--brand-font-size);cursor:pointer;white-space:nowrap;color:var(--brand-color-text-secondary);border-bottom:2px solid transparent">Settings</span></div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size-lg);justify-content:space-between;gap:var(--brand-size-lg)">
<nav style="display:inline-flex;align-items:center;gap:var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size);"><span style="color:var(--brand-color-text-tertiary);">Malenia</span><span style="color:var(--brand-color-text-quaternary);">/</span><span style="color:var(--brand-color-text-tertiary);">Projects</span><span style="color:var(--brand-color-text-quaternary);">/</span><span style="color:var(--brand-color-text);">Brand system</span></nav>
<div style="display:inline-flex;gap:var(--brand-size-xxs);align-items:center;"><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">&lsaquo;</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">1</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-primary);background:var(--brand-color-primary);color:#fff">2</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">3</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">4</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">5</span><span style="display:inline-flex;align-items:center;justify-content:center;min-width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xxs);border-radius:var(--brand-border-radius-sm);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);cursor:pointer;user-select:none;box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-bg-container);color:var(--brand-color-text-secondary)">&rsaquo;</span></div>
</div>
<div style="margin-top:var(--brand-size-xl);">
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;flex-wrap:wrap;font-family:var(--brand-font-family);"><div style="flex:0 1 auto;min-width:140px;display:flex;gap:var(--brand-size-sm);align-items:flex-start"><span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:28px;height:28px;border-radius:50%;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-primary);background:var(--brand-color-primary);color:#fff">&#10003;</span><div style="min-width:0;padding-top:4px;"><div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-text);line-height:1.3">Connect</div><div style="margin-top:2px;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:var(--brand-line-height)">Point at a source</div></div></div><div style="flex:1 1 var(--brand-size-xl);min-width:var(--brand-size-md);height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin-top:14px"></div><div style="flex:0 1 auto;min-width:140px;display:flex;gap:var(--brand-size-sm);align-items:flex-start"><span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:28px;height:28px;border-radius:50%;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-primary);background:var(--brand-color-primary);color:#fff">2</span><div style="min-width:0;padding-top:4px;"><div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-text);line-height:1.3">Derive</div><div style="margin-top:2px;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:var(--brand-line-height)">Seed to tokens</div></div></div><div style="flex:1 1 var(--brand-size-xl);min-width:var(--brand-size-md);height:var(--brand-line-width);background:var(--brand-color-border-secondary);margin-top:14px"></div><div style="flex:0 1 auto;min-width:140px;display:flex;gap:var(--brand-size-sm);align-items:flex-start"><span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:28px;height:28px;border-radius:50%;font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);box-sizing:border-box;border:var(--brand-line-width) solid var(--brand-color-border);background:var(--brand-color-bg-container);color:var(--brand-color-text-tertiary)">3</span><div style="min-width:0;padding-top:4px;"><div style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size);color:var(--brand-color-text);line-height:1.3">Ship</div><div style="margin-top:2px;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);line-height:var(--brand-line-height)">Export everywhere</div></div></div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Cards</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Elevated content surfaces with optional header &amp; footer.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--brand-size-lg)">
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)">Overview</div><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">Cards group related content on an elevated surface. Radius, border and shadow all read from the derived tokens.</div><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-fill-quaternary)"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">Learn more</a></div></div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)">Usage</div><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">Drop the same component into a dark or compact theme and it re-skins automatically — no per-variant markup.</div></div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">A bodyonly card. <span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">live</span> <span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">beta</span></div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Data display</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Tables, stats, avatars and a no-JS accordion for structured content.</p>
<div style="border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;background:var(--brand-color-bg-container)"><table style="width:100%;border-collapse:collapse;font-family:var(--brand-font-family);"><thead><tr><th style="text-align:left;padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text-secondary);background:var(--brand-color-fill-quaternary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);white-space:nowrap">Token</th><th style="text-align:left;padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text-secondary);background:var(--brand-color-fill-quaternary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);white-space:nowrap">Role</th><th style="text-align:left;padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text-secondary);background:var(--brand-color-fill-quaternary);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);white-space:nowrap">Value</th></tr></thead><tbody><tr><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">colorPrimary</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">Brand accent</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">seed</td></tr><tr><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">fontSize</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">Body type base</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">derived</td></tr><tr><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text)">borderRadius</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text)">Corner rounding</td><td style="padding:var(--brand-size-sm) var(--brand-size-md);font-size:var(--brand-font-size);color:var(--brand-color-text)">derived</td></tr></tbody></table></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--brand-size-lg);margin-top:var(--brand-size-xl)">
<div style="font-family:var(--brand-font-family);min-width:0">
<div style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:var(--brand-color-text);letter-spacing:-0.01em">10k+</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Teams onboard</div>
</div>
<div style="font-family:var(--brand-font-family);min-width:0">
<div style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:var(--brand-color-text);letter-spacing:-0.01em">99.9%</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Uptime SLA</div>
</div>
<div style="font-family:var(--brand-font-family);min-width:0">
<div style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);line-height:1.1;color:var(--brand-color-text);letter-spacing:-0.01em">4.9/5</div>
<div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Average rating</div>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center;margin-top:var(--brand-size-xl)">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height-lg);height:var(--brand-control-height-lg);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">M</span>
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">AL</span>
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height-sm);height:var(--brand-control-height-sm);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">TB</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">design</span> <span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-success);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">ops</span>
</div>
<div style="margin-top:var(--brand-size-xl);">
<div><details class="kit-acc" open style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">What is a seed token?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">The ~20-field input every other token is derived from. Change one field and the whole system follows.</div>
</details>
<details class="kit-acc" style="border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:var(--brand-size-md);padding:var(--brand-size-md) 0;font-family:var(--brand-font-family);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)"><span style="min-width:0;">Can I theme dark mode separately?</span><span class="kit-acc-icon" style="flex:0 0 auto;color:var(--brand-color-primary);font-size:var(--brand-font-size-xl);line-height:1;font-weight:400">+</span></summary>
<div style="padding:0 0 var(--brand-size-md);font-family:var(--brand-font-family);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);max-width:70ch">No need — dark is the same seed run through a different algorithm, so it can never drift from the light theme.</div>
</details></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Marketing blocks</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Testimonials and pricing compose the primitives — landing pages are these in the right order.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--brand-size-lg)">
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1"><p style="margin:0 0 var(--brand-size-lg);font-size:var(--brand-font-size-lg);line-height:1.6;color:var(--brand-color-text)">&ldquo;We replaced three hand-maintained style guides with one generated system.&rdquo;</p>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);">
<span style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:var(--brand-control-height);height:var(--brand-control-height);border-radius:50%;background:var(--brand-color-primary-bg);color:var(--brand-color-primary-text);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.02em;user-select:none">AL</span>
<div style="min-width:0;">
<div style="font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);font-size:var(--brand-font-size);">Ada Lovelace</div>
<div style="font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">Design lead</div>
</div>
</div></div></div>
<div style="display:flex;flex-direction:column;position:relative;background:var(--brand-color-bg-container);border:2px solid var(--brand-color-primary);border-radius:var(--brand-border-radius-lg);padding:var(--brand-size-xl);font-family:var(--brand-font-family);box-shadow:0 12px 32px var(--brand-color-primary-bg)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--brand-size-sm);margin-bottom:var(--brand-size-xs);">
<span style="font-weight:var(--brand-font-weight-strong);font-size:var(--brand-font-size-lg);color:var(--brand-color-text);">Pro</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Most popular</span>
</div>
<p style="margin:0 0 var(--brand-size-md);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary);">For growing teams.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-xxs);margin-bottom:var(--brand-size-lg);">
<span style="font-size:var(--brand-font-size-heading-2);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:1;letter-spacing:-0.01em;">$29</span>
<span style="color:var(--brand-color-text-tertiary);font-size:var(--brand-font-size-sm);">/ mo</span>
</div>
<ul style="list-style:none;margin:0 0 var(--brand-size-xl);padding:0;flex:1;"><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Unlimited brands</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Dark &amp; compact themes</span></li><li style="display:flex;gap:var(--brand-size-xs);align-items:flex-start;padding:var(--brand-size-xxs) 0;font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary)"><span style="color:var(--brand-color-success);font-weight:var(--brand-font-weight-strong);flex:0 0 auto;">&#10003;</span><span>Priority support</span></li></ul>
<div style="display:flex;flex-direction:column;"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height);padding:0 var(--brand-size);font-size:var(--brand-font-size);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:var(--brand-color-primary);color:#fff;border-color:var(--brand-color-primary);box-shadow:0 2px 0 var(--brand-color-primary-bg)">Start free trial</a></div>
</div>
<div style="display:flex;flex-direction:column;background:var(--brand-color-bg-container);border:var(--brand-line-width) solid var(--brand-color-border-secondary);border-radius:var(--brand-border-radius-lg);overflow:hidden;font-family:var(--brand-font-family);color:var(--brand-color-text);box-shadow:0 1px 2px var(--brand-color-fill-quaternary)"><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary);font-size:var(--brand-font-size-lg);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text)">Composable</div><div style="padding:var(--brand-size-lg);font-size:var(--brand-font-size);line-height:var(--brand-line-height);color:var(--brand-color-text-secondary);flex:1">Marketing blocks (testimonials, pricing, stats) compose the same primitives — so a landing page is just kit pieces in the right order.</div><div style="padding:var(--brand-size-md) var(--brand-size-lg);border-top:var(--brand-line-width) solid var(--brand-color-border-secondary);background:var(--brand-color-fill-quaternary)"><a role="button" tabindex="0" style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--brand-font-family);font-weight:var(--brand-font-weight-strong);line-height:1;text-decoration:none;transition:all var(--brand-motion-duration-mid) var(--brand-motion-ease-in-out);height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-size:var(--brand-font-size-sm);border-radius:var(--brand-border-radius);border:var(--brand-line-width) solid transparent;background:transparent;color:var(--brand-color-link);border-color:transparent">View artifacts</a></div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Tags</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Compact labels keyed to functional colors.</p>
<div style="display:flex;flex-wrap:wrap;gap:var(--brand-size);align-items:center">
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-primary);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Primary</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-success);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Success</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-warning);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Warning</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-error);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Error</span>
<span style="display:inline-flex;align-items:center;height:var(--brand-control-height-sm);padding:0 var(--brand-size-xs);font-family:var(--brand-font-family);font-size:var(--brand-font-size-sm);line-height:1;color:var(--brand-color-info);background:var(--brand-color-primary-bg);border:var(--brand-line-width) solid var(--brand-color-primary-border);border-radius:var(--brand-border-radius-sm)">Info</span>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Alerts &amp; progress</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">Semantic feedback states.</p>
<div style="display:flex;flex-direction:column;gap:var(--brand-size)">
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-success-bg);border:var(--brand-line-width) solid var(--brand-color-success-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-success);font-size:var(--brand-font-size-lg);line-height:1">&#10003;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:var(--brand-line-height)">Brand system generated</div><div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);line-height:var(--brand-line-height)">All design tokens were derived from a single seed color.</div></div></div>
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-info-bg);border:var(--brand-line-width) solid var(--brand-color-info-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-info);font-size:var(--brand-font-size-lg);line-height:1">&#8505;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:inherit;color:var(--brand-color-text);line-height:var(--brand-line-height)">Tokens map 1:1 onto CSS custom properties.</div></div></div>
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-warning-bg);border:var(--brand-line-width) solid var(--brand-color-warning-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-warning);font-size:var(--brand-font-size-lg);line-height:1">&#9888;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:var(--brand-font-weight-strong);color:var(--brand-color-text);line-height:var(--brand-line-height)">Wireframe mode is off</div><div style="margin-top:var(--brand-size-xxs);font-size:var(--brand-font-size);color:var(--brand-color-text-secondary);line-height:var(--brand-line-height)">Filled surfaces are in use for this theme.</div></div></div>
<div style="display:flex;gap:var(--brand-size-sm);align-items:flex-start;padding:var(--brand-size-sm) var(--brand-size);font-family:var(--brand-font-family);background:var(--brand-color-error-bg);border:var(--brand-line-width) solid var(--brand-color-error-border);border-radius:var(--brand-border-radius-lg)"><span style="flex:0 0 auto;margin-top:2px;color:var(--brand-color-error);font-size:var(--brand-font-size-lg);line-height:1">&#10005;</span><div style="flex:1"><div style="font-size:var(--brand-font-size);font-weight:inherit;color:var(--brand-color-text);line-height:var(--brand-line-height)">Could not reach the source URL.</div></div></div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--brand-size-sm);margin-top:var(--brand-size-xl);max-width:480px">
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);font-family:var(--brand-font-family);">
<div style="flex:1;height:8px;border-radius:4px;background:var(--brand-color-fill-secondary);overflow:hidden"><div style="width:72%;height:100%;border-radius:4px;background:var(--brand-color-primary);transition:width var(--brand-motion-duration-slow) var(--brand-motion-ease-out)"></div></div>
<span style="flex:0 0 auto;font-size:var(--brand-font-size-sm);color:var(--brand-color-text-secondary)">72%</span>
</div>
<div style="display:flex;align-items:center;gap:var(--brand-size-sm);font-family:var(--brand-font-family);">
<div style="flex:1;height:8px;border-radius:4px;background:var(--brand-color-fill-secondary);overflow:hidden"><div style="width:100%;height:100%;border-radius:4px;background:var(--brand-color-success);transition:width var(--brand-motion-duration-slow) var(--brand-motion-ease-out)"></div></div>
<span style="flex:0 0 auto;font-size:var(--brand-font-size-sm);color:var(--brand-color-success)">&#10003;</span>
</div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Color</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">The full derived palette and neutrals.</p>
<div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Primary palette</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#fffef0"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">50</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#fcfaed"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">200</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#f0eace"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">300</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#e3d69f"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">400</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#d6bf74"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">500</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#c9a84c"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">600</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#a38336"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">700</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#7d5f23"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">800</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#573e14"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">900</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#30210b"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">1050</span>
</div></div>
</div><div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Functional</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#c9a84c"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">primary</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#52c41a"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">success</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#faad14"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">warning</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#ff4d4f"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">error</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#c9a84c"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">info</span>
</div></div>
</div><div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Neutral text</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#35312b"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#68655f"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text/2</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#95928c"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text/3</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#c2bfba"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">text/4</span>
</div></div>
</div><div style="margin-bottom:var(--brand-size-lg)">
<div style="margin-bottom:var(--brand-size-sm);font-size:var(--brand-font-size-sm);font-weight:var(--brand-font-weight-strong);letter-spacing:0.04em;text-transform:uppercase;color:var(--brand-color-text-tertiary)">Surfaces &amp; borders</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--brand-size-sm)"><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#f1eee9"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">layout</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#faf7f2"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">container</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#faf7f2"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">elevated</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#d8d5d0"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">border</span>
</div><div style="display:flex;flex-direction:column;gap:var(--brand-size-xxs)">
<div style="height:48px;border-radius:var(--brand-border-radius-sm);border:var(--brand-line-width) solid var(--brand-color-border-secondary);background:#ede9e4"></div>
<span style="font-size:11px;font-family:var(--brand-font-family-code);color:var(--brand-color-text-tertiary)">border/2</span>
</div></div>
</div>
</section></div>
<div class="panel"><section style="margin-bottom:var(--brand-size-xxl)">
<h2 style="margin:0;font-size:var(--brand-font-size-heading-4);font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">Type scale</h2>
<p style="margin:var(--brand-size-xxs) 0 var(--brand-size-lg);font-size:var(--brand-font-size);color:var(--brand-color-text-tertiary)">A modular ramp derived from the base font size.</p>
<div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 1 · 38px</span>
<span style="font-family:var(--brand-font-family);font-size:38px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 2 · 32px</span>
<span style="font-family:var(--brand-font-family);font-size:32px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 3 · 26px</span>
<span style="font-family:var(--brand-font-family);font-size:26px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 4 · 22px</span>
<span style="font-family:var(--brand-font-family);font-size:22px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Heading 5 · 18px</span>
<span style="font-family:var(--brand-font-family);font-size:18px;font-weight:var(--brand-font-weight-strong);line-height:var(--brand-line-height-heading);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Large · 18px</span>
<span style="font-family:var(--brand-font-family);font-size:18px;font-weight:inherit;line-height:var(--brand-line-height);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Body · 14px</span>
<span style="font-family:var(--brand-font-family);font-size:14px;font-weight:inherit;line-height:var(--brand-line-height);color:var(--brand-color-text)">The quick brown fox</span>
</div><div style="display:flex;align-items:baseline;gap:var(--brand-size-lg);padding:var(--brand-size-sm) 0;border-bottom:var(--brand-line-width) solid var(--brand-color-border-secondary)">
<span style="flex:0 0 96px;font-family:var(--brand-font-family-code);font-size:var(--brand-font-size-sm);color:var(--brand-color-text-tertiary)">Small · 12px</span>
<span style="font-family:var(--brand-font-family);font-size:12px;font-weight:inherit;line-height:var(--brand-line-height);color:var(--brand-color-text)">The quick brown fox</span>
</div>
</section></div>
</div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const target = process.argv[2];
if (!target) {
console.error('Usage: node scripts/apply-design-tokens.mjs <target-css-path>');
process.exit(1);
}
const here = path.dirname(fileURLToPath(import.meta.url));
const source = path.resolve(here, '..', 'variables.css');
const destination = path.resolve(process.cwd(), target);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
console.log(`Copied design tokens to ${destination}`);

22
.design/system/seed.json Normal file
View File

@@ -0,0 +1,22 @@
{
"colorPrimary": "#c9a84c",
"colorSuccess": "#52c41a",
"colorWarning": "#faad14",
"colorError": "#ff4d4f",
"colorInfo": "#c9a84c",
"colorLink": "",
"colorTextBase": "#1a1610",
"colorBgBase": "#faf7f2",
"fontFamily": "'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
"fontFamilyCode": "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
"fontSize": 14,
"borderRadius": 8,
"sizeUnit": 4,
"sizeStep": 4,
"controlHeight": 32,
"lineWidth": 1,
"motionUnit": 0.1,
"motionBase": 0,
"wireframe": false,
"motion": true
}

24
.design/system/theme.json Normal file
View File

@@ -0,0 +1,24 @@
{
"token": {
"colorPrimary": "#c9a84c",
"colorSuccess": "#52c41a",
"colorWarning": "#faad14",
"colorError": "#ff4d4f",
"colorInfo": "#c9a84c",
"colorTextBase": "#1a1610",
"colorBgBase": "#faf7f2",
"fontFamily": "'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
"fontFamilyCode": "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
"fontSize": 14,
"borderRadius": 8,
"sizeUnit": 4,
"sizeStep": 4,
"controlHeight": 32,
"lineWidth": 1,
"motionUnit": 0.1,
"motionBase": 0,
"wireframe": false,
"motion": true
},
"algorithm": "default"
}

View File

@@ -0,0 +1,251 @@
{
"algorithm": "compact",
"colorPrimary": "#c9a84c",
"colorPrimaryBg": "#fffef0",
"colorPrimaryBgHover": "#fcfaed",
"colorPrimaryBorder": "#f0eace",
"colorPrimaryBorderHover": "#e3d69f",
"colorPrimaryHover": "#d6bf74",
"colorPrimaryActive": "#a38336",
"colorPrimaryText": "#573e14",
"colorSuccess": "#52c41a",
"colorSuccessBg": "#f6ffed",
"colorSuccessBorder": "#b6eb8f",
"colorSuccessHover": "#72d13d",
"colorSuccessActive": "#389e0d",
"colorWarning": "#faad14",
"colorWarningBg": "#fffbe6",
"colorWarningBorder": "#ffe58f",
"colorWarningHover": "#ffc53d",
"colorWarningActive": "#d48806",
"colorError": "#ff4d4f",
"colorErrorBg": "#fff2f0",
"colorErrorBorder": "#ffccc7",
"colorErrorHover": "#ff7875",
"colorErrorActive": "#d9363d",
"colorInfo": "#c9a84c",
"colorInfoBg": "#fffef0",
"colorInfoBorder": "#f0eace",
"colorLink": "#c9a84c",
"colorLinkHover": "#d6bf74",
"colorLinkActive": "#a38336",
"colorText": "#35312b",
"colorTextSecondary": "#68655f",
"colorTextTertiary": "#95928c",
"colorTextQuaternary": "#c2bfba",
"colorFill": "#d8d5d0",
"colorFillSecondary": "#ede9e4",
"colorFillTertiary": "#f1eee9",
"colorFillQuaternary": "#f6f3ed",
"colorBgLayout": "#f1eee9",
"colorBgContainer": "#faf7f2",
"colorBgElevated": "#faf7f2",
"colorBorder": "#d8d5d0",
"colorBorderSecondary": "#ede9e4",
"fontFamily": "'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
"fontFamilyCode": "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
"fontSize": 14,
"fontSizeSM": 12,
"fontSizeLG": 18,
"fontSizeXL": 22,
"fontSizeHeading1": 38,
"fontSizeHeading2": 32,
"fontSizeHeading3": 26,
"fontSizeHeading4": 22,
"fontSizeHeading5": 18,
"lineHeight": 1.5714285714285714,
"lineHeightHeading": 1.25,
"fontWeightStrong": 600,
"sizeXXS": 4,
"sizeXS": 8,
"sizeSM": 8,
"size": 12,
"sizeMD": 16,
"sizeLG": 16,
"sizeXL": 24,
"sizeXXL": 32,
"borderRadiusXS": 3,
"borderRadiusSM": 5,
"borderRadius": 8,
"borderRadiusLG": 10,
"lineWidth": 1,
"controlHeightXS": 14,
"controlHeightSM": 21,
"controlHeight": 28,
"controlHeightLG": 35,
"motionDurationFast": "0.1s",
"motionDurationMid": "0.2s",
"motionDurationSlow": "0.3s",
"motionEaseInOut": "cubic-bezier(0.645, 0.045, 0.355, 1)",
"motionEaseOut": "cubic-bezier(0.215, 0.61, 0.355, 1)",
"primaryPalette": [
"#fffef0",
"#fcfaed",
"#f0eace",
"#e3d69f",
"#d6bf74",
"#c9a84c",
"#a38336",
"#7d5f23",
"#573e14",
"#30210b"
],
"presets": {
"red": [
"#fff1f0",
"#ffcbc7",
"#ffa39e",
"#ff7775",
"#ff4d50",
"#f5222d",
"#cf1323",
"#a8071a",
"#820014",
"#5c0011"
],
"volcano": [
"#fff2e8",
"#ffd8bf",
"#ffbb96",
"#ff9c6e",
"#ff7a45",
"#fa541c",
"#d4380d",
"#ad2202",
"#871500",
"#610c00"
],
"orange": [
"#fff7e6",
"#ffe7ba",
"#ffd591",
"#ffc069",
"#ffa940",
"#fa8c16",
"#d46b08",
"#ad4e00",
"#873800",
"#612500"
],
"gold": [
"#fffbe6",
"#fff1b8",
"#ffe58f",
"#ffd666",
"#ffc53d",
"#faad14",
"#d48806",
"#ad6800",
"#874c00",
"#613400"
],
"yellow": [
"#feffe6",
"#ffffb8",
"#fffb8f",
"#fff566",
"#ffeb3d",
"#fadb14",
"#d4b106",
"#ad8a00",
"#876700",
"#614700"
],
"lime": [
"#fcffe6",
"#f4ffb8",
"#eaff8f",
"#d3f261",
"#bae637",
"#a0d911",
"#7bb305",
"#5b8c00",
"#3f6600",
"#254000"
],
"green": [
"#f6ffed",
"#d9f7be",
"#b6eb8f",
"#94de64",
"#72d13d",
"#52c41a",
"#389e0d",
"#227804",
"#135200",
"#092b00"
],
"cyan": [
"#e6fffb",
"#b5f5ec",
"#87e8de",
"#5cdbd3",
"#36cfc9",
"#13c2c2",
"#08979c",
"#006d75",
"#00474f",
"#002329"
],
"blue": [
"#e6f4ff",
"#bae0ff",
"#91caff",
"#69b1ff",
"#4096ff",
"#1677ff",
"#0958d9",
"#003eb3",
"#002c8c",
"#001d66"
],
"geekblue": [
"#f0f5ff",
"#d6e4ff",
"#adc6ff",
"#85a5ff",
"#597df7",
"#2f54eb",
"#1d39c4",
"#10229e",
"#061178",
"#030852"
],
"purple": [
"#f9f0ff",
"#efdbff",
"#d4adf7",
"#b37feb",
"#9254de",
"#722ed1",
"#531dab",
"#391085",
"#22075e",
"#120338"
],
"magenta": [
"#fff0f6",
"#ffd6e7",
"#ffadd2",
"#ff85bf",
"#f759aa",
"#eb2f96",
"#c41d7e",
"#9e1067",
"#780650",
"#520339"
],
"grey": [
"#a6a6a6",
"#999999",
"#8c8c8c",
"#808080",
"#737373",
"#666666",
"#404040",
"#1a1a1a",
"#000000",
"#000000"
]
}
}

View File

@@ -0,0 +1,251 @@
{
"algorithm": "dark",
"colorPrimary": "#b9a566",
"colorPrimaryBg": "#292519",
"colorPrimaryBgHover": "#413922",
"colorPrimaryBorder": "#4e4731",
"colorPrimaryBorderHover": "#6b613f",
"colorPrimaryHover": "#cec391",
"colorPrimaryActive": "#928352",
"colorPrimaryText": "#f5f3e6",
"colorSuccess": "#64b537",
"colorSuccessBg": "#192913",
"colorSuccessBorder": "#304d20",
"colorSuccessHover": "#87ca5c",
"colorSuccessActive": "#518f2f",
"colorWarning": "#dcaa37",
"colorWarningBg": "#312512",
"colorWarningBorder": "#5b4920",
"colorWarningHover": "#e8c35e",
"colorWarningActive": "#ad872f",
"colorError": "#dc6966",
"colorErrorBg": "#32191a",
"colorErrorBorder": "#5b3231",
"colorErrorHover": "#e89590",
"colorErrorActive": "#ad5553",
"colorInfo": "#b9a566",
"colorInfoBg": "#292519",
"colorInfoBorder": "#4e4731",
"colorLink": "#b9a566",
"colorLinkHover": "#cec391",
"colorLinkActive": "#928352",
"colorText": "#dcdcdc",
"colorTextSecondary": "#adadad",
"colorTextTertiary": "#7e7e7e",
"colorTextQuaternary": "#4f4f4f",
"colorFill": "#3e3e3e",
"colorFillSecondary": "#303030",
"colorFillTertiary": "#272727",
"colorFillQuaternary": "#1d1d1d",
"colorBgLayout": "#1d1d1d",
"colorBgContainer": "#141414",
"colorBgElevated": "#272727",
"colorBorder": "#3e3e3e",
"colorBorderSecondary": "#303030",
"fontFamily": "'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
"fontFamilyCode": "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
"fontSize": 14,
"fontSizeSM": 12,
"fontSizeLG": 18,
"fontSizeXL": 22,
"fontSizeHeading1": 38,
"fontSizeHeading2": 32,
"fontSizeHeading3": 26,
"fontSizeHeading4": 22,
"fontSizeHeading5": 18,
"lineHeight": 1.5714285714285714,
"lineHeightHeading": 1.25,
"fontWeightStrong": 600,
"sizeXXS": 4,
"sizeXS": 8,
"sizeSM": 12,
"size": 16,
"sizeMD": 20,
"sizeLG": 24,
"sizeXL": 32,
"sizeXXL": 48,
"borderRadiusXS": 3,
"borderRadiusSM": 5,
"borderRadius": 8,
"borderRadiusLG": 10,
"lineWidth": 1,
"controlHeightXS": 16,
"controlHeightSM": 24,
"controlHeight": 32,
"controlHeightLG": 40,
"motionDurationFast": "0.1s",
"motionDurationMid": "0.2s",
"motionDurationSlow": "0.3s",
"motionEaseInOut": "cubic-bezier(0.645, 0.045, 0.355, 1)",
"motionEaseOut": "cubic-bezier(0.215, 0.61, 0.355, 1)",
"primaryPalette": [
"#292519",
"#413922",
"#4e4731",
"#6b613f",
"#928352",
"#b9a566",
"#cec391",
"#e5dfc5",
"#f5f3e6",
"#faf9ec"
],
"presets": {
"red": [
"#fff1f0",
"#ffcbc7",
"#ffa39e",
"#ff7775",
"#ff4d50",
"#f5222d",
"#cf1323",
"#a8071a",
"#820014",
"#5c0011"
],
"volcano": [
"#fff2e8",
"#ffd8bf",
"#ffbb96",
"#ff9c6e",
"#ff7a45",
"#fa541c",
"#d4380d",
"#ad2202",
"#871500",
"#610c00"
],
"orange": [
"#fff7e6",
"#ffe7ba",
"#ffd591",
"#ffc069",
"#ffa940",
"#fa8c16",
"#d46b08",
"#ad4e00",
"#873800",
"#612500"
],
"gold": [
"#fffbe6",
"#fff1b8",
"#ffe58f",
"#ffd666",
"#ffc53d",
"#faad14",
"#d48806",
"#ad6800",
"#874c00",
"#613400"
],
"yellow": [
"#feffe6",
"#ffffb8",
"#fffb8f",
"#fff566",
"#ffeb3d",
"#fadb14",
"#d4b106",
"#ad8a00",
"#876700",
"#614700"
],
"lime": [
"#fcffe6",
"#f4ffb8",
"#eaff8f",
"#d3f261",
"#bae637",
"#a0d911",
"#7bb305",
"#5b8c00",
"#3f6600",
"#254000"
],
"green": [
"#f6ffed",
"#d9f7be",
"#b6eb8f",
"#94de64",
"#72d13d",
"#52c41a",
"#389e0d",
"#227804",
"#135200",
"#092b00"
],
"cyan": [
"#e6fffb",
"#b5f5ec",
"#87e8de",
"#5cdbd3",
"#36cfc9",
"#13c2c2",
"#08979c",
"#006d75",
"#00474f",
"#002329"
],
"blue": [
"#e6f4ff",
"#bae0ff",
"#91caff",
"#69b1ff",
"#4096ff",
"#1677ff",
"#0958d9",
"#003eb3",
"#002c8c",
"#001d66"
],
"geekblue": [
"#f0f5ff",
"#d6e4ff",
"#adc6ff",
"#85a5ff",
"#597df7",
"#2f54eb",
"#1d39c4",
"#10229e",
"#061178",
"#030852"
],
"purple": [
"#f9f0ff",
"#efdbff",
"#d4adf7",
"#b37feb",
"#9254de",
"#722ed1",
"#531dab",
"#391085",
"#22075e",
"#120338"
],
"magenta": [
"#fff0f6",
"#ffd6e7",
"#ffadd2",
"#ff85bf",
"#f759aa",
"#eb2f96",
"#c41d7e",
"#9e1067",
"#780650",
"#520339"
],
"grey": [
"#a6a6a6",
"#999999",
"#8c8c8c",
"#808080",
"#737373",
"#666666",
"#404040",
"#1a1a1a",
"#000000",
"#000000"
]
}
}

View File

@@ -0,0 +1,251 @@
{
"algorithm": "default",
"colorPrimary": "#c9a84c",
"colorPrimaryBg": "#fffef0",
"colorPrimaryBgHover": "#fcfaed",
"colorPrimaryBorder": "#f0eace",
"colorPrimaryBorderHover": "#e3d69f",
"colorPrimaryHover": "#d6bf74",
"colorPrimaryActive": "#a38336",
"colorPrimaryText": "#573e14",
"colorSuccess": "#52c41a",
"colorSuccessBg": "#f6ffed",
"colorSuccessBorder": "#b6eb8f",
"colorSuccessHover": "#72d13d",
"colorSuccessActive": "#389e0d",
"colorWarning": "#faad14",
"colorWarningBg": "#fffbe6",
"colorWarningBorder": "#ffe58f",
"colorWarningHover": "#ffc53d",
"colorWarningActive": "#d48806",
"colorError": "#ff4d4f",
"colorErrorBg": "#fff2f0",
"colorErrorBorder": "#ffccc7",
"colorErrorHover": "#ff7875",
"colorErrorActive": "#d9363d",
"colorInfo": "#c9a84c",
"colorInfoBg": "#fffef0",
"colorInfoBorder": "#f0eace",
"colorLink": "#c9a84c",
"colorLinkHover": "#d6bf74",
"colorLinkActive": "#a38336",
"colorText": "#35312b",
"colorTextSecondary": "#68655f",
"colorTextTertiary": "#95928c",
"colorTextQuaternary": "#c2bfba",
"colorFill": "#d8d5d0",
"colorFillSecondary": "#ede9e4",
"colorFillTertiary": "#f1eee9",
"colorFillQuaternary": "#f6f3ed",
"colorBgLayout": "#f1eee9",
"colorBgContainer": "#faf7f2",
"colorBgElevated": "#faf7f2",
"colorBorder": "#d8d5d0",
"colorBorderSecondary": "#ede9e4",
"fontFamily": "'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
"fontFamilyCode": "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
"fontSize": 14,
"fontSizeSM": 12,
"fontSizeLG": 18,
"fontSizeXL": 22,
"fontSizeHeading1": 38,
"fontSizeHeading2": 32,
"fontSizeHeading3": 26,
"fontSizeHeading4": 22,
"fontSizeHeading5": 18,
"lineHeight": 1.5714285714285714,
"lineHeightHeading": 1.25,
"fontWeightStrong": 600,
"sizeXXS": 4,
"sizeXS": 8,
"sizeSM": 12,
"size": 16,
"sizeMD": 20,
"sizeLG": 24,
"sizeXL": 32,
"sizeXXL": 48,
"borderRadiusXS": 3,
"borderRadiusSM": 5,
"borderRadius": 8,
"borderRadiusLG": 10,
"lineWidth": 1,
"controlHeightXS": 16,
"controlHeightSM": 24,
"controlHeight": 32,
"controlHeightLG": 40,
"motionDurationFast": "0.1s",
"motionDurationMid": "0.2s",
"motionDurationSlow": "0.3s",
"motionEaseInOut": "cubic-bezier(0.645, 0.045, 0.355, 1)",
"motionEaseOut": "cubic-bezier(0.215, 0.61, 0.355, 1)",
"primaryPalette": [
"#fffef0",
"#fcfaed",
"#f0eace",
"#e3d69f",
"#d6bf74",
"#c9a84c",
"#a38336",
"#7d5f23",
"#573e14",
"#30210b"
],
"presets": {
"red": [
"#fff1f0",
"#ffcbc7",
"#ffa39e",
"#ff7775",
"#ff4d50",
"#f5222d",
"#cf1323",
"#a8071a",
"#820014",
"#5c0011"
],
"volcano": [
"#fff2e8",
"#ffd8bf",
"#ffbb96",
"#ff9c6e",
"#ff7a45",
"#fa541c",
"#d4380d",
"#ad2202",
"#871500",
"#610c00"
],
"orange": [
"#fff7e6",
"#ffe7ba",
"#ffd591",
"#ffc069",
"#ffa940",
"#fa8c16",
"#d46b08",
"#ad4e00",
"#873800",
"#612500"
],
"gold": [
"#fffbe6",
"#fff1b8",
"#ffe58f",
"#ffd666",
"#ffc53d",
"#faad14",
"#d48806",
"#ad6800",
"#874c00",
"#613400"
],
"yellow": [
"#feffe6",
"#ffffb8",
"#fffb8f",
"#fff566",
"#ffeb3d",
"#fadb14",
"#d4b106",
"#ad8a00",
"#876700",
"#614700"
],
"lime": [
"#fcffe6",
"#f4ffb8",
"#eaff8f",
"#d3f261",
"#bae637",
"#a0d911",
"#7bb305",
"#5b8c00",
"#3f6600",
"#254000"
],
"green": [
"#f6ffed",
"#d9f7be",
"#b6eb8f",
"#94de64",
"#72d13d",
"#52c41a",
"#389e0d",
"#227804",
"#135200",
"#092b00"
],
"cyan": [
"#e6fffb",
"#b5f5ec",
"#87e8de",
"#5cdbd3",
"#36cfc9",
"#13c2c2",
"#08979c",
"#006d75",
"#00474f",
"#002329"
],
"blue": [
"#e6f4ff",
"#bae0ff",
"#91caff",
"#69b1ff",
"#4096ff",
"#1677ff",
"#0958d9",
"#003eb3",
"#002c8c",
"#001d66"
],
"geekblue": [
"#f0f5ff",
"#d6e4ff",
"#adc6ff",
"#85a5ff",
"#597df7",
"#2f54eb",
"#1d39c4",
"#10229e",
"#061178",
"#030852"
],
"purple": [
"#f9f0ff",
"#efdbff",
"#d4adf7",
"#b37feb",
"#9254de",
"#722ed1",
"#531dab",
"#391085",
"#22075e",
"#120338"
],
"magenta": [
"#fff0f6",
"#ffd6e7",
"#ffadd2",
"#ff85bf",
"#f759aa",
"#eb2f96",
"#c41d7e",
"#9e1067",
"#780650",
"#520339"
],
"grey": [
"#a6a6a6",
"#999999",
"#8c8c8c",
"#808080",
"#737373",
"#666666",
"#404040",
"#1a1a1a",
"#000000",
"#000000"
]
}
}

View File

@@ -0,0 +1,441 @@
:root {
--brand-color-primary: #c9a84c;
--brand-color-primary-bg: #fffef0;
--brand-color-primary-bg-hover: #fcfaed;
--brand-color-primary-border: #f0eace;
--brand-color-primary-border-hover: #e3d69f;
--brand-color-primary-hover: #d6bf74;
--brand-color-primary-active: #a38336;
--brand-color-primary-text: #573e14;
--brand-color-success: #52c41a;
--brand-color-success-bg: #f6ffed;
--brand-color-success-border: #b6eb8f;
--brand-color-success-hover: #72d13d;
--brand-color-success-active: #389e0d;
--brand-color-warning: #faad14;
--brand-color-warning-bg: #fffbe6;
--brand-color-warning-border: #ffe58f;
--brand-color-warning-hover: #ffc53d;
--brand-color-warning-active: #d48806;
--brand-color-error: #ff4d4f;
--brand-color-error-bg: #fff2f0;
--brand-color-error-border: #ffccc7;
--brand-color-error-hover: #ff7875;
--brand-color-error-active: #d9363d;
--brand-color-info: #c9a84c;
--brand-color-info-bg: #fffef0;
--brand-color-info-border: #f0eace;
--brand-color-link: #c9a84c;
--brand-color-link-hover: #d6bf74;
--brand-color-link-active: #a38336;
--brand-color-text: #35312b;
--brand-color-text-secondary: #68655f;
--brand-color-text-tertiary: #95928c;
--brand-color-text-quaternary: #c2bfba;
--brand-color-fill: #d8d5d0;
--brand-color-fill-secondary: #ede9e4;
--brand-color-fill-tertiary: #f1eee9;
--brand-color-fill-quaternary: #f6f3ed;
--brand-color-bg-layout: #f1eee9;
--brand-color-bg-container: #faf7f2;
--brand-color-bg-elevated: #faf7f2;
--brand-color-border: #d8d5d0;
--brand-color-border-secondary: #ede9e4;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #fffef0;
--brand-primary-palette-2: #fcfaed;
--brand-primary-palette-3: #f0eace;
--brand-primary-palette-4: #e3d69f;
--brand-primary-palette-5: #d6bf74;
--brand-primary-palette-6: #c9a84c;
--brand-primary-palette-7: #a38336;
--brand-primary-palette-8: #7d5f23;
--brand-primary-palette-9: #573e14;
--brand-primary-palette-10: #30210b;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}
.dark {
--brand-color-primary: #b9a566;
--brand-color-primary-bg: #292519;
--brand-color-primary-bg-hover: #413922;
--brand-color-primary-border: #4e4731;
--brand-color-primary-border-hover: #6b613f;
--brand-color-primary-hover: #cec391;
--brand-color-primary-active: #928352;
--brand-color-primary-text: #f5f3e6;
--brand-color-success: #64b537;
--brand-color-success-bg: #192913;
--brand-color-success-border: #304d20;
--brand-color-success-hover: #87ca5c;
--brand-color-success-active: #518f2f;
--brand-color-warning: #dcaa37;
--brand-color-warning-bg: #312512;
--brand-color-warning-border: #5b4920;
--brand-color-warning-hover: #e8c35e;
--brand-color-warning-active: #ad872f;
--brand-color-error: #dc6966;
--brand-color-error-bg: #32191a;
--brand-color-error-border: #5b3231;
--brand-color-error-hover: #e89590;
--brand-color-error-active: #ad5553;
--brand-color-info: #b9a566;
--brand-color-info-bg: #292519;
--brand-color-info-border: #4e4731;
--brand-color-link: #b9a566;
--brand-color-link-hover: #cec391;
--brand-color-link-active: #928352;
--brand-color-text: #dcdcdc;
--brand-color-text-secondary: #adadad;
--brand-color-text-tertiary: #7e7e7e;
--brand-color-text-quaternary: #4f4f4f;
--brand-color-fill: #3e3e3e;
--brand-color-fill-secondary: #303030;
--brand-color-fill-tertiary: #272727;
--brand-color-fill-quaternary: #1d1d1d;
--brand-color-bg-layout: #1d1d1d;
--brand-color-bg-container: #141414;
--brand-color-bg-elevated: #272727;
--brand-color-border: #3e3e3e;
--brand-color-border-secondary: #303030;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #292519;
--brand-primary-palette-2: #413922;
--brand-primary-palette-3: #4e4731;
--brand-primary-palette-4: #6b613f;
--brand-primary-palette-5: #928352;
--brand-primary-palette-6: #b9a566;
--brand-primary-palette-7: #cec391;
--brand-primary-palette-8: #e5dfc5;
--brand-primary-palette-9: #f5f3e6;
--brand-primary-palette-10: #faf9ec;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}

View File

@@ -0,0 +1,220 @@
:root {
--brand-color-primary: #b9a566;
--brand-color-primary-bg: #292519;
--brand-color-primary-bg-hover: #413922;
--brand-color-primary-border: #4e4731;
--brand-color-primary-border-hover: #6b613f;
--brand-color-primary-hover: #cec391;
--brand-color-primary-active: #928352;
--brand-color-primary-text: #f5f3e6;
--brand-color-success: #64b537;
--brand-color-success-bg: #192913;
--brand-color-success-border: #304d20;
--brand-color-success-hover: #87ca5c;
--brand-color-success-active: #518f2f;
--brand-color-warning: #dcaa37;
--brand-color-warning-bg: #312512;
--brand-color-warning-border: #5b4920;
--brand-color-warning-hover: #e8c35e;
--brand-color-warning-active: #ad872f;
--brand-color-error: #dc6966;
--brand-color-error-bg: #32191a;
--brand-color-error-border: #5b3231;
--brand-color-error-hover: #e89590;
--brand-color-error-active: #ad5553;
--brand-color-info: #b9a566;
--brand-color-info-bg: #292519;
--brand-color-info-border: #4e4731;
--brand-color-link: #b9a566;
--brand-color-link-hover: #cec391;
--brand-color-link-active: #928352;
--brand-color-text: #dcdcdc;
--brand-color-text-secondary: #adadad;
--brand-color-text-tertiary: #7e7e7e;
--brand-color-text-quaternary: #4f4f4f;
--brand-color-fill: #3e3e3e;
--brand-color-fill-secondary: #303030;
--brand-color-fill-tertiary: #272727;
--brand-color-fill-quaternary: #1d1d1d;
--brand-color-bg-layout: #1d1d1d;
--brand-color-bg-container: #141414;
--brand-color-bg-elevated: #272727;
--brand-color-border: #3e3e3e;
--brand-color-border-secondary: #303030;
--brand-font-family: 'Tenor Sans', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--brand-font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--brand-font-size: 14px;
--brand-font-size-sm: 12px;
--brand-font-size-lg: 18px;
--brand-font-size-xl: 22px;
--brand-font-size-heading-1: 38px;
--brand-font-size-heading-2: 32px;
--brand-font-size-heading-3: 26px;
--brand-font-size-heading-4: 22px;
--brand-font-size-heading-5: 18px;
--brand-line-height: 1.5714285714285714;
--brand-line-height-heading: 1.25;
--brand-font-weight-strong: 600;
--brand-size-xxs: 4px;
--brand-size-xs: 8px;
--brand-size-sm: 12px;
--brand-size: 16px;
--brand-size-md: 20px;
--brand-size-lg: 24px;
--brand-size-xl: 32px;
--brand-size-xxl: 48px;
--brand-border-radius-xs: 3px;
--brand-border-radius-sm: 5px;
--brand-border-radius: 8px;
--brand-border-radius-lg: 10px;
--brand-line-width: 1px;
--brand-control-height-xs: 16px;
--brand-control-height-sm: 24px;
--brand-control-height: 32px;
--brand-control-height-lg: 40px;
--brand-motion-duration-fast: 0.1s;
--brand-motion-duration-mid: 0.2s;
--brand-motion-duration-slow: 0.3s;
--brand-motion-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
--brand-motion-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
--brand-primary-palette-1: #292519;
--brand-primary-palette-2: #413922;
--brand-primary-palette-3: #4e4731;
--brand-primary-palette-4: #6b613f;
--brand-primary-palette-5: #928352;
--brand-primary-palette-6: #b9a566;
--brand-primary-palette-7: #cec391;
--brand-primary-palette-8: #e5dfc5;
--brand-primary-palette-9: #f5f3e6;
--brand-primary-palette-10: #faf9ec;
--brand-presets-red-1: #fff1f0;
--brand-presets-red-2: #ffcbc7;
--brand-presets-red-3: #ffa39e;
--brand-presets-red-4: #ff7775;
--brand-presets-red-5: #ff4d50;
--brand-presets-red-6: #f5222d;
--brand-presets-red-7: #cf1323;
--brand-presets-red-8: #a8071a;
--brand-presets-red-9: #820014;
--brand-presets-red-10: #5c0011;
--brand-presets-volcano-1: #fff2e8;
--brand-presets-volcano-2: #ffd8bf;
--brand-presets-volcano-3: #ffbb96;
--brand-presets-volcano-4: #ff9c6e;
--brand-presets-volcano-5: #ff7a45;
--brand-presets-volcano-6: #fa541c;
--brand-presets-volcano-7: #d4380d;
--brand-presets-volcano-8: #ad2202;
--brand-presets-volcano-9: #871500;
--brand-presets-volcano-10: #610c00;
--brand-presets-orange-1: #fff7e6;
--brand-presets-orange-2: #ffe7ba;
--brand-presets-orange-3: #ffd591;
--brand-presets-orange-4: #ffc069;
--brand-presets-orange-5: #ffa940;
--brand-presets-orange-6: #fa8c16;
--brand-presets-orange-7: #d46b08;
--brand-presets-orange-8: #ad4e00;
--brand-presets-orange-9: #873800;
--brand-presets-orange-10: #612500;
--brand-presets-gold-1: #fffbe6;
--brand-presets-gold-2: #fff1b8;
--brand-presets-gold-3: #ffe58f;
--brand-presets-gold-4: #ffd666;
--brand-presets-gold-5: #ffc53d;
--brand-presets-gold-6: #faad14;
--brand-presets-gold-7: #d48806;
--brand-presets-gold-8: #ad6800;
--brand-presets-gold-9: #874c00;
--brand-presets-gold-10: #613400;
--brand-presets-yellow-1: #feffe6;
--brand-presets-yellow-2: #ffffb8;
--brand-presets-yellow-3: #fffb8f;
--brand-presets-yellow-4: #fff566;
--brand-presets-yellow-5: #ffeb3d;
--brand-presets-yellow-6: #fadb14;
--brand-presets-yellow-7: #d4b106;
--brand-presets-yellow-8: #ad8a00;
--brand-presets-yellow-9: #876700;
--brand-presets-yellow-10: #614700;
--brand-presets-lime-1: #fcffe6;
--brand-presets-lime-2: #f4ffb8;
--brand-presets-lime-3: #eaff8f;
--brand-presets-lime-4: #d3f261;
--brand-presets-lime-5: #bae637;
--brand-presets-lime-6: #a0d911;
--brand-presets-lime-7: #7bb305;
--brand-presets-lime-8: #5b8c00;
--brand-presets-lime-9: #3f6600;
--brand-presets-lime-10: #254000;
--brand-presets-green-1: #f6ffed;
--brand-presets-green-2: #d9f7be;
--brand-presets-green-3: #b6eb8f;
--brand-presets-green-4: #94de64;
--brand-presets-green-5: #72d13d;
--brand-presets-green-6: #52c41a;
--brand-presets-green-7: #389e0d;
--brand-presets-green-8: #227804;
--brand-presets-green-9: #135200;
--brand-presets-green-10: #092b00;
--brand-presets-cyan-1: #e6fffb;
--brand-presets-cyan-2: #b5f5ec;
--brand-presets-cyan-3: #87e8de;
--brand-presets-cyan-4: #5cdbd3;
--brand-presets-cyan-5: #36cfc9;
--brand-presets-cyan-6: #13c2c2;
--brand-presets-cyan-7: #08979c;
--brand-presets-cyan-8: #006d75;
--brand-presets-cyan-9: #00474f;
--brand-presets-cyan-10: #002329;
--brand-presets-blue-1: #e6f4ff;
--brand-presets-blue-2: #bae0ff;
--brand-presets-blue-3: #91caff;
--brand-presets-blue-4: #69b1ff;
--brand-presets-blue-5: #4096ff;
--brand-presets-blue-6: #1677ff;
--brand-presets-blue-7: #0958d9;
--brand-presets-blue-8: #003eb3;
--brand-presets-blue-9: #002c8c;
--brand-presets-blue-10: #001d66;
--brand-presets-geekblue-1: #f0f5ff;
--brand-presets-geekblue-2: #d6e4ff;
--brand-presets-geekblue-3: #adc6ff;
--brand-presets-geekblue-4: #85a5ff;
--brand-presets-geekblue-5: #597df7;
--brand-presets-geekblue-6: #2f54eb;
--brand-presets-geekblue-7: #1d39c4;
--brand-presets-geekblue-8: #10229e;
--brand-presets-geekblue-9: #061178;
--brand-presets-geekblue-10: #030852;
--brand-presets-purple-1: #f9f0ff;
--brand-presets-purple-2: #efdbff;
--brand-presets-purple-3: #d4adf7;
--brand-presets-purple-4: #b37feb;
--brand-presets-purple-5: #9254de;
--brand-presets-purple-6: #722ed1;
--brand-presets-purple-7: #531dab;
--brand-presets-purple-8: #391085;
--brand-presets-purple-9: #22075e;
--brand-presets-purple-10: #120338;
--brand-presets-magenta-1: #fff0f6;
--brand-presets-magenta-2: #ffd6e7;
--brand-presets-magenta-3: #ffadd2;
--brand-presets-magenta-4: #ff85bf;
--brand-presets-magenta-5: #f759aa;
--brand-presets-magenta-6: #eb2f96;
--brand-presets-magenta-7: #c41d7e;
--brand-presets-magenta-8: #9e1067;
--brand-presets-magenta-9: #780650;
--brand-presets-magenta-10: #520339;
--brand-presets-grey-1: #a6a6a6;
--brand-presets-grey-2: #999999;
--brand-presets-grey-3: #8c8c8c;
--brand-presets-grey-4: #808080;
--brand-presets-grey-5: #737373;
--brand-presets-grey-6: #666666;
--brand-presets-grey-7: #404040;
--brand-presets-grey-8: #1a1a1a;
--brand-presets-grey-9: #000000;
--brand-presets-grey-10: #000000;
}

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
dist
*.local
.DS_Store
*.log
.vite
coverage

18
.info/ToV.md Normal file
View File

@@ -0,0 +1,18 @@
# Malenia
## Основная информация
MaleniaVPN (malenia, маления) — маленькая студия из двух, разрабатывающая VPN решения. Наш имидж строится на критичной насмешке над неспособностью цензоров внедрить серьезные меры против свободного доступа в интернет.
## Ценности
Мы ценим честность, и именно поэтому мы ведём максимально честный диалог с нашими пользователями. Инциденты, недочёты, ошибки и совершенствование — мы выносим в свет, так как отказываемся считать, что зрители являются просто потребителями.
Мы ценим надёжность, из-за чего стремимся к максимальной простоте и устойчивости для наших клиентов. Принцип включил и забыл стоит в основе нашей деятельности.
Мы ценим свободу слова, поэтому мы продолжаем работать, даже когда блокировки становятся сложнее и моральная давка становится сильнее.
Для нас это больше чем сервис. Для нас Malenia — это способ борьбы с несправедливыми репрессиями. Мы будем стоять против, не смотря ни на что.
## Сторонний отзыв об имидже компании
Бренд Malenia строится вокруг образа независимой, небольшой, но технически компетентной команды, которая противопоставляет себя крупным игрокам рынка и бюрократическим структурам. В коммуникации постоянно подчеркивается происхождение проекта: «два программиста-самоучки», отсутствие маркетингового отдела, работа руками инженеров, а не продавцов. Центральные ценности бренда — свобода доступа к информации, прозрачность, инженерный подход и практическая польза для пользователя. Malenia продаёт не просто VPN, а идею «возвращения интернета пользователю», где технологии должны работать незаметно и без сложной настройки. Компания стремится выглядеть союзником аудитории, регулярно вовлекает пользователей в принятие решений через голосования, признаёт ошибки публично и демонстрирует открытость даже в кризисных ситуациях. Имидж бренда — это сочетание технического андеграунда, цифрового активизма и честного сервиса без корпоративного лоска.
Tone of Voice построен на разговорном, ироничном и местами дерзком стиле. Malenia не говорит языком пресс-релизов: в текстах много сарказма, мемов, бытовых сравнений и намеренно простых формулировок, которые делают сложные сетевые темы понятными широкой аудитории. При этом за шутками всегда прослеживается позиция эксперта, который готов спорить с конкурентами и публично разбирать их заявления. Отношение к оппонентам критическое и даже насмешливое: компания активно высмеивает маркетинговые манипуляции, драматизацию и попытки играть на страхах пользователей. Однако главным объектом критики становятся не конкретные конкуренты, а некомпетентность, непрозрачность и искусственное нагнетание паники. В отношении к работе прослеживается принцип «сначала решаем проблему, потом рассказываем о ней»: команда демонстрирует готовность быстро адаптироваться к изменениям, экспериментировать, исправлять собственные ошибки и честно признавать просчёты. В результате бренд воспринимается как «свои люди в индустрии» — немного хулиганы, немного инженеры, но прежде всего защитники открытого интернета и интересов своих пользователей.

366
.info/index.html Normal file
View File

@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Malenia — Свобода в сети</title>
<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=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;family=Tenor+Sans&amp;display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>
:root {
--gold: #C9A84C;
--gold-light: #E8CA7A;
--gold-dim: #8B6914;
--cream: #FAF7F2;
--warm-white: #F5F0E8;
--ink: #1A1610;
--ink-soft: #3D3428;
--ink-muted: #7A6E5F;
--border: rgba(201,168,76,0.25);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { cursor: none; }
body {
font-family: 'Tenor Sans', sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
overflow-x: hidden;
cursor: none;
}
#cursor-dot,
#cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity .3s;
}
#cursor-dot {
width: 6px; height: 6px;
background: var(--gold);
border-radius: 50%;
}
#cursor-ring {
width: 32px; height: 32px;
border: 1px solid var(--gold);
border-radius: 50%;
transition: width .25s cubic-bezier(.4,0,.2,1),
height .25s cubic-bezier(.4,0,.2,1),
opacity .3s;
}
body.hovering #cursor-ring {
width: 48px; height: 48px;
border-color: var(--gold-light);
}
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: .035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 180px;
}
.page {
position: relative;
z-index: 1;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
header {
padding: 2.5rem 4rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.logo {
font-family: 'Cormorant Garamond', serif;
font-weight: 300;
font-size: 1.5rem;
letter-spacing: .35em;
text-transform: uppercase;
color: var(--ink);
text-decoration: none;
}
.logo span { color: var(--gold); }
.header-rule {
flex: 1;
height: 1px;
background: var(--border);
margin: 0 2rem;
}
.header-tag {
font-size: .65rem;
letter-spacing: .25em;
text-transform: uppercase;
color: var(--ink-muted);
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6rem 2rem;
gap: 3.5rem;
}
.emblem {
position: relative;
width: 90px;
height: 90px;
opacity: 0;
animation: fadeUp .9s .1s forwards;
}
.emblem svg { width: 100%; height: 100%; }
.quote-block {
max-width: 640px;
opacity: 0;
animation: fadeUp .9s .25s forwards;
}
.quote-text {
font-family: 'Cormorant Garamond', serif;
font-size: clamp(2rem, 5vw, 3.4rem);
font-weight: 300;
line-height: 1.25;
color: var(--ink);
letter-spacing: .01em;
}
.quote-text em {
font-style: italic;
color: var(--gold-dim);
}
.quote-author {
margin-top: 1.25rem;
font-size: .7rem;
letter-spacing: .3em;
text-transform: uppercase;
color: var(--ink-muted);
}
.divider {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 480px;
opacity: 0;
animation: fadeUp .9s .4s forwards;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.divider-diamond {
width: 6px; height: 6px;
background: var(--gold);
transform: rotate(45deg);
flex-shrink: 0;
}
.cta-group {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
justify-content: center;
opacity: 0;
animation: fadeUp .9s .55s forwards;
}
.btn {
position: relative;
display: inline-flex;
align-items: center;
gap: .75rem;
padding: .85rem 2rem;
font-family: 'Tenor Sans', sans-serif;
font-size: .78rem;
letter-spacing: .18em;
text-transform: uppercase;
text-decoration: none;
border-radius: 0;
cursor: none;
transition: color .25s, background .25s, border-color .25s;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
inset: 0;
background: var(--gold);
transform: scaleX(0);
transform-origin: left;
transition: transform .35s cubic-bezier(.4,0,.2,1);
z-index: 0;
}
.btn:hover::after { transform: scaleX(1); }
.btn > * { position: relative; z-index: 1; }
.btn-primary {
background: var(--ink);
color: var(--cream);
border: 1px solid var(--ink);
}
.btn-primary:hover { color: var(--ink); border-color: var(--gold); }
.btn-secondary {
background: transparent;
color: var(--ink);
border: 1px solid var(--gold);
}
.btn-secondary::after { background: var(--ink); }
.btn-secondary:hover { color: var(--cream); }
.btn-icon {
width: 18px; height: 18px;
fill: currentColor;
flex-shrink: 0;
}
footer {
padding: 2rem 4rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-copy {
font-size: .65rem;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--ink-muted);
}
.footer-dot {
width: 4px; height: 4px;
background: var(--gold);
border-radius: 50%;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 640px) {
header { padding: 1.75rem 1.5rem; }
.header-rule { display: none; }
.header-tag { display: none; }
.hero { padding: 4rem 1.5rem; gap: 2.5rem; }
footer { padding: 1.5rem; flex-direction: column; align-items: flex-start; }
.btn { padding: .8rem 1.5rem; font-size: .72rem; }
.cta-group { flex-direction: column; align-items: stretch; }
.btn { justify-content: center; }
}
</style>
</head>
<body>
<!-- Cursor -->
<div id="cursor-dot" style="left: 283px; top: 238px;"></div>
<div id="cursor-ring" style="left: 283px; top: 238px;"></div>
<div class="page">
<!-- Header -->
<header>
<a class="logo" href="#">Mal<span>e</span>nia</a>
<div class="header-rule"></div>
<span class="header-tag">Свобода. Практика. Простота.</span>
</header>
<!-- Hero -->
<main class="hero">
<!-- Emblem -->
<div class="emblem" aria-hidden="true">
<svg viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- outer ring -->
<circle cx="45" cy="45" r="42" stroke="#C9A84C" stroke-width=".8" stroke-dasharray="3 4"></circle>
<!-- inner ring -->
<circle cx="45" cy="45" r="32" stroke="#C9A84C" stroke-width=".5" opacity=".5"></circle>
<!-- diamond -->
<path d="M45 18 L72 45 L45 72 L18 45 Z" stroke="#C9A84C" stroke-width=".9" fill="none"></path>
<!-- center dot -->
<circle cx="45" cy="45" r="3.5" fill="#C9A84C"></circle>
<!-- cardinal marks -->
<line x1="45" y1="3" x2="45" y2="12" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="45" y1="78" x2="45" y2="87" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="3" y1="45" x2="12" y2="45" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="78" y1="45" x2="87" y2="45" stroke="#C9A84C" stroke-width=".8"></line>
</svg>
</div>
<!-- Quote -->
<div class="quote-block">
<p class="quote-text" id="quote-text">Тишина в сети —<br><em>это тоже выбор.</em></p>
<p class="quote-author">malenia.space</p>
</div>
<!-- Divider -->
<div class="divider" aria-hidden="true">
<div class="divider-diamond"></div>
</div>
<!-- CTA Buttons -->
<div class="cta-group">
<a class="btn btn-primary" href="https://t.me/maleniavpn" target="_blank" rel="noopener">
<!-- Telegram icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.265 2.428a1.523 1.523 0 0 0-1.547-.232L1.91 9.9a1.523 1.523 0 0 0 .089 2.844l4.498 1.498 1.815 5.784a.761.761 0 0 0 1.295.3l2.617-2.818 4.898 3.674a1.523 1.523 0 0 0 2.389-.98l3.047-16.762a1.523 1.523 0 0 0-.293-1.012zM9.94 13.88l-.98 3.267-.98-3.143L18.5 6.04z"></path>
</svg>
<span>Канал</span>
</a>
<a class="btn btn-secondary" href="https://t.me/maleniasupportbot" target="_blank" rel="noopener">
<!-- Bot icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-1 1.732V7h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h3V5.732A2 2 0 0 1 10 4a2 2 0 0 1 2-2zm-3 9a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm6 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"></path>
</svg>
<span>Поддержка</span>
</a>
</div>
</main>
<!-- Footer -->
<footer>
<span class="footer-copy">© 2026 Malenia</span>
<div class="footer-dot"></div>
<span class="footer-copy">malenia.space</span>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>

1
.info/openapi.json Normal file

File diff suppressed because one or more lines are too long

9
.prettierrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all",
"semi": true,
"tabWidth": 2,
"bracketSpacing": true,
"arrowParens": "always"
}

921
.ref/account.html Normal file
View File

@@ -0,0 +1,921 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MaleniaVPN · Account</title>
<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=Cormorant+Garamond:wght@400;500;600&family=Tenor+Sans&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #faf7f2;
--surface: #f5f0e8;
--fg: #1a1610;
--muted: #3d3428;
--accent: #c9a84c;
--font-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
--font-body: 'Tenor Sans', 'Helvetica Neue', Arial, sans-serif;
--radius: 8px;
--baseline: 8px;
--hairline: rgba(26, 22, 16, 0.10);
--hairline-strong: rgba(26, 22, 16, 0.18);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
/* Grain overlay */
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9998;
opacity: 0.035;
mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* Custom cursor */
.cursor-dot, .cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity 0.2s;
}
.cursor-dot {
width: 5px; height: 5px;
background: var(--fg);
border-radius: 50%;
}
.cursor-ring {
width: 30px; height: 30px;
border: 1px solid var(--muted);
border-radius: 50%;
opacity: 0.4;
transition: width 0.25s, height 0.25s, border-color 0.25s, opacity 0.25s, transform 0.08s linear;
}
.cursor-ring.is-active {
width: 52px; height: 52px;
border-color: var(--accent);
opacity: 0.9;
}
@media (hover: none), (pointer: coarse) {
.cursor-dot, .cursor-ring { display: none; }
* { cursor: auto !important; }
}
@media (hover: hover) and (pointer: fine) {
body, button, a, [data-cursor] { cursor: none; }
}
/* Layout */
.shell {
max-width: 1080px;
margin: 0 auto;
padding: 64px 32px 96px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 32px;
border-bottom: 1px solid var(--hairline);
}
.logo {
font-family: var(--font-display);
font-size: 22px;
font-weight: 500;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--fg);
}
.logo .e { color: var(--accent); }
.nav {
display: flex;
gap: 28px;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.nav a {
color: inherit;
text-decoration: none;
position: relative;
padding: 4px 0;
transition: color 0.3s;
}
.nav a:hover { color: var(--fg); }
.nav a.is-active::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: -2px;
height: 1px;
background: var(--accent);
}
/* Hero / identity */
.hero {
padding: 56px 0 48px;
text-align: center;
border-bottom: 1px solid var(--hairline);
}
.kicker {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 20px;
}
.hero-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 44px;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--fg);
}
.hero-sub {
margin-top: 14px;
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
}
/* Profile stat strip — graphics-led */
.profile {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-bottom: 1px solid var(--hairline);
}
.stat {
padding: 40px 24px;
border-right: 1px solid var(--hairline);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
min-height: 168px;
justify-content: center;
}
.stat:last-child { border-right: none; }
.stat-icon {
width: 34px; height: 34px;
color: var(--muted);
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s;
}
.stat:hover .stat-icon { color: var(--accent); }
.stat-label {
font-size: 10px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
}
.stat-value {
font-family: var(--font-display);
font-weight: 500;
font-size: 26px;
line-height: 1.1;
color: var(--fg);
letter-spacing: -0.01em;
word-break: break-word;
}
.stat-value.is-mono { font-family: var(--font-body); font-size: 15px; letter-spacing: 0.04em; }
.stat-value.is-empty {
color: var(--muted);
opacity: 0.45;
font-family: var(--font-body);
font-size: 13px;
font-style: italic;
letter-spacing: 0.02em;
}
.stat-value .accent { color: var(--accent); }
/* Subscription access link */
.subscription-link {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 24px;
padding: 20px 24px;
border-bottom: 1px solid var(--hairline);
background: color-mix(in srgb, var(--surface) 86%, var(--accent) 14%);
}
.subscription-emblem {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--accent);
border-radius: 50%;
color: var(--fg);
}
.subscription-emblem svg { width: 18px; height: 18px; }
.subscription-copy { min-width: 0; }
.subscription-label {
margin-bottom: 4px;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
.subscription-url {
display: block;
overflow: hidden;
color: var(--fg);
font-size: 13px;
letter-spacing: 0.02em;
text-overflow: ellipsis;
white-space: nowrap;
}
.subscription-url strong {
color: var(--fg);
font-weight: 400;
}
.copy-link {
appearance: none;
min-height: 44px;
padding: 0 16px;
border: 1px solid var(--fg);
border-radius: var(--radius);
background: var(--fg);
color: var(--bg);
font-family: var(--font-body);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
transition: background 0.25s, color 0.25s, border-color 0.25s, transform 0.15s;
}
.copy-link:hover { background: var(--accent); color: var(--fg); border-color: var(--accent); }
.copy-link:active { transform: translateY(1px); }
.copy-link:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
}
/* Devices section */
.devices-head {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 48px 0 24px;
}
.devices-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 28px;
letter-spacing: -0.01em;
}
.devices-count {
font-size: 11px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
}
.devices-count strong {
color: var(--fg);
font-weight: 400;
font-family: var(--font-display);
font-size: 16px;
margin-right: 4px;
letter-spacing: 0;
}
.device-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1px;
background: var(--hairline);
border: 1px solid var(--hairline);
border-radius: var(--radius);
overflow: hidden;
}
.device {
background: var(--surface);
padding: 28px 28px 24px;
display: flex;
align-items: flex-start;
gap: 22px;
position: relative;
transition: background 0.3s;
}
.device:hover { background: color-mix(in srgb, var(--surface) 80%, var(--fg) 4%); }
.device-glyph {
flex: 0 0 auto;
width: 48px; height: 48px;
border: 1px solid var(--hairline-strong);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
color: var(--fg);
background: var(--bg);
}
.device-glyph svg { width: 22px; height: 22px; }
.device-meta {
flex: 1 1 auto;
min-width: 0;
}
.device-model {
font-family: var(--font-display);
font-weight: 500;
font-size: 20px;
letter-spacing: -0.01em;
line-height: 1.2;
color: var(--fg);
margin-bottom: 6px;
}
.device-tags {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.device-tags .tag-client {
color: var(--fg);
}
.device-tags .sep {
width: 1px;
background: var(--hairline-strong);
align-self: stretch;
}
.device-delete {
flex: 0 0 auto;
appearance: none;
background: transparent;
border: 1px solid var(--hairline-strong);
color: var(--muted);
width: 36px; height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.25s, color 0.25s, background 0.25s, transform 0.25s;
}
.device-delete:hover {
border-color: var(--fg);
color: var(--fg);
background: var(--bg);
}
.device-delete:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
border-color: var(--accent);
}
.device-delete svg { width: 14px; height: 14px; }
/* Empty devices state */
.devices-empty {
border: 1px solid var(--hairline);
border-radius: var(--radius);
background: var(--surface);
padding: 72px 24px;
text-align: center;
}
.devices-empty-glyph {
width: 44px; height: 44px;
margin: 0 auto 20px;
color: var(--muted);
opacity: 0.55;
}
.devices-empty-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 22px;
color: var(--fg);
margin-bottom: 8px;
}
.devices-empty-sub {
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
/* Footer rule */
.foot {
margin-top: 64px;
padding-top: 28px;
border-top: 1px solid var(--hairline);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
/* Verification modal */
.modal-veil {
position: fixed;
inset: 0;
background: rgba(26, 22, 16, 0.32);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
padding: 24px;
}
.modal-veil.is-open {
opacity: 1;
pointer-events: auto;
}
.modal {
background: var(--bg);
border: 1px solid var(--hairline-strong);
border-radius: var(--radius);
max-width: 420px;
width: 100%;
padding: 36px 32px 28px;
text-align: center;
transform: translateY(8px);
transition: transform 0.3s cubic-bezier(.2,.7,.2,1);
box-shadow: 0 24px 60px -20px rgba(26, 22, 16, 0.35);
}
.modal-veil.is-open .modal { transform: translateY(0); }
.modal-glyph {
width: 52px; height: 52px;
margin: 0 auto 20px;
border: 1px solid var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
}
.modal-glyph svg { width: 22px; height: 22px; }
.modal-kicker {
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
.modal-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 24px;
line-height: 1.25;
color: var(--fg);
margin-bottom: 12px;
}
.modal-body {
font-size: 14px;
color: var(--muted);
line-height: 1.55;
margin-bottom: 26px;
}
.modal-body strong {
color: var(--fg);
font-weight: 400;
font-family: var(--font-display);
font-size: 16px;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.btn {
appearance: none;
font-family: var(--font-body);
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 13px 22px;
border-radius: var(--radius);
border: 1px solid var(--hairline-strong);
background: transparent;
color: var(--fg);
transition: background 0.25s, color 0.25s, border-color 0.25s, transform 0.15s;
min-height: 44px;
}
.btn:hover { background: var(--surface); }
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.35);
border-color: var(--accent);
}
.btn-primary {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
.btn-primary:hover {
background: var(--accent);
color: var(--fg);
border-color: var(--accent);
}
.btn-danger:hover {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
.btn:active { transform: translateY(1px); }
/* Fade-up */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 0.7s cubic-bezier(.2,.7,.2,1), transform 0.7s cubic-bezier(.2,.7,.2,1);
}
.reveal.is-in { opacity: 1; transform: translateY(0); }
/* Toast */
.toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translate(-50%, 16px);
background: var(--fg);
color: var(--bg);
padding: 13px 22px;
border-radius: var(--radius);
font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
z-index: 10001;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
box-shadow: 0 12px 30px -10px rgba(26, 22, 16, 0.4);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 860px) {
.profile { grid-template-columns: repeat(2, 1fr); }
.stat:nth-child(2) { border-right: none; }
.stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--hairline); }
.device-grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
.shell { padding: 40px 20px 64px; }
.topbar { padding-bottom: 24px; }
.nav { gap: 18px; }
.nav a { font-size: 10px; }
.hero { padding: 40px 0 36px; }
.hero-title { font-size: 34px; }
.profile { grid-template-columns: 1fr; }
.stat { border-right: none; border-bottom: 1px solid var(--hairline); min-height: 132px; padding: 32px 20px; }
.stat:last-child { border-bottom: none; }
.subscription-link { grid-template-columns: auto minmax(0, 1fr); gap: 16px; padding: 20px; }
.subscription-url { font-size: 12px; }
.copy-link { grid-column: 1 / -1; width: 100%; }
.devices-head { padding: 40px 0 20px; }
.device { padding: 22px 20px; gap: 16px; }
.device-model { font-size: 18px; }
.device-delete { width: 34px; height: 34px; }
.foot { flex-direction: column; gap: 10px; text-align: center; }
.modal { padding: 28px 24px 24px; }
.modal-title { font-size: 21px; }
}
</style>
</head>
<body>
<div class="cursor-ring" aria-hidden="true"></div>
<div class="cursor-dot" aria-hidden="true"></div>
<main class="shell">
<!-- Topbar -->
<header class="topbar" data-od-id="topbar">
<div class="logo" data-od-id="brand">Mal<span class="e">e</span>nia</div>
<nav class="nav" data-od-id="primary-nav">
<a href="#" class="is-active">Account</a>
<a href="#">Devices</a>
<a href="#">Billing</a>
<a href="#">Support</a>
</nav>
</header>
<!-- Hero identity -->
<section class="hero reveal" data-od-id="account-hero">
<div class="kicker">Your account</div>
<h1 class="hero-title">Eleanor Ashworth</h1>
<p class="hero-sub">Member since March 2023 · Plan: Solitude</p>
</section>
<!-- Profile stat strip -->
<section class="profile reveal" data-od-id="profile-stats" aria-label="Account summary">
<!-- Username -->
<div class="stat" data-od-id="stat-username">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<circle cx="12" cy="8" r="4"/>
<path d="M4 20c0-3.6 3.6-6 8-6s8 2.4 8 6"/>
</svg>
</div>
<div class="stat-label">Username</div>
<div class="stat-value">e.ashworth</div>
</div>
<!-- Telegram ID -->
<div class="stat" data-od-id="stat-telegram">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<path d="M21 4L3 11l5 2 2 6 3-4 4 3 4-14z"/>
<path d="M8 13l8-6"/>
</svg>
</div>
<div class="stat-label">Telegram ID</div>
<div class="stat-value is-mono">@eashworth</div>
</div>
<!-- Device count -->
<div class="stat" data-od-id="stat-device-count">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<rect x="3" y="4" width="18" height="12" rx="1.5"/>
<path d="M9 20h6M12 16v4"/>
</svg>
</div>
<div class="stat-label">Devices linked</div>
<div class="stat-value"><span id="device-count">4</span> <span class="accent">/</span> <span style="font-family:var(--font-body); font-size:14px; color:var(--muted); letter-spacing:0.04em">of 6</span></div>
</div>
<!-- Subscription expiry -->
<div class="stat" data-od-id="stat-expiry">
<div class="stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" width="100%" height="100%">
<rect x="3" y="5" width="18" height="16" rx="1.5"/>
<path d="M3 9h18M8 3v4M16 3v4"/>
</svg>
</div>
<div class="stat-label">Renews</div>
<div class="stat-value">14 Mar 2026</div>
</div>
</section>
<section class="subscription-link reveal" data-od-id="subscription-link" aria-label="Subscription link">
<div class="subscription-emblem" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1"/>
<path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1"/>
</svg>
</div>
<div class="subscription-copy">
<div class="subscription-label">Subscription link</div>
<span class="subscription-url" id="subscription-url">https://malenia.network/sub/<strong>mle_7f2a9c</strong></span>
</div>
<button class="copy-link" id="copy-subscription-link" data-od-id="copy-subscription-link" data-cursor type="button">Copy link</button>
</section>
<!-- Devices -->
<section data-od-id="devices-section">
<div class="devices-head reveal">
<h2 class="devices-title">Connected devices</h2>
<div class="devices-count" data-od-id="devices-count"><strong id="devices-count-num">4</strong>active</div>
</div>
<div class="device-grid reveal" data-od-id="device-grid" id="device-grid">
<!-- devices injected by JS -->
</div>
</section>
<!-- Footer -->
<footer class="foot" data-od-id="footer">
<span>MaleniaVPN · Solitude plan</span>
<span>Sign out</span>
</footer>
</main>
<!-- Verification modal -->
<div class="modal-veil" id="modal-veil" data-od-id="delete-verify-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal" role="document">
<div class="modal-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/>
</svg>
</div>
<div class="modal-kicker">Confirm removal</div>
<h3 class="modal-title" id="modal-title">Remove this device?</h3>
<p class="modal-body">Disconnecting <strong id="modal-device-name">—</strong> will end its VPN session immediately. You can reconnect it later from the device.</p>
<div class="modal-actions">
<button class="btn" id="modal-cancel" data-od-id="modal-cancel">Keep it</button>
<button class="btn btn-danger" id="modal-confirm" data-od-id="modal-confirm">Remove</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
(function() {
// ----- Device data -----
const devices = [
{ id: 'd1', os: 'macos', model: 'MacBook Pro 14"', client: 'Malenia 2.4', joined: 'Joined 2d ago' },
{ id: 'd2', os: 'ios', model: 'iPhone 15 Pro', client: 'Malenia 2.4', joined: 'Joined 11d ago' },
{ id: 'd3', os: 'android', model: 'Pixel 8', client: 'OpenVPN', joined: 'Joined 28d ago' },
{ id: 'd4', os: 'windows', model: 'Surface Laptop 5', client: 'Malenia 2.4', joined: 'Joined 3mo ago' }
];
// ----- OS glyphs (monoline SVG) -----
const glyphs = {
macos: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="13" rx="1.5"/><path d="M1 20h22M9 20l1-3h4l1 3"/></svg>',
ios: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="2.5" width="10" height="19" rx="2"/><path d="M11 5.5h2"/></svg>',
android: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 14a7 7 0 0 1 14 0v5H5z"/><path d="M5 14L3 12M19 14l2-2M8 9l-1.5-2M16 9l1.5-2M9 8.5h6"/></svg>',
windows: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5l8-1v8H3zM12 4l9-1v10h-9zM3 13h8v6l-8-1zM12 13h9v8l-9-1z"/></svg>',
linux: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="13" rx="6" ry="7"/><path d="M9 9.5l1 1M15 9.5l-1 1M10 17h4"/></svg>'
};
const osLabels = { macos: 'macOS', ios: 'iOS', android: 'Android', windows: 'Windows', linux: 'Linux' };
// ----- Render -----
const grid = document.getElementById('device-grid');
function renderDevices() {
grid.innerHTML = '';
if (devices.length === 0) {
const empty = document.createElement('div');
empty.className = 'devices-empty';
empty.setAttribute('data-od-id', 'devices-empty');
empty.innerHTML = `
<div class="devices-empty-glyph">${glyphs.macos}</div>
<div class="devices-empty-title">No devices linked</div>
<div class="devices-empty-sub">Install MaleniaVPN on a device to connect it to your account.</div>
`;
grid.appendChild(empty);
} else {
devices.forEach((d, i) => {
const card = document.createElement('article');
card.className = 'device reveal';
card.setAttribute('data-od-id', 'device-' + d.id);
card.innerHTML = `
<div class="device-glyph" aria-hidden="true">${glyphs[d.os] || glyphs.linux}</div>
<div class="device-meta">
<h3 class="device-model">${d.model}</h3>
<div class="device-tags">
<span>${osLabels[d.os] || d.os}</span>
<span class="sep"></span>
<span class="tag-client">${d.client}</span>
<span class="sep"></span>
<span>${d.joined}</span>
</div>
</div>
<button class="device-delete" data-od-id="device-delete-${d.id}" data-cursor aria-label="Remove ${d.model}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>
</button>
`;
grid.appendChild(card);
requestAnimationFrame(() => {
setTimeout(() => card.classList.add('is-in'), 40 * i);
});
});
}
document.getElementById('device-count').textContent = devices.length;
document.getElementById('devices-count-num').textContent = devices.length;
bindDeleteButtons();
}
// ----- Delete flow -----
const veil = document.getElementById('modal-veil');
const modalName = document.getElementById('modal-device-name');
const cancelBtn = document.getElementById('modal-cancel');
const confirmBtn = document.getElementById('modal-confirm');
const toast = document.getElementById('toast');
let pendingId = null;
function bindDeleteButtons() {
grid.querySelectorAll('.device-delete').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-od-id').replace('device-delete-', '');
const d = devices.find(x => x.id === id);
if (!d) return;
pendingId = id;
modalName.textContent = d.model;
openModal();
});
});
}
function openModal() {
veil.classList.add('is-open');
veil.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
setTimeout(() => cancelBtn.focus(), 60);
}
function closeModal() {
veil.classList.remove('is-open');
veil.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
pendingId = null;
}
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('is-show');
setTimeout(() => toast.classList.remove('is-show'), 2200);
}
// ----- Subscription link -----
const subscriptionUrl = document.getElementById('subscription-url');
const copySubscriptionLink = document.getElementById('copy-subscription-link');
copySubscriptionLink.addEventListener('click', async () => {
const link = subscriptionUrl.textContent.trim();
try {
await navigator.clipboard.writeText(link);
showToast('Subscription link copied');
} catch (error) {
const fallback = document.createElement('textarea');
fallback.value = link;
fallback.setAttribute('readonly', '');
fallback.style.position = 'fixed';
fallback.style.opacity = '0';
document.body.appendChild(fallback);
fallback.select();
document.execCommand('copy');
fallback.remove();
showToast('Subscription link copied');
}
});
cancelBtn.addEventListener('click', closeModal);
confirmBtn.addEventListener('click', () => {
if (!pendingId) return;
const idx = devices.findIndex(x => x.id === pendingId);
if (idx > -1) {
const removed = devices.splice(idx, 1)[0];
renderDevices();
showToast(removed.model + ' disconnected');
}
closeModal();
});
veil.addEventListener('click', (e) => { if (e.target === veil) closeModal(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && veil.classList.contains('is-open')) closeModal();
});
// ----- Reveal observer -----
const io = new IntersectionObserver((entries) => {
entries.forEach(en => {
if (en.isIntersecting) {
en.target.classList.add('is-in');
io.unobserve(en.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach((el, i) => {
el.style.transitionDelay = (i * 80) + 'ms';
io.observe(el);
});
// ----- Custom cursor -----
const dot = document.querySelector('.cursor-dot');
const ring = document.querySelector('.cursor-ring');
let mx = 0, my = 0, rx = 0, ry = 0;
const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
if (fine) {
window.addEventListener('mousemove', (e) => {
mx = e.clientX; my = e.clientY;
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
});
function tick() {
rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18;
ring.style.left = rx + 'px'; ring.style.top = ry + 'px';
requestAnimationFrame(tick);
}
tick();
document.addEventListener('mouseover', (e) => {
const t = e.target.closest('a, button, [data-cursor]');
ring.classList.toggle('is-active', !!t);
});
} else {
dot.style.display = 'none'; ring.style.display = 'none';
}
// ----- Init -----
renderDevices();
})();
</script>
</body>
</html>

1212
.ref/calculator.html Normal file

File diff suppressed because it is too large Load Diff

467
.ref/landing.html Normal file
View File

@@ -0,0 +1,467 @@
<html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Malenia — Свобода в сети</title>
<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=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&amp;family=Tenor+Sans&amp;display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>
:root {
--gold: #C9A84C;
--gold-light: #E8CA7A;
--gold-dim: #8B6914;
--cream: #FAF7F2;
--warm-white: #F5F0E8;
--ink: #1A1610;
--ink-soft: #3D3428;
--ink-muted: #7A6E5F;
--border: rgba(201,168,76,0.25);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { cursor: none; }
body {
font-family: 'Tenor Sans', sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
overflow-x: hidden;
cursor: none;
}
/* ── Custom cursor ─────────────────────────────────── */
#cursor-dot,
#cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity .3s;
}
#cursor-dot {
width: 6px; height: 6px;
background: var(--gold);
border-radius: 50%;
}
#cursor-ring {
width: 32px; height: 32px;
border: 1px solid var(--gold);
border-radius: 50%;
transition: width .25s cubic-bezier(.4,0,.2,1),
height .25s cubic-bezier(.4,0,.2,1),
opacity .3s;
}
body.hovering #cursor-ring {
width: 48px; height: 48px;
border-color: var(--gold-light);
}
/* ── Noise overlay ─────────────────────────────────── */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: .035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 180px;
}
/* ── Layout ────────────────────────────────────────── */
.page {
position: relative;
z-index: 1;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
/* ── Header ────────────────────────────────────────── */
header {
padding: 2.5rem 4rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.logo {
font-family: 'Cormorant Garamond', serif;
font-weight: 300;
font-size: 1.5rem;
letter-spacing: .35em;
text-transform: uppercase;
color: var(--ink);
text-decoration: none;
}
.logo span { color: var(--gold); }
.header-rule {
flex: 1;
height: 1px;
background: var(--border);
margin: 0 2rem;
}
.header-tag {
font-size: .65rem;
letter-spacing: .25em;
text-transform: uppercase;
color: var(--ink-muted);
}
/* ── Hero ──────────────────────────────────────────── */
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6rem 2rem;
gap: 3.5rem;
}
.emblem {
position: relative;
width: 90px;
height: 90px;
opacity: 0;
animation: fadeUp .9s .1s forwards;
}
.emblem svg { width: 100%; height: 100%; }
.quote-block {
max-width: 640px;
opacity: 0;
animation: fadeUp .9s .25s forwards;
}
.quote-text {
font-family: 'Cormorant Garamond', serif;
font-size: clamp(2rem, 5vw, 3.4rem);
font-weight: 300;
line-height: 1.25;
color: var(--ink);
letter-spacing: .01em;
}
.quote-text em {
font-style: italic;
color: var(--gold-dim);
}
.quote-author {
margin-top: 1.25rem;
font-size: .7rem;
letter-spacing: .3em;
text-transform: uppercase;
color: var(--ink-muted);
}
/* ── Divider ───────────────────────────────────────── */
.divider {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 480px;
opacity: 0;
animation: fadeUp .9s .4s forwards;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.divider-diamond {
width: 6px; height: 6px;
background: var(--gold);
transform: rotate(45deg);
flex-shrink: 0;
}
/* ── Buttons ───────────────────────────────────────── */
.cta-group {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
justify-content: center;
opacity: 0;
animation: fadeUp .9s .55s forwards;
}
.btn {
position: relative;
display: inline-flex;
align-items: center;
gap: .75rem;
padding: .85rem 2rem;
font-family: 'Tenor Sans', sans-serif;
font-size: .78rem;
letter-spacing: .18em;
text-transform: uppercase;
text-decoration: none;
border-radius: 0;
cursor: none;
transition: color .25s, background .25s, border-color .25s;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
inset: 0;
background: var(--gold);
transform: scaleX(0);
transform-origin: left;
transition: transform .35s cubic-bezier(.4,0,.2,1);
z-index: 0;
}
.btn:hover::after { transform: scaleX(1); }
.btn > * { position: relative; z-index: 1; }
.btn-primary {
background: var(--ink);
color: var(--cream);
border: 1px solid var(--ink);
}
.btn-primary:hover { color: var(--ink); border-color: var(--gold); }
.btn-secondary {
background: transparent;
color: var(--ink);
border: 1px solid var(--gold);
}
.btn-secondary::after { background: var(--ink); }
.btn-secondary:hover { color: var(--cream); }
.btn-icon {
width: 18px; height: 18px;
fill: currentColor;
flex-shrink: 0;
}
/* ── Footer ────────────────────────────────────────── */
footer {
padding: 2rem 4rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-copy {
font-size: .65rem;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--ink-muted);
}
.footer-dot {
width: 4px; height: 4px;
background: var(--gold);
border-radius: 50%;
}
/* ── Animations ────────────────────────────────────── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Responsive ────────────────────────────────────── */
@media (max-width: 640px) {
header { padding: 1.75rem 1.5rem; }
.header-rule { display: none; }
.header-tag { display: none; }
.hero { padding: 4rem 1.5rem; gap: 2.5rem; }
footer { padding: 1.5rem; flex-direction: column; align-items: flex-start; }
.btn { padding: .8rem 1.5rem; font-size: .72rem; }
.cta-group { flex-direction: column; align-items: stretch; }
.btn { justify-content: center; }
}
</style>
</head>
<body>
<!-- Cursor -->
<div id="cursor-dot" style="left: 233px; top: 539px;"></div>
<div id="cursor-ring" style="left: 233px; top: 539px;"></div>
<div class="page">
<!-- Header -->
<header>
<a class="logo" href="#">Mal<span>e</span>nia</a>
<div class="header-rule"></div>
<span class="header-tag">Свобода. Практика. Простота.</span>
</header>
<!-- Hero -->
<main class="hero">
<!-- Emblem -->
<div class="emblem" aria-hidden="true">
<svg viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- outer ring -->
<circle cx="45" cy="45" r="42" stroke="#C9A84C" stroke-width=".8" stroke-dasharray="3 4"></circle>
<!-- inner ring -->
<circle cx="45" cy="45" r="32" stroke="#C9A84C" stroke-width=".5" opacity=".5"></circle>
<!-- diamond -->
<path d="M45 18 L72 45 L45 72 L18 45 Z" stroke="#C9A84C" stroke-width=".9" fill="none"></path>
<!-- center dot -->
<circle cx="45" cy="45" r="3.5" fill="#C9A84C"></circle>
<!-- cardinal marks -->
<line x1="45" y1="3" x2="45" y2="12" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="45" y1="78" x2="45" y2="87" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="3" y1="45" x2="12" y2="45" stroke="#C9A84C" stroke-width=".8"></line>
<line x1="78" y1="45" x2="87" y2="45" stroke="#C9A84C" stroke-width=".8"></line>
</svg>
</div>
<!-- Quote -->
<div class="quote-block">
<p class="quote-text" id="quote-text">Верните себе<br><em>свободный интернет.</em></p>
<p class="quote-author">malenia.space</p>
</div>
<!-- Divider -->
<div class="divider" aria-hidden="true">
<div class="divider-diamond"></div>
</div>
<!-- CTA Buttons -->
<div class="cta-group">
<a class="btn btn-primary" href="https://t.me/maleniavpn" target="_blank" rel="noopener">
<!-- Telegram icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.265 2.428a1.523 1.523 0 0 0-1.547-.232L1.91 9.9a1.523 1.523 0 0 0 .089 2.844l4.498 1.498 1.815 5.784a.761.761 0 0 0 1.295.3l2.617-2.818 4.898 3.674a1.523 1.523 0 0 0 2.389-.98l3.047-16.762a1.523 1.523 0 0 0-.293-1.012zM9.94 13.88l-.98 3.267-.98-3.143L18.5 6.04z"></path>
</svg>
<span>Канал</span>
</a>
<a class="btn btn-secondary" href="https://t.me/maleniasupportbot" target="_blank" rel="noopener">
<!-- Bot icon -->
<svg class="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-1 1.732V7h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h3V5.732A2 2 0 0 1 10 4a2 2 0 0 1 2-2zm-3 9a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm6 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"></path>
</svg>
<span>Поддержка</span>
</a>
</div>
</main>
<!-- Footer -->
<footer>
<span class="footer-copy">© 2026 Malenia</span>
<div class="footer-dot"></div>
<span class="footer-copy">malenia.space</span>
</footer>
</div>
<script>
/* ── Quotes ─────────────────────────────────────────── */
const quotes = [
"Верните себе\nсвободный интернет.",
"Свобода должна\nбыть доступной.",
"Интернет без границ —\nне привилегия, а норма.",
"Ваш трафик.\nВаши правила.",
"Тишина в сети —\nэто тоже выбор.",
"Открытый мир\nначинается здесь.",
"Без цензуры.\nБез компромиссов.",
"Простота —\nлучшая форма свободы.",
"Один клик\nмежду вами и миром.",
"Malenia —\nсвобода в форме привычки.",
"Будьте везде.\nОставайтесь невидимым.",
"Приватность — это право,\nа не роскошь.",
"Интернет таким,\nкаким он задумывался.",
"Ваши данные —\nтолько ваше дело.",
"Закрытых дверей\nбольше не существует.",
"Любой сайт.\nИз любой точки мира.",
"Оставьте след в истории,\nа не в браузере.",
"Malenia —\nключ к открытой сети.",
"Фильтры придумали они.\nРешение выбрали вы.",
"Безопасность, которую\nвы не замечаете.",
"Сотрите цифровые границы.\nОстаньтесь собой.",
"Скорость без компромиссов.\nСеть без преград.",
"Включили и забыли.\nВы в безопасности.",
"Блокировки иллюзорны.\nСвобода реальна.",
"Никакой слежки.\nТолько вы и интернет.",
"Доступ к информации —\nваше базовое право.",
"Malenia.\nИнтернет снова ваш.",
"Контроль над сетью\nв ваших руках.",
"Невидимый щит\nдля вашей свободы.",
"Весь мир на экране.\nБез лишних вопросов.",
"Никогда не поздно\nвыбрать свободу.",
"Привычка к запретам —\nповод проснуться.",
"Откройте глаза.\nСтен больше нет.",
"Вспомните, как выглядит\nнастоящая сеть.",
"Запреты стали нормой.\nПора это менять.",
"Свобода начинается\nс вашего решения.",
"Выход из спящего режима.\nСделайте первый шаг.",
"Лучший момент для\nсвободы — сейчас.",
"Иллюзия границ рушится\nв один клик.",
"Хватит мириться с рамками.\nМир шире, чем кажется.",
"Не ждите свободы.\nВключите её.",
"Цифровая амнезия окончена.\nВерните своё.",
"Осознанность начинается\nтам, где кончается цензура.",
"Первый шаг к свободе —\nперестать верить в стены.",
"Malenia.\nВаше цифровое пробуждение."
];
function renderQuote(raw) {
// italic the second line if two lines exist
const lines = raw.split('\n');
if (lines.length === 2) {
return `${lines[0]}<br><em>${lines[1]}</em>`;
}
return raw;
}
const el = document.getElementById('quote-text');
const chosen = quotes[Math.floor(Math.random() * quotes.length)];
el.innerHTML = renderQuote(chosen);
/* ── Custom cursor ──────────────────────────────────── */
const dot = document.getElementById('cursor-dot');
const ring = document.getElementById('cursor-ring');
let mx = -100, my = -100, rx = -100, ry = -100;
document.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
(function loop() {
rx += (mx - rx) * .12;
ry += (my - ry) * .12;
dot.style.left = mx + 'px';
dot.style.top = my + 'px';
ring.style.left = rx + 'px';
ring.style.top = ry + 'px';
requestAnimationFrame(loop);
})();
document.querySelectorAll('a, button').forEach(el => {
el.addEventListener('mouseenter', () => document.body.classList.add('hovering'));
el.addEventListener('mouseleave', () => document.body.classList.remove('hovering'));
});
document.addEventListener('mouseleave', () => {
dot.style.opacity = '0';
ring.style.opacity = '0';
});
document.addEventListener('mouseenter', () => {
dot.style.opacity = '1';
ring.style.opacity = '1';
});
</script>
</body></html>

561
.ref/login.html Normal file
View File

@@ -0,0 +1,561 @@
<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MaleniaVPN · Sign In</title>
<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=Cormorant+Garamond:wght@400;500;600&amp;family=Tenor+Sans&amp;display=swap" rel="stylesheet">
<style>
:root {
--bg: #faf7f2;
--surface: #f5f0e8;
--fg: #1a1610;
--muted: #3d3428;
--accent: #c9a84c;
--font-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
--font-body: 'Tenor Sans', 'Helvetica Neue', Arial, sans-serif;
--radius: 8px;
--baseline: 8px;
--hairline: rgba(26, 22, 16, 0.10);
--hairline-strong: rgba(26, 22, 16, 0.18);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
/* Grain overlay */
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9998;
opacity: 0.035;
mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* Custom cursor */
.cursor-dot, .cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity 0.2s;
}
.cursor-dot {
width: 5px; height: 5px;
background: var(--fg);
border-radius: 50%;
}
.cursor-ring {
width: 30px; height: 30px;
border: 1px solid var(--muted);
border-radius: 50%;
opacity: 0.4;
transition: width 0.25s, height 0.25s, border-color 0.25s, opacity 0.25s, transform 0.08s linear;
}
.cursor-ring.is-active {
width: 52px; height: 52px;
border-color: var(--accent);
opacity: 0.9;
}
@media (hover: none), (pointer: coarse) {
.cursor-dot, .cursor-ring { display: none; }
* { cursor: auto !important; }
}
@media (hover: hover) and (pointer: fine) {
body, button, a, input, [data-cursor] { cursor: none; }
}
/* Shell */
.shell {
max-width: 460px;
margin: 0 auto;
padding: 56px 32px 48px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Topbar — logo + kicker anchored to the top, together */
.topbar {
text-align: center;
}
.topbar .kicker {
margin-bottom: 0;
}
.logo {
font-family: var(--font-display);
font-size: 22px;
font-weight: 500;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--fg);
text-decoration: none;
display: inline-block;
}
.logo .e { color: var(--accent); }
/* Emblem — geometric mark above the form */
.emblem {
display: flex;
justify-content: center;
margin-bottom: 28px;
color: var(--accent);
}
.emblem svg { width: 44px; height: 44px; display: block; }
/* Card — centered in remaining space */
.card {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
}
.kicker {
text-align: center;
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 18px;
}
.title {
font-family: var(--font-display);
font-weight: 500;
font-size: 36px;
line-height: 1.2;
letter-spacing: -0.01em;
text-align: center;
color: var(--fg);
margin-bottom: 8px;
}
.sub {
text-align: center;
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
margin-bottom: 40px;
}
/* Form */
.form {
display: flex;
flex-direction: column;
gap: 22px;
}
.field {
position: relative;
}
.field-label {
display: block;
font-size: 10px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
.field-input {
width: 100%;
background: transparent;
border: none;
border-bottom: 1px solid var(--hairline-strong);
padding: 8px 36px 12px 0;
font-family: var(--font-body);
font-size: 16px;
color: var(--fg);
letter-spacing: 0.02em;
transition: border-color 0.3s;
outline: none;
}
.field-input::placeholder {
color: var(--muted);
opacity: 0.45;
font-style: italic;
}
.field-input:focus {
border-color: var(--fg);
}
.field-input:focus + .field-line {
transform: scaleX(1);
}
.field-line {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 1px;
background: var(--accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
pointer-events: none;
}
.field-icon {
position: absolute;
right: 0;
bottom: 12px;
color: var(--muted);
opacity: 0.5;
display: flex;
align-items: center;
transition: opacity 0.25s, color 0.25s;
background: none;
border: none;
padding: 0;
}
.field-icon svg { width: 18px; height: 18px; display: block; }
.field-icon:hover { opacity: 0.9; color: var(--fg); }
.field-icon:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
border-radius: 4px;
}
/* Row: remember + forgot */
.row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
font-size: 13px;
}
.check {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--muted);
cursor: pointer;
user-select: none;
}
.check input {
appearance: none;
-webkit-appearance: none;
width: 16px; height: 16px;
border: 1px solid var(--hairline-strong);
border-radius: 3px;
background: transparent;
position: relative;
transition: border-color 0.2s, background 0.2s;
cursor: pointer;
flex-shrink: 0;
}
.check input:checked {
background: var(--fg);
border-color: var(--fg);
}
.check input:checked::after {
content: "";
position: absolute;
left: 4px; top: 1px;
width: 5px; height: 9px;
border: solid var(--bg);
border-width: 0 1.5px 1.5px 0;
transform: rotate(45deg);
}
.check input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.link {
color: var(--muted);
text-decoration: none;
letter-spacing: 0.02em;
border-bottom: 1px solid transparent;
transition: color 0.25s, border-color 0.25s;
}
.link:hover { color: var(--fg); border-bottom-color: var(--hairline-strong); }
.link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
border-radius: 2px;
}
/* Primary action */
.btn {
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 16px 28px;
border-radius: var(--radius);
border: 1px solid var(--fg);
background: var(--fg);
color: var(--bg);
cursor: pointer;
transition: background 0.25s, color 0.25s, border-color 0.25s, transform 0.08s;
margin-top: 14px;
}
.btn:hover {
background: var(--muted);
border-color: var(--muted);
}
.btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.btn:active { transform: translateY(1px); }
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* Divider */
.divider {
display: flex;
align-items: center;
gap: 14px;
margin: 32px 0 22px;
color: var(--muted);
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--hairline);
}
/* Secondary — telegram */
.btn-ghost {
width: 100%;
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.16em;
text-transform: uppercase;
padding: 15px 28px;
border-radius: var(--radius);
border: 1px solid var(--hairline-strong);
background: transparent;
color: var(--fg);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: border-color 0.25s, background 0.25s, transform 0.08s;
}
.btn-ghost:hover {
border-color: var(--fg);
background: var(--surface);
}
.btn-ghost:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.btn-ghost:active { transform: translateY(1px); }
.btn-ghost svg { width: 16px; height: 16px; }
/* Sign-up hint */
.signup {
text-align: center;
margin-top: 36px;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
.signup .link { color: var(--fg); }
/* Error / status */
.status {
text-align: center;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--muted);
margin-top: 16px;
opacity: 0;
transition: opacity 0.25s;
}
.status:empty { display: none; }
.status.is-error { color: var(--fg); opacity: 1; }
.status.is-info { opacity: 1; }
/* Footer */
.foot {
text-align: center;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
opacity: 0.6;
padding-top: 40px;
}
/* Reveal animation */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.is-in {
opacity: 1;
transform: none;
}
/* Responsive */
@media (max-width: 480px) {
.shell { padding: 40px 22px 36px; }
.title { font-size: 30px; }
}
</style>
</head>
<body>
<div class="cursor-dot"></div>
<div class="cursor-ring"></div>
<div class="shell">
<header class="topbar reveal" data-od-id="topbar">
<a href="malenia-vpn-account.html" class="logo" data-od-id="logo" aria-label="MaleniaVPN home">
Mal<span class="e">e</span>nia
</a>
<div class="kicker">Private access</div>
</header>
<main class="card">
<h1 class="title reveal" data-od-id="login-title">Sign in</h1>
<p class="sub reveal">Enter your credentials to resume your session.</p>
<form class="form reveal" data-od-id="login-form" novalidate="">
<div class="field" data-od-id="field-username">
<label class="field-label" for="username">Username</label>
<input class="field-input" id="username" name="username" type="text" autocomplete="username" placeholder="your handle" spellcheck="false" required="">
<span class="field-line"></span>
</div>
<div class="field" data-od-id="field-password">
<label class="field-label" for="password">Password</label>
<input class="field-input" id="password" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required="">
<span class="field-line"></span>
<button type="button" class="field-icon" id="toggle-pass" data-od-id="toggle-password" aria-label="Show password" aria-pressed="false">
<svg id="eye-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<div class="row" data-od-id="login-options">
<label class="check">
<input type="checkbox" id="remember" name="remember">
<span>Keep me signed in</span>
</label>
<a href="#" class="link" data-od-id="forgot-password">Forgot password?</a>
</div>
<button type="submit" class="btn" data-od-id="btn-signin">Sign in</button>
<div class="status" id="status" role="status" aria-live="polite"></div>
</form>
<p class="signup reveal" data-od-id="signup-hint">
No account yet? <a href="#" class="link" data-od-id="signup-link">Create one</a>
</p>
</main>
<footer class="foot reveal" data-od-id="footer">
Protected by MaleniaVPN
</footer>
</div>
<script>
(function () {
// ----- Password visibility toggle -----
const toggle = document.getElementById('toggle-pass');
const pass = document.getElementById('password');
const eye = document.getElementById('eye-icon');
toggle.addEventListener('click', () => {
const show = pass.type === 'password';
pass.type = show ? 'text' : 'password';
toggle.setAttribute('aria-pressed', String(show));
toggle.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eye.innerHTML = show
? '<path d="M3 3l18 18M10.5 10.7a3 3 0 0 0 4 4M9.4 5.2A10.6 10.6 0 0 1 12 5c6.5 0 10 7 10 7a17.3 17.3 0 0 1-3.2 4.1M6.1 6.2A17.3 17.3 0 0 0 2 12s3.5 7 10 7c1.4 0 2.7-.3 3.8-.7"/>'
: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" />';
});
// ----- Form submit (demo) -----
const form = document.querySelector('.form');
const status = document.getElementById('status');
const btn = form.querySelector('.btn');
form.addEventListener('submit', (e) => {
e.preventDefault();
const u = document.getElementById('username').value.trim();
const p = document.getElementById('password').value;
status.classList.remove('is-error', 'is-info');
if (!u || !p) {
status.textContent = 'Username and password are required.';
status.classList.add('is-error');
return;
}
status.textContent = 'Verifying…';
status.classList.add('is-info');
btn.disabled = true;
setTimeout(() => {
btn.disabled = false;
status.classList.remove('is-info');
status.textContent = 'Unable to reach server. Check your connection.';
status.classList.add('is-error');
}, 1400);
});
// ----- Reveal observer -----
const io = new IntersectionObserver((entries) => {
entries.forEach((en) => {
if (en.isIntersecting) {
en.target.classList.add('is-in');
io.unobserve(en.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach((el, i) => {
el.style.transitionDelay = (i * 70) + 'ms';
io.observe(el);
});
// ----- Custom cursor -----
const dot = document.querySelector('.cursor-dot');
const ring = document.querySelector('.cursor-ring');
let mx = 0, my = 0, rx = 0, ry = 0;
const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
if (fine) {
window.addEventListener('mousemove', (e) => {
mx = e.clientX; my = e.clientY;
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
});
function tick() {
rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18;
ring.style.left = rx + 'px'; ring.style.top = ry + 'px';
requestAnimationFrame(tick);
}
tick();
document.addEventListener('mouseover', (e) => {
const t = e.target.closest('a, button, input, [data-cursor]');
ring.classList.toggle('is-active', !!t);
});
} else {
dot.style.display = 'none'; ring.style.display = 'none';
}
})();
</script>
</body></html>

423
AGENTS.md Normal file
View File

@@ -0,0 +1,423 @@
# MaleniaVPN — Frontend Agent Guidance
## Project Overview
MaleniaVPN (malenia) is a VPN purchase service built around a **pay-for-device** model:
one flat rate per device ("seat"), no tiers. The frontend is a **Russian-language** web
app living in `frontend/` on top of a FastAPI backend. This file is the canonical
guidance for AI assistants working on it.
The service is run by a two-person studio. The brand is minimalist and archival
(cream/ink/gold, extreme whitespace), but the *voice* is honest, ironic and sometimes
snarky. Keep the UI elegant; keep the copy human.
### Screens
| Screen | Route | Access | Source of truth |
|---|---|---|---|
| Landing | `/` | Public | `.ref/landing.html` (existing hosted page) |
| Plan calculator | `/plan` | Public (auth needed only to confirm an order) | `.ref/calculator.html` |
| Login / Signup | `/login` | Public | `.ref/login.html` |
| Account | `/account` | Protected (auth required) | `.ref/account.html` |
## Authoritative Source Directories (read-only)
Never edit these; they are reference material.
| Path | What it is |
| --- | --- |
| `.info/openapi.json` | **The** backend API contract. All types below mirror it. |
| `.info/ToV.md` | Company values and tone of voice (Russian). |
| `.design/DESIGN.md`, `.design/brand.json` | Visual identity: palette, typography, layout posture. |
| `.design/fonts/` | Self-hosted `.woff2` + `fonts.css`. **Latin-only subsets — see Typography.** |
| `.design/logos/` | `favicon-0.ico` — the favicon. |
| `.ref/*.html` | Ready-made page references: full markup, styles and behaviors. Refactor into React, do not copy verbatim. |
The `.design/system/` folder (tokens, `kit.html`, artifacts) is brand-system material;
at page build time use the **canonical tokens below** rather than the `--brand-*`
derived tokens.
## Tech Stack
- Vite + React 19 + TypeScript (`"strict": true`)
- React Router v6
- Zustand — **auth tokens and UI state only**, never server-fetched data
- Native `fetch` API client (no axios)
- Vanilla CSS with custom properties (no Tailwind, no CSS-in-JS, no CSS frameworks)
- Vitest + React Testing Library for tests
- ESLint (typescript/react/hooks) + Prettier (print width 100, single quotes, trailing commas, semicolons)
## Design System
### Canonical CSS tokens (`src/styles/tokens.css`)
| Token | Value | Usage |
| --- | --- | --- |
| `--bg` | `#faf7f2` | Page background (cream) |
| `--surface` | `#f5f0e8` | Cards, raised blocks (warm white) |
| `--fg` | `#1a1610` | Primary text, primary buttons (ink) |
| `--muted` | `#3d3428` | Secondary text (ink-soft) |
| `--fg-muted` | `#7a6e5f` | Tertiary text, captions (ink-muted) |
| `--accent` | `#c9a84c` | Highlights, active states, emblems (gold) |
| `--accent-light` | `#e8ca7a` | Hover glow, gold-light |
| `--accent-dim` | `#8b6914` | Emphasis inside quote text |
| `--hairline` | `rgba(26, 22, 16, 0.10)` | Faint structural rules, grid gaps |
| `--hairline-strong` | `rgba(26, 22, 16, 0.18)` | Input underlines, card borders |
| `--border` | `rgba(201, 168, 76, 0.25)` | Landing header/footer rules |
| `--font-display` | `'Cormorant Garamond', Georgia, serif` | Headings, numerals |
| `--font-body` | `'Tenor Sans', 'Helvetica Neue', Arial, sans-serif` | Body, labels, buttons |
| `--radius` | `8px` | All corners |
| `--baseline` | `8px` | Spacing unit |
### Layout rules
- **8px baseline grid** for spacing and padding; generous padding everywhere.
- **Extreme whitespace + centered alignment**. One primary purpose per screen.
- **1px hairlines** for structural division (horizontal rules, stat strips divided by
grid `gap: 1px` over a hairline background).
- Buttons: uppercase, letterspacing `0.16–0.22em`, 10px font-size, `--radius` corners.
Primary = `--fg` background / `--bg` text; hover → `--accent`. Secondary = transparent,
`1px solid var(--hairline-strong)`.
- Inputs: underline style (`border-bottom: 1px solid var(--hairline-strong)`),
transparent background, gold animated focus line (`scaleX`), no boxed inputs.
### Signature UI details (all screens)
- **Custom cursor**: gold dot (6px) + trailing ring (32px) that grows to 48px over
interactive elements. Hide on touch devices (`@media (hover: none), (pointer: coarse)`)
— these also need `* { cursor: auto !important; }`.
- **Grain overlay**: fixed `body::before` with the fractal-noise SVG data-URI, `opacity: 0.035`, `mix-blend-mode: multiply`.
- **Fade-up reveals**: `.reveal { opacity: 0; transform: translateY(14px); }` → `.is-in`;
staggered `transition-delay` via IntersectionObserver.
- **Logo**: `Mal<span class="e">e</span>nia` — Cormorant Garamond, uppercase,
`letter-spacing: 0.35em`; the `e` is `var(--accent)`.
- **Emblem**: the 90×90 geometric SVG (ring + diamond + cardinal marks), gold stroke.
- Respect `prefers-reduced-motion`: disable cursor animation, grain is fine (static),
use plain opacity transitions without translate.
### Typography
- **Display/headings:** Cormorant Garamond (weights 400, 500, 600 + italics). Large
sizes with `line-height 1.1–1.25` and slight negative `letter-spacing`.
- **Body:** Tenor Sans.
- **CRITICAL — Cyrillic fonts:** the self-hosted `.design/fonts/*.woff2` subsets have
`unicode-range` for **Latin Extended only** (no `U+0400–04FF`) and will render Russian
text with fallback glyphs. The interface is Russian, so load fonts from **Google Fonts**
exactly as the `.ref` pages do:
`https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Tenor+Sans&display=swap`
(with `preconnect` links). Self-hosting is only acceptable after adding Cyrillic subsets.
## Brand Voice & Copy (from `.info/ToV.md`)
- All interface copy is **Russian**. No placeholders left in English.
- Tone: restrained, archival and minimal on the surface; honest, ironic, occasionally
snarky underneath. Plain wording, no corporate clichés, no hype ("revolutionize",
"unleash the power", "best-in-class" are banned).
- Service attitude: "включил и забыл" (set-and-forget), transparency about incidents,
engineering-first, anti-censorship.
- Tagline: **«Свобода. Практика. Простота.»**
- Landing quote block: rotate randomly through the Russian quotes array from
`.ref/landing.html`; render the second line as an italic `<em>`.
## Routing (`src/App.tsx`)
| Path | Element | Notes |
| --- | --- | --- |
| `/` | `Landing` | Existing marketing page |
| `/login` | `LoginPage` | Credentials only. `?returnTo=` preserved for post-auth redirect |
| `/plan` | `PlanPage` | Calculator + confirm → checkout |
| `/account` | `AccountPage` | Protected: redirect to `/login` when unauthenticated |
## Backend API (mirrors `.info/openapi.json` — the only source of truth)
Base: backend routes have **no `/api/` prefix**. In dev, `vite.config.ts` proxies
`/auth`, `/plans`, `/orders`, `/users`, `/payments` to `http://127.0.0.1:8000`.
In prod the reverse proxy serves same-origin. Never hardcode absolute URLs.
### Types (`src/types.ts`)
```ts
type Provider = 'credentials' | 'telegram' | 'api';
interface UserInfo {
username: string | null;
telegram_id: string | null;
referal_code: string;
}
interface LoginResponse { // POST /auth/login
access_token: string;
refresh_token: string;
expires_at: number; // epoch seconds
user: UserInfo;
}
interface Tokens { // POST /auth/refresh
access_token: string;
refresh_token: string;
expires_at: number;
}
interface AddonData { // inside GET /plans/
id: string;
name: string;
price: number;
free_threshold: number;
}
interface PricingPlans { // GET /plans/
device_price: number;
addons: AddonData[];
}
interface OrderDetails { // POST /orders/checkout body
devices: number;
addons: string[]; // addon ids
duration_days: number;
}
interface CheckoutResponse { // POST /orders/checkout → 201
order_id: string;
total_amount: number;
bonus_paid: number;
amount_to_pay: number;
payment_link: string | null;
}
interface Device { // GET /users/subscription/hwid
os: string | null;
model: string | null;
client: string | null;
hwid: string;
}
```
### Endpoint table
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | `/auth/signup` | – | Register, body `{provider, username?, password?, telegram_id?, referal_code?}` |
| POST | `/auth/login` | – | Login, body `{provider:'credentials', username, password}` |
| POST | `/auth/refresh?refresh_token=&iss=` | – | New token pair; `iss` is the provider |
| GET | `/plans/` | – | Pricing: `device_price` + addons |
| POST | `/orders/checkout` | yes | Create order → `payment_link` |
| GET | `/users/me` | yes | `UserInfo` |
| GET | `/users/subscription` | yes | Subscription state (schema is `{}` — shape to be confirmed at implementation) |
| GET | `/users/subscription/hwid` | yes | `Device[]` |
| DELETE | `/users/subscription/hwid?hwid=` | yes | Unlink a device |
| POST | `/payments/pally/result` | – | Payment callback — backend-internal, frontend never calls it |
### Auth flow
- **Credentials only.** Sign up / sign in are username + password. Telegram auth is
explicitly **out of scope** for now (linking ships later). The API *accepts*
`provider: telegram`, but the frontend must only use `credentials`.
- Tokens stored via a `useLocalStorage`-style hook under keys
`malenia.access`, `malenia.refresh`, `malenia.expires_at` (epoch seconds).
- The API client injects `Authorization: Bearer <access>` on every request.
- On **401**: try `POST /auth/refresh?refresh_token=…&iss=credentials` once; on success
retry the original request, on failure clear tokens and redirect to `/login`.
Guard against refresh loops.
- **Signup** may include `referal_code` — expose an optional field on the signup form
and pass it through.
### API client conventions
- Generic `request<T>()` helper; throws `Error` with the backend `detail` for non-2xx.
- 201 responses return parsed JSON; no 204s in this API.
- Errors surface as **toast notifications** (inline errors only for form validation).
## Pages — implementation notes (from `.ref/`)
### Landing (`/`) — `.ref/landing.html`
Keep the overall structure and design. **Apply the existing page's corrections:**
- **Header** gains navigation links (`<nav>`): e.g. «Оформить» → `/plan`,
«Канал» → https://t.me/maleniavpn (external), «Поддержка» → https://t.me/maleniasupportbot
(external). Logo links to `/`. Keep the hairline rule + tagline layout.
- **CTA buttons** (hero): primary **«Оформить»** → `/plan`; secondary **«Канал»** →
Telegram channel (external link, keep the Telegram icon). Support link may live in
the header/footer.
- Keep: emblem, rotating quote block, divider-diamond, footer, fade-up sequence.
- This is the **existing hosted page** — preserve its copy and content, only wire the
corrected navigation/CTAs.
### Login / Signup (`/login`) — `.ref/login.html`
- Single page with sign-in and sign-up modes (toggle), or cleanly split states.
- **No Telegram button and no "Forgot password?" link** (no such endpoint exists;
credentials-only auth). Remove the ghost-telegram affordance entirely.
- Sign-in: `provider: 'credentials'`, username, password, show/hide password toggle,
"remember" checkbox (drives refresh persistence).
- Sign-up: username, password, optional `referal_code`.
- On success → `/account` (or `returnTo`). Show auth errors via the page's status
line plus toast.
### Plan calculator (`/plan`) — `.ref/calculator.html`
Keep the visual language: readout, rhombus device slider (range 3–12, drag + keyboard +
pointer capture), ticks, duration grid, breakdown, addon list, CTA. **Rewrite the pricing
logic — the `.ref` has known bugs and placeholder numbers.**
#### Rewritten pricing (authoritative)
1. Load pricing from `GET /plans/` on mount (`usePlans` hook). Do **not** hardcode.
2. Device cost is linear: `deviceCost = device_price * devices`. No per-seat declining
curves, no volume multipliers.
3. Addon cost is the sum of selected addons' `price`.
4. Duration: options are **1 / 3 / 6 / 12 months** mapped to `duration_days` as
`30 / 90 / 180 / 365`. The `.ref` discount multipliers (`mult 0.95/0.90/0.80`) are
**design-only placeholders** — the backend data has no discount field, so do not
invent one. If a "выгода" badge is shown, it must be derived from real backend data
only; otherwise drop it.
5. `free_threshold` on an addon: present in the backend model with unspecified
semantics — **ask the backend owner before wiring any rule**. Until confirmed, render
addons at their flat `price` and do not guess.
6. Total display = `deviceCost + addonCost`. Show the monthly and the full-order amount
(`total * months`) in the breakdown.
#### Bug notes (do not reproduce)
- `.ref` referenced DOM nodes (`#bd-total`, `#bd-period-row`) that were missing from the
HTML, crashing `render()` for multi-month durations.
- Pricing `priceFor()` declining per-seat curve contradicts backend `device_price`.
- The addons array was a hardcoded placeholder.
#### Checkout flow
- CTA «Оформить» → if no token, redirect to `/login?returnTo=/plan` (preserve the
chosen config in state/URL). If authed → `POST /orders/checkout`
`{ devices, addons, duration_days }`.
- On `201`: redirect the user to `payment_link` (when non-null) in the same tab;
fall back to a "order created" confirmation state showing `order_id` /
`amount_to_pay` when the link is null.
- Show `amount_to_pay` prominently since `bonus_paid` may reduce it below
`total_amount`.
### Account (`/account`) — `.ref/account.html`
- **Hero**: «Ваш аккаунт» + username (Cormorant, serif), sub-line from subscription data.
- **Stat strip** (4 columns, `gap: 1px` hairline grid; 2 cols tablet, 1 col mobile):
Username, Telegram ID (`@…` or empty state), Devices linked (`n / limit`), Renews
/ subscription expiry. Data from `GET /users/me` + `GET /users/subscription`.
- **Devices grid** from `GET /users/subscription/hwid`. Card: OS glyph (monoline SVGs —
macos/ios/android/windows/linux from `.ref`), model, tags (OS label · client · joined),
delete button.
- **Delete flow**: confirmation modal («Удалить это устройство?», shows device model),
then `DELETE /users/subscription/hwid?hwid=…`, toast on success, grid re-rendered.
Empty state: «Нет подключённых устройств» + instructions.
- **Sign out** in the footer: clear tokens → `/`.
- Toast: fixed bottom-center, `--fg` background, `--bg` text, auto-dismiss ~2.2s.
## State Management (Zustand)
```
Store shape {
auth: {
access: string | null;
refresh: string | null;
expiresAt: number | null;
user: UserInfo | null;
setSession(tokens, user), updateUser(user), clear();
}
}
```
- Server data (plans, devices, subscription) is fetched in **hooks inside components** —
never stored in the store.
- Persist tokens to localStorage; hydrate on boot; empty store = unauthenticated.
## Project Structure
```
frontend/
├── AGENTS.md
├── .design/ .info/ .ref/ # read-only reference
├── index.html # mount + Google Fonts links + favicon
├── public/
│ ├── favicon.ico # from .design/logos/favicon-0.ico
│ └── fonts/ # optional self-hosted (latin-only — see above)
├── src/
│ ├── main.tsx
│ ├── App.tsx # Router, auth guard
│ ├── types.ts # mirrors openapi (above)
│ ├── api/client.ts # fetch wrapper + token injection + refresh
│ ├── hooks/
│ │ ├── useLocalStorage.ts
│ │ ├── useAuth.ts
│ │ ├── usePlans.ts
│ │ └── useDevices.ts
│ ├── store/useStore.ts
│ ├── lib/format.ts # money/pluralization helpers
│ ├── components/
│ │ ├── Cursor.tsx # dot + ring
│ │ ├── Emblem.tsx / Logo.tsx
│ │ ├── Reveal.tsx
│ │ ├── TopBar.tsx # landing header w/ nav + CTA links
│ │ ├── Shell.tsx # app topbar + content (plan/account)
│ │ ├── DeviceSlider.tsx
│ │ ├── DurationSelect.tsx
│ │ ├── AddonList.tsx
│ │ ├── Breakdown.tsx
│ │ ├── StatStrip.tsx
│ │ ├── DeviceCard.tsx
│ │ ├── Modal.tsx / Toast.tsx
│ │ └── Skeleton.tsx
│ ├── pages/
│ │ ├── Landing.tsx
│ │ ├── LoginPage.tsx
│ │ ├── PlanPage.tsx
│ │ └── AccountPage.tsx
│ └── styles/
│ ├── tokens.css # canonical tokens (table above)
│ ├── base.css # reset, grain overlay, cursor, reveal
│ ├── components.css
│ └── pages.css
```
## Conventions
- TypeScript strict; all props/state/responses explicitly typed; no `any`; narrow with
type guards from `unknown`.
- No business logic in components — extract pricing/formatting into `lib/` or hooks.
- Form submit buttons show a disabled "Сохранение…" state while pending.
- Loading → `<Skeleton />` matching the content shape.
- Errors → toast (auto-dismiss ~4s). Critical auth errors redirect to `/login`.
- Semantics: `<nav>`, `<main>`, `<section>`, `<article>`, `aria-label` on the slider and
icon-only buttons; keyboard support already in `.ref` (arrows/Home/End, Escape closes modal).
- Interface copy must be in Russian (see Voice above).
## Testing (Vitest + React Testing Library)
- `api/client.ts` — request construction, auth header, refresh-on-401, error mapping.
- Pricing logic (`lib`) — device × price, addon sums, duration→days mapping, totals.
- `LoginPage` — sign-in/sign-up submit + validation, redirect after auth.
- `PlanPage` — slider/duration/addon state, checkout payload, payment_link redirect.
- `AccountPage` — device render, empty state, delete → confirm modal → DELETE call → toast.
- Mock at the `api/client.ts` level, not per-call.
## Anti-Patterns
- No server-fetched data in Zustand. No business logic in components/routers.
- No Tailwind/Bootstrap/CSS frameworks. No inline `<style>` in React components.
- No hardcoded prices, no per-seat discount curves, no invented promo multipliers.
- No English UI strings left in the app.
- Don't edit `.design/`, `.info/`, `.ref/` — reference only.
- Don't call `/payments/pally/result` from the frontend.
## Key Files Reference
| File | Purpose |
|---|---|
| `AGENTS.md` | This file |
| `.info/openapi.json` | Backend API contract |
| `.info/ToV.md` | Voice & tone |
| `.design/DESIGN.md`, `brand.json` | Brand identity |
| `.ref/landing.html` | Landing page reference + quotes |
| `.ref/login.html` | Auth page reference |
| `.ref/calculator.html` | Plan/calculator reference (pricing logic is buggy — rewrite per above) |
| `.ref/account.html` | Account page reference |
| `src/api/client.ts` | Central API client |
| `src/types.ts` | Backend-mirroring types |
| `src/styles/tokens.css` | Design tokens |

27
eslint.config.js Normal file
View File

@@ -0,0 +1,27 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist', '.ref', '.design', '.info', 'node_modules'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2022,
globals: { ...globals.browser, ...globals.jest },
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
);

19
index.html Normal file
View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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=Cormorant+Garamond:wght@400;500;600&family=Tenor+Sans&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>Malenia — Свобода в сети</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

7763
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "malenia-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^6.28.0",
"zustand": "^5.0.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^25.0.1",
"prettier": "^3.4.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.66.0",
"vite": "^6.0.5",
"vitest": "^2.1.8"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

44
src/App.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { useEffect } from 'react';
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { Cursor } from './components/Cursor';
import { Toast } from './components/Toast';
import { useAuthStore } from './store/useStore';
import { Landing } from './pages/Landing';
import { LoginPage } from './pages/LoginPage';
import { PlanPage } from './pages/PlanPage';
import { AccountPage } from './pages/AccountPage';
function ProtectedRoute() {
const access = useAuthStore((s) => s.access);
const expiresAt = useAuthStore((s) => s.expiresAt);
const location = useLocation();
if (!access) {
return <Navigate to="/login" replace state={{ returnTo: location.pathname }} />;
}
if (expiresAt !== null && expiresAt * 1000 < Date.now()) {
return <Navigate to="/login" replace state={{ returnTo: location.pathname }} />;
}
return <AccountPage />;
}
export default function App() {
const hydrate = useAuthStore((s) => s.hydrate);
useEffect(() => {
hydrate();
}, [hydrate]);
return (
<BrowserRouter>
<Cursor />
<Toast />
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/plan" element={<PlanPage />} />
<Route path="/account" element={<ProtectedRoute />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
);
}

146
src/api/client.test.ts Normal file
View File

@@ -0,0 +1,146 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { api, ApiError, setStoredTokens, clearStoredTokens } from './client';
import { useAuthStore } from '../store/useStore';
const ACCESS = 'access-token';
const REFRESH = 'refresh-token';
type FetchMock = ReturnType<typeof vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>>;
function mockFetch(
impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
): FetchMock {
const fn = vi.fn(impl);
globalThis.fetch = fn as typeof fetch;
return fn;
}
function urlOf(input: RequestInfo | URL): string {
if (typeof input === 'string') return input;
if (input instanceof URL) return input.href;
return input.url;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
describe('api client', () => {
beforeEach(() => {
clearStoredTokens();
localStorage.clear();
useAuthStore.setState({
access: null,
refresh: null,
expiresAt: null,
user: null,
remember: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('injects Authorization header when token is stored', async () => {
setStoredTokens({ access_token: ACCESS, refresh_token: REFRESH, expires_at: 9999999999 });
const fetchMock = mockFetch(async () => jsonResponse({ ok: true }));
await api.getMe();
const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
expect(init?.headers).toMatchObject({ Authorization: `Bearer ${ACCESS}` });
});
it('does not inject header on auth:false calls', async () => {
const fetchMock = mockFetch(async () =>
jsonResponse({ device_price: 100, addons: [] }),
);
await api.getPlans();
const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
expect(init?.headers).not.toHaveProperty('Authorization');
});
it('serialises body as JSON for POST', async () => {
const fetchMock = mockFetch(async (_input, init) => {
expect(init?.body).toBe(JSON.stringify({ provider: 'credentials', username: 'u', password: 'p' }));
return jsonResponse({
access_token: 'a',
refresh_token: 'r',
expires_at: 1,
user: { username: 'u', telegram_id: null, referal_code: 'x' },
});
});
await api.login({ provider: 'credentials', username: 'u', password: 'p' });
expect(fetchMock).toHaveBeenCalledOnce();
});
it('throws ApiError with backend detail on non-2xx', async () => {
mockFetch(async () => jsonResponse({ detail: 'bad credentials' }, 401));
await expect(
api.login({ provider: 'credentials', username: 'u', password: 'p' }),
).rejects.toMatchObject({ message: 'bad credentials', status: 401 });
});
it('maps validation error array to a joined message', async () => {
mockFetch(async () =>
jsonResponse(
{ detail: [{ msg: 'field required' }, { msg: 'too short' }] },
422,
),
);
await expect(api.getPlans()).rejects.toBeInstanceOf(ApiError);
});
it('refreshes once on 401 then retries the original request', async () => {
setStoredTokens({ access_token: ACCESS, refresh_token: REFRESH, expires_at: 9999999999 });
let calls = 0;
mockFetch(async (input) => {
calls++;
const url = urlOf(input);
if (url.startsWith('/auth/refresh')) {
return jsonResponse({ access_token: 'new', refresh_token: 'new-r', expires_at: 9999999999 });
}
if (calls === 1) return jsonResponse({ detail: 'expired' }, 401);
return jsonResponse({ username: 'u', telegram_id: null, referal_code: 'x' });
});
const me = await api.getMe();
expect(me.username).toBe('u');
expect(localStorage.getItem('malenia.access')).toBe('new');
});
it('clears tokens and rejects when refresh fails on 401', async () => {
setStoredTokens({ access_token: ACCESS, refresh_token: REFRESH, expires_at: 9999999999 });
mockFetch(async (input) => {
const url = urlOf(input);
if (url.startsWith('/auth/refresh')) return jsonResponse({ detail: 'invalid' }, 401);
return jsonResponse({ detail: 'expired' }, 401);
});
await expect(api.getMe()).rejects.toMatchObject({ status: 401 });
expect(localStorage.getItem('malenia.access')).toBeNull();
});
it('checkout sends DELETE-style? no — POST body with order details', async () => {
const fetchMock = mockFetch(async () =>
jsonResponse({
order_id: 'ord-1',
total_amount: 100,
bonus_paid: 0,
amount_to_pay: 100,
payment_link: 'https://pay',
}, 201),
);
const res = await api.checkout({ devices: 3, addons: [], duration_days: 30 });
expect(res.order_id).toBe('ord-1');
expect(fetchMock).toHaveBeenCalledOnce();
});
it('deleteDevice uses DELETE method and encodes hwid', async () => {
const fetchMock = mockFetch(async () => jsonResponse({ success: true }));
await api.deleteDevice('hw id/123');
const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
expect(init?.method).toBe('DELETE');
const url = fetchMock.mock.calls[0]?.[0] as string;
expect(url).toContain(encodeURIComponent('hw id/123'));
});
});

184
src/api/client.ts Normal file
View File

@@ -0,0 +1,184 @@
import { useAuthStore } from '../store/useStore';
import type {
CheckoutResponse,
Device,
LoginPayload,
LoginResponse,
OperationResult,
OrderDetails,
PricingPlans,
SignupPayload,
SubscriptionData,
Tokens,
UserInfo,
} from '../types';
const ACCESS_KEY = 'malenia.access';
const REFRESH_KEY = 'malenia.refresh';
const EXPIRES_KEY = 'malenia.expires_at';
/* ── Token storage helpers (mirror of store, used by client) ── */
function readToken(key: string): string | null {
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
export function setStoredTokens(tokens: Tokens): void {
try {
localStorage.setItem(ACCESS_KEY, tokens.access_token);
localStorage.setItem(REFRESH_KEY, tokens.refresh_token);
localStorage.setItem(EXPIRES_KEY, String(tokens.expires_at));
} catch {
/* ignore */
}
}
export function clearStoredTokens(): void {
try {
localStorage.removeItem(ACCESS_KEY);
localStorage.removeItem(REFRESH_KEY);
localStorage.removeItem(EXPIRES_KEY);
} catch {
/* ignore */
}
}
function getRefreshToken(): string | null {
return readToken(REFRESH_KEY);
}
/* ── RequestError ─────────────────────────────────────────── */
export class ApiError extends Error {
readonly status: number;
constructor(message: string, status: number) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
async function parseError(res: Response): Promise<ApiError> {
let detail = `Запрос не выполнен (${res.status})`;
try {
const data = (await res.json()) as unknown;
if (Array.isArray((data as { detail?: unknown }).detail)) {
const arr = (data as { detail: { msg?: string }[] }).detail;
detail = arr.map((e) => e.msg ?? '').filter(Boolean).join('; ') || detail;
} else if (typeof (data as { detail?: unknown }).detail === 'string') {
detail = (data as { detail: string }).detail;
} else if (typeof data === 'string' && data.length > 0) {
detail = data;
}
} catch {
/* keep default */
}
return new ApiError(detail, res.status);
}
let refreshing: Promise<boolean> | null = null;
async function refreshTokens(): Promise<boolean> {
if (refreshing) return refreshing;
const refresh = getRefreshToken();
if (!refresh) return false;
refreshing = (async () => {
try {
const res = await fetch(
`/auth/refresh?refresh_token=${encodeURIComponent(refresh)}&iss=credentials`,
{ method: 'POST' },
);
if (!res.ok) return false;
const tokens = (await res.json()) as Tokens;
setStoredTokens(tokens);
useAuthStore.getState().setTokens(tokens);
return true;
} catch {
return false;
} finally {
refreshing = null;
}
})();
return refreshing;
}
interface RequestOptions {
method?: string;
body?: unknown;
signal?: AbortSignal;
auth?: boolean;
}
async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
const { method = 'GET', body, signal, auth = true } = opts;
function buildHeaders(): Record<string, string> {
const headers: Record<string, string> = {};
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (auth) {
const access = readToken(ACCESS_KEY);
if (access) headers.Authorization = `Bearer ${access}`;
}
return headers;
}
let res = await fetch(path, {
method,
headers: buildHeaders(),
body: body === undefined ? undefined : JSON.stringify(body),
signal,
});
if (res.status === 401 && auth) {
const ok = await refreshTokens();
if (ok) {
res = await fetch(path, {
method,
headers: buildHeaders(),
body: body === undefined ? undefined : JSON.stringify(body),
signal,
});
} else {
useAuthStore.getState().clear();
clearStoredTokens();
throw new ApiError('Сессия истекла', 401);
}
}
if (!res.ok) throw await parseError(res);
if (res.status === 204) return undefined as T;
return (await res.json()) as T;
}
/* ── Public API ───────────────────────────────────────────── */
export const api = {
signup(payload: SignupPayload): Promise<LoginResponse> {
return request<LoginResponse>('/auth/signup', { method: 'POST', body: payload, auth: false });
},
login(payload: LoginPayload): Promise<LoginResponse> {
return request<LoginResponse>('/auth/login', { method: 'POST', body: payload, auth: false });
},
getPlans(): Promise<PricingPlans> {
return request<PricingPlans>('/plans/', { auth: false });
},
checkout(order: OrderDetails): Promise<CheckoutResponse> {
return request<CheckoutResponse>('/orders/checkout', { method: 'POST', body: order });
},
getMe(): Promise<UserInfo> {
return request<UserInfo>('/users/me');
},
getSubscription(): Promise<SubscriptionData> {
return request<SubscriptionData>('/users/subscription');
},
getDevices(): Promise<Device[]> {
return request<Device[]>('/users/subscription/hwid');
},
deleteDevice(hwid: string): Promise<OperationResult> {
return request<OperationResult>(
`/users/subscription/hwid?hwid=${encodeURIComponent(hwid)}`,
{ method: 'DELETE' },
);
},
};

View File

@@ -0,0 +1,43 @@
import type { AddonData } from '../types';
import { formatMoney } from '../lib/format';
interface AddonListProps {
addons: AddonData[];
selected: string[];
onToggle: (id: string) => void;
}
export function AddonList({ addons, selected, onToggle }: AddonListProps) {
if (addons.length === 0) return null;
return (
<div className="plan-addons">
<div className="addons-head">
<h3 className="addons-title">Дополнения</h3>
<span className="addons-cap">Опционально</span>
</div>
<div className="addon-list">
{addons.map((a) => {
const on = selected.includes(a.id);
return (
<button
key={a.id}
type="button"
className={`addon-row ${on ? 'is-selected' : ''}`.trim()}
aria-pressed={on}
onClick={() => onToggle(a.id)}
>
<span className="addon-toggle" aria-hidden="true" />
<span className="addon-info">
<span className="addon-name">{a.name}</span>
</span>
<span className="addon-price">
<span className="plus">+</span>
{formatMoney(a.price)}
</span>
</button>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import type { DurationOption } from '../lib/format';
import { formatMoney } from '../lib/format';
interface BreakdownProps {
devices: number;
devicePrice: number;
addonCount: number;
addonSum: number;
duration: DurationOption;
monthly: number;
periodTotal: number;
}
export function Breakdown({
devices,
devicePrice,
addonCount,
addonSum,
duration,
monthly,
periodTotal,
}: BreakdownProps) {
return (
<div className="plan-breakdown">
<div className="breakdown-row">
<div className="breakdown-key">
Устройства<em>{devices}</em>
</div>
<div className="breakdown-val">
{formatMoney(devicePrice)}
<span className="muted">/ месяц</span>
</div>
</div>
{addonCount > 0 ? (
<div className="breakdown-row">
<div className="breakdown-key">
Дополнения<em>{addonCount}</em>
</div>
<div className="breakdown-val">
{formatMoney(addonSum)}
<span className="muted">/ месяц</span>
</div>
</div>
) : null}
<div className="breakdown-row breakdown-total">
<div className="breakdown-key">Итого</div>
<div className="breakdown-val">
{formatMoney(monthly)}
<span className="per">в месяц</span>
</div>
</div>
<div className="breakdown-row breakdown-billed">
<div className="breakdown-key">К оплате за {duration.months} мес.</div>
<div className="breakdown-val">{formatMoney(periodTotal)}</div>
</div>
</div>
);
}

74
src/components/Cursor.tsx Normal file
View File

@@ -0,0 +1,74 @@
import { useEffect, useRef } from 'react';
const FINE_QUERY = '(hover: hover) and (pointer: fine)';
export function Cursor(): null {
const dotRef = useRef<HTMLDivElement | null>(null);
const ringRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const fine = window.matchMedia(FINE_QUERY).matches;
if (!fine) return;
const dot = document.createElement('div');
dot.className = 'cursor-dot';
const ring = document.createElement('div');
ring.className = 'cursor-ring';
document.body.appendChild(dot);
document.body.appendChild(ring);
dotRef.current = dot;
ringRef.current = ring;
let mx = -100;
let my = -100;
let rx = -100;
let ry = -100;
let raf = 0;
const onMove = (e: MouseEvent) => {
mx = e.clientX;
my = e.clientY;
dot.style.left = `${mx}px`;
dot.style.top = `${my}px`;
};
const onOver = (e: MouseEvent) => {
const target = e.target as Element | null;
const interactive = !!target?.closest('a, button, input, [data-cursor]');
ring.classList.toggle('is-active', interactive);
};
const onLeave = () => {
dot.style.opacity = '0';
ring.style.opacity = '0';
};
const onEnter = () => {
dot.style.opacity = '1';
ring.style.opacity = '1';
};
const tick = () => {
rx += (mx - rx) * 0.18;
ry += (my - ry) * 0.18;
ring.style.left = `${rx}px`;
ring.style.top = `${ry}px`;
raf = requestAnimationFrame(tick);
};
window.addEventListener('mousemove', onMove);
document.addEventListener('mouseover', onOver);
document.addEventListener('mouseleave', onLeave);
document.addEventListener('mouseenter', onEnter);
raf = requestAnimationFrame(tick);
return () => {
window.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseover', onOver);
document.removeEventListener('mouseleave', onLeave);
document.removeEventListener('mouseenter', onEnter);
cancelAnimationFrame(raf);
dot.remove();
ring.remove();
};
}, []);
return null;
}

View File

@@ -0,0 +1,76 @@
import type { Device } from '../types';
const GLYPHS_DEFAULT =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="13" rx="6" ry="7"/><path d="M9 9.5l1 1M15 9.5l-1 1M10 17h4"/></svg>';
const GLYPHS: Record<string, string> = {
macos:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="13" rx="1.5"/><path d="M1 20h22M9 20l1-3h4l1 3"/></svg>',
ios:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="2.5" width="10" height="19" rx="2"/><path d="M11 5.5h2"/></svg>',
android:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 14a7 7 0 0 1 14 0v5H5z"/><path d="M5 14L3 12M19 14l2-2M8 9l-1.5-2M16 9l1.5-2M9 8.5h6"/></svg>',
windows:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5l8-1v8H3zM12 4l9-1v10h-9zM3 13h8v6l-8-1zM12 13h9v8l-9-1z"/></svg>',
linux:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="13" rx="6" ry="7"/><path d="M9 9.5l1 1M15 9.5l-1 1M10 17h4"/></svg>',
};
const OS_LABELS: Record<string, string> = {
macos: 'macOS',
ios: 'iOS',
android: 'Android',
windows: 'Windows',
linux: 'Linux',
};
function glyphFor(os: string | null): string {
if (!os) return GLYPHS.linux ?? GLYPHS_DEFAULT;
const key = os.toLowerCase();
return GLYPHS[key] ?? GLYPHS.linux ?? GLYPHS_DEFAULT;
}
function osLabel(os: string | null): string {
if (!os) return 'Устройство';
return OS_LABELS[os.toLowerCase()] ?? os;
}
interface DeviceCardProps {
device: Device;
onDelete: (device: Device) => void;
}
export function DeviceCard({ device, onDelete }: DeviceCardProps) {
const model = device.model ?? 'Без названия';
return (
<article className="device reveal is-in">
<div className="device-glyph" aria-hidden="true" dangerouslySetInnerHTML={{ __html: glyphFor(device.os) }} />
<div className="device-meta">
<h3 className="device-model">{model}</h3>
<div className="device-tags">
<span>{osLabel(device.os)}</span>
{device.client ? (
<>
<span className="sep" />
<span className="tag-client">{device.client}</span>
</>
) : null}
<span className="sep" />
<span className="tag-client" title={device.hwid}>
HWID
</span>
</div>
</div>
<button
type="button"
className="device-delete"
aria-label={`Удалить ${model}`}
onClick={() => onDelete(device)}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14" />
</svg>
</button>
</article>
);
}

View File

@@ -0,0 +1,165 @@
import { useCallback, useEffect, useRef, useState } from 'react';
interface DeviceSliderProps {
min: number;
max: number;
value: number;
onChange: (v: number) => void;
}
const pctOf = (v: number, min: number, max: number) => (v - min) / (max - min);
export function DeviceSlider({ min, max, value, onChange }: DeviceSliderProps) {
const wrapRef = useRef<HTMLDivElement | null>(null);
const frameRef = useRef<HTMLDivElement | null>(null);
const hitRef = useRef<HTMLDivElement | null>(null);
const pointerRef = useRef<HTMLDivElement | null>(null);
const haloRef = useRef<HTMLDivElement | null>(null);
const fillRef = useRef<HTMLDivElement | null>(null);
const [dragging, setDragging] = useState(false);
const renderPositions = useCallback(() => {
const frame = frameRef.current;
if (!frame) return;
const innerW = Math.max(0, frame.clientWidth - 8);
const x = 4 + pctOf(value, min, max) * innerW;
if (pointerRef.current) pointerRef.current.style.left = `${x}px`;
if (haloRef.current) haloRef.current.style.left = `${x}px`;
if (fillRef.current) fillRef.current.style.width = `${x - 4}px`;
}, [max, min, value]);
useEffect(() => {
renderPositions();
}, [renderPositions]);
useEffect(() => {
window.addEventListener('resize', renderPositions);
return () => window.removeEventListener('resize', renderPositions);
}, [renderPositions]);
const valueFromClientX = useCallback(
(clientX: number) => {
const frame = frameRef.current;
if (!frame) return value;
const rect = frame.getBoundingClientRect();
const innerLeft = rect.left + 4;
const innerW = rect.width - 8;
const p = (clientX - innerLeft) / innerW;
const clamped = Math.max(0, Math.min(1, p));
return Math.round(min + clamped * (max - min));
},
[max, min, value],
);
useEffect(() => {
const hit = hitRef.current;
if (!hit) return;
const onDown = (e: PointerEvent) => {
e.preventDefault();
try {
hit.setPointerCapture(e.pointerId);
} catch {
/* ignore */
}
setDragging(true);
onChange(valueFromClientX(e.clientX));
};
const onMove = (e: PointerEvent) => {
if (!dragging) return;
onChange(valueFromClientX(e.clientX));
};
const onUp = (e: PointerEvent) => {
if (!dragging) return;
setDragging(false);
try {
hit.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
};
hit.addEventListener('pointerdown', onDown);
hit.addEventListener('pointermove', onMove);
hit.addEventListener('pointerup', onUp);
hit.addEventListener('pointercancel', onUp);
return () => {
hit.removeEventListener('pointerdown', onDown);
hit.removeEventListener('pointermove', onMove);
hit.removeEventListener('pointerup', onUp);
hit.removeEventListener('pointercancel', onUp);
};
}, [dragging, onChange, valueFromClientX]);
const onFrameClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (dragging) return;
if (e.target instanceof Element && e.target.closest('.pointer')) return;
onChange(valueFromClientX(e.clientX));
};
const onKey = (e: React.KeyboardEvent<HTMLDivElement>) => {
let next = value;
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = Math.min(max, value + (e.shiftKey ? 2 : 1));
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = Math.max(min, value - (e.shiftKey ? 2 : 1));
else if (e.key === 'Home') next = min;
else if (e.key === 'End') next = max;
else return;
e.preventDefault();
onChange(next);
};
const ticks: number[] = [];
for (let i = min; i <= max; i++) ticks.push(i);
const mid = Math.round((min + max) / 2);
return (
<div className={`slider-wrap ${dragging ? 'is-dragging' : ''}`.trim()} ref={wrapRef} data-cursor="">
<div className="slider-axis">
<span>Устройства</span>
<span>
<span className="axis-now">{value}</span> &nbsp;мест
</span>
</div>
<div className="slider-track-frame" ref={frameRef} onClick={onFrameClick}>
<div className="slider-track">
<div className="slider-fill" ref={fillRef} />
</div>
<div className="slider-ticks" aria-hidden="true">
{ticks.map((i) => (
<div
key={i}
className={`tick ${i === min || i === max || i === mid ? 'is-major' : ''} ${i === value ? 'is-active' : ''}`.trim()}
>
<span className="tick-label">{i}</span>
</div>
))}
</div>
<div className="pointer-halo" ref={haloRef} aria-hidden="true" />
<div
className="pointer"
ref={pointerRef}
role="slider"
tabIndex={0}
aria-label="Количество устройств"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
aria-valuetext={`${value} устройств`}
onKeyDown={onKey}
onFocus={() => setDragging(true)}
onBlur={() => setDragging(false)}
/>
<div className="slider-hit" ref={hitRef} />
</div>
<div className="slider-caption">
<span className="hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 12h8M12 8l-4 4 4 4M12 8l4 4-4 4" />
</svg>
Тяните ромб · стрелки для точной настройки
</span>
<span>Диапазон {min} – {max}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { DURATION_OPTIONS } from '../lib/format';
interface DurationSelectProps {
days: number;
onChange: (days: number) => void;
}
export function DurationSelect({ days, onChange }: DurationSelectProps) {
return (
<div className="plan-duration">
<div className="duration-head">
<span className="duration-label">Срок</span>
<span className="duration-cap">Оплата за весь срок сразу</span>
</div>
<div className="duration-grid" role="radiogroup" aria-label="Срок подписки">
{DURATION_OPTIONS.map((d) => {
const active = d.days === days;
return (
<button
key={d.days}
type="button"
role="radio"
aria-checked={active}
className={`dur ${active ? 'is-active' : ''}`.trim()}
onClick={() => onChange(d.days)}
>
<span className="dur-num">{d.months}</span>
<span className="dur-unit">{d.months === 1 ? 'месяц' : d.months < 12 ? 'месяцев' : 'год'}</span>
<span className="dur-unit">{d.tag}</span>
</button>
);
})}
</div>
</div>
);
}

16
src/components/Emblem.tsx Normal file
View File

@@ -0,0 +1,16 @@
export function Emblem({ size = 90 }: { size?: number }) {
return (
<div className="emblem" aria-hidden="true" style={{ width: size, height: size }}>
<svg viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="45" cy="45" r="42" stroke="currentColor" strokeWidth="0.8" strokeDasharray="3 4" />
<circle cx="45" cy="45" r="32" stroke="currentColor" strokeWidth="0.5" opacity="0.5" />
<path d="M45 18 L72 45 L45 72 L18 45 Z" stroke="currentColor" strokeWidth="0.9" fill="none" />
<circle cx="45" cy="45" r="3.5" fill="currentColor" />
<line x1="45" y1="3" x2="45" y2="12" stroke="currentColor" strokeWidth="0.8" />
<line x1="45" y1="78" x2="45" y2="87" stroke="currentColor" strokeWidth="0.8" />
<line x1="3" y1="45" x2="12" y2="45" stroke="currentColor" strokeWidth="0.8" />
<line x1="78" y1="45" x2="87" y2="45" stroke="currentColor" strokeWidth="0.8" />
</svg>
</div>
);
}

9
src/components/Logo.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { Link } from 'react-router-dom';
export function Logo({ to = '/' }: { to?: string }) {
return (
<Link to={to} className="logo" aria-label="Malenia">
Mal<span className="e">e</span>nia
</Link>
);
}

77
src/components/Modal.tsx Normal file
View File

@@ -0,0 +1,77 @@
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
open: boolean;
onClose: () => void;
title: string;
kicker?: string;
body: React.ReactNode;
confirmLabel: string;
cancelLabel?: string;
onConfirm: () => void;
}
export function Modal({
open,
onClose,
title,
kicker,
body,
confirmLabel,
cancelLabel = 'Оставить',
onConfirm,
}: ModalProps) {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', onKey);
document.body.style.overflow = '';
};
}, [open, onClose]);
if (!open) return null;
return createPortal(
<div
className={`modal-veil ${open ? 'is-open' : ''}`}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="modal" role="document">
<div className="modal-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14" />
</svg>
</div>
{kicker ? <div className="modal-kicker">{kicker}</div> : null}
<h3 className="modal-title" id="modal-title">
{title}
</h3>
<div className="modal-body">{body}</div>
<div className="modal-actions">
<button type="button" className="btn" onClick={onClose} autoFocus>
{cancelLabel}
</button>
<button
type="button"
className="btn btn-danger"
onClick={onConfirm}
>
{confirmLabel}
</button>
</div>
</div>
</div>,
document.body,
);
}

43
src/components/Reveal.tsx Normal file
View File

@@ -0,0 +1,43 @@
import { createElement, useEffect, useRef, type ReactNode } from 'react';
interface RevealProps {
children: ReactNode;
delay?: number;
className?: string;
}
export function Reveal({ children, delay = 0, className }: RevealProps) {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (typeof IntersectionObserver === 'undefined') {
el.classList.add('is-in');
return;
}
const io = new IntersectionObserver(
(entries) => {
entries.forEach((en) => {
if (en.isIntersecting) {
en.target.classList.add('is-in');
io.unobserve(en.target);
}
});
},
{ threshold: 0.12 },
);
io.observe(el);
return () => io.disconnect();
}, []);
return createElement(
'div',
{
ref,
className: `reveal ${className ?? ''}`.trim(),
style: { transitionDelay: `${delay}ms` },
},
children,
);
}

16
src/components/Shell.tsx Normal file
View File

@@ -0,0 +1,16 @@
import type { ReactNode } from 'react';
import { TopBar } from './TopBar';
interface ShellProps {
active?: 'plan' | 'account' | 'support' | 'channel';
children: ReactNode;
}
export function Shell({ active, children }: ShellProps) {
return (
<div className="plan-shell">
<TopBar active={active} />
{children}
</div>
);
}

View File

@@ -0,0 +1,19 @@
import type { CSSProperties } from 'react';
interface SkeletonProps {
width?: string;
height?: string;
radius?: string;
className?: string;
style?: CSSProperties;
}
export function Skeleton({ width = '100%', height = '1em', radius, className, style }: SkeletonProps) {
return (
<span
className={`skeleton ${className ?? ''}`.trim()}
style={{ display: 'block', width, height, borderRadius: radius ?? undefined, ...style }}
aria-hidden="true"
/>
);
}

View File

@@ -0,0 +1,94 @@
import type { SubscriptionData, UserInfo } from '../types';
import { formatDate } from '../lib/format';
interface StatStripProps {
user: UserInfo | null;
subscription: SubscriptionData | null;
deviceCount: number;
}
function Stat({
icon,
label,
children,
}: {
icon: React.ReactNode;
label: string;
children: React.ReactNode;
}) {
return (
<div className="stat">
<div className="stat-icon" aria-hidden="true">
{icon}
</div>
<div className="stat-label">{label}</div>
<div className="stat-value">{children}</div>
</div>
);
}
export function StatStrip({ user, subscription, deviceCount }: StatStripProps) {
const username = user?.username ?? null;
const telegramId = user?.telegram_id ?? null;
const devicesLimit = subscription?.devices ?? null;
const expiresAt = subscription?.expires_at ?? null;
return (
<section className="profile" aria-label="Сводка по аккаунту">
<Stat
icon={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="8" r="4" />
<path d="M4 20c0-3.6 3.6-6 8-6s8 2.4 8 6" />
</svg>
}
label="Имя пользователя"
>
{username ? <span>{username}</span> : <span className="is-empty">не задано</span>}
</Stat>
<Stat
icon={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 4L3 11l5 2 2 6 3-4 4 3 4-14z" />
<path d="M8 13l8-6" />
</svg>
}
label="Telegram ID"
>
{telegramId ? <span className="is-mono">@{telegramId}</span> : <span className="is-empty">не привязан</span>}
</Stat>
<Stat
icon={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="12" rx="1.5" />
<path d="M9 20h6M12 16v4" />
</svg>
}
label="Устройства"
>
{devicesLimit ? (
<span>
{deviceCount} <span className="accent">/</span>{' '}
<span className="stat-small">из {devicesLimit}</span>
</span>
) : (
<span className="is-empty">нет подписки</span>
)}
</Stat>
<Stat
icon={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="5" width="18" height="16" rx="1.5" />
<path d="M3 9h18M8 3v4M16 3v4" />
</svg>
}
label="Продление"
>
{expiresAt ? <span>{formatDate(expiresAt)}</span> : <span className="is-empty">—</span>}
</Stat>
</section>
);
}

17
src/components/Toast.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { createPortal } from 'react-dom';
import { useToastStore } from '../store/useToast';
export function Toast() {
const toasts = useToastStore((s) => s.toasts);
if (toasts.length === 0) return null;
return createPortal(
<div aria-live="polite" role="status">
{toasts.map((t) => (
<div key={t.id} className="toast is-show">
{t.message}
</div>
))}
</div>,
document.body,
);
}

38
src/components/TopBar.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { Link } from 'react-router-dom';
import { Logo } from './Logo';
interface TopBarProps {
active?: 'plan' | 'account' | 'support' | 'channel';
}
export function TopBar({ active }: TopBarProps) {
return (
<header className="topbar">
<Logo to="/" />
<nav className="nav" aria-label="Основная навигация">
<Link to="/plan" className={active === 'plan' ? 'is-active' : undefined}>
Оформить
</Link>
<a
href="https://t.me/maleniavpn"
target="_blank"
rel="noopener noreferrer"
className={active === 'channel' ? 'is-active' : undefined}
>
Канал
</a>
<a
href="https://t.me/maleniasupportbot"
target="_blank"
rel="noopener noreferrer"
className={active === 'support' ? 'is-active' : undefined}
>
Поддержка
</a>
<Link to="/account" className={active === 'account' ? 'is-active' : undefined}>
Аккаунт
</Link>
</nav>
</header>
);
}

25
src/hooks/useAuth.ts Normal file
View File

@@ -0,0 +1,25 @@
import { useAuthStore } from '../store/useStore';
export function useAuth() {
const access = useAuthStore((s) => s.access);
const user = useAuthStore((s) => s.user);
const expiresAt = useAuthStore((s) => s.expiresAt);
const setSession = useAuthStore((s) => s.setSession);
const updateUser = useAuthStore((s) => s.updateUser);
const setRemember = useAuthStore((s) => s.setRemember);
const remember = useAuthStore((s) => s.remember);
const clear = useAuthStore((s) => s.clear);
const isAuthenticated = Boolean(access);
const isExpired = expiresAt !== null && expiresAt * 1000 < Date.now();
return {
isAuthenticated: isAuthenticated && !isExpired,
user,
remember,
setSession,
updateUser,
setRemember,
clear,
};
}

54
src/hooks/useDevices.ts Normal file
View File

@@ -0,0 +1,54 @@
import { useCallback, useEffect, useState } from 'react';
import { api, ApiError } from '../api/client';
import type { Device } from '../types';
interface UseDevicesState {
data: Device[];
loading: boolean;
error: string | null;
reload: () => Promise<void>;
remove: (hwid: string) => Promise<boolean>;
}
export function useDevices(): UseDevicesState {
const [data, setData] = useState<Device[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const devices = await api.getDevices();
setData(devices);
} catch (err: unknown) {
const message = err instanceof ApiError ? err.message : 'Не удалось загрузить устройства';
setError(message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
const remove = useCallback(
async (hwid: string) => {
try {
const res = await api.deleteDevice(hwid);
if (res.success) {
setData((prev) => prev.filter((d) => d.hwid !== hwid));
}
return res.success;
} catch (err: unknown) {
const message = err instanceof ApiError ? err.message : 'Ошибка удаления устройства';
setError(message);
return false;
}
},
[],
);
return { data, loading, error, reload: load, remove };
}

View File

@@ -0,0 +1,39 @@
import { useCallback, useEffect, useState } from 'react';
export function useLocalStorage<T>(key: string, initial: T): [T, (v: T | ((prev: T) => T)) => void] {
const [value, setValue] = useState<T>(() => {
try {
const raw = localStorage.getItem(key);
return raw === null ? initial : (JSON.parse(raw) as T);
} catch {
return initial;
}
});
const set = useCallback(
(v: T | ((prev: T) => T)) => {
setValue((prev) => {
const next = typeof v === 'function' ? (v as (prev: T) => T)(prev) : v;
try {
localStorage.setItem(key, JSON.stringify(next));
} catch {
/* ignore */
}
return next;
});
},
[key],
);
useEffect(() => {
try {
const raw = localStorage.getItem(key);
if (raw !== null) setValue(raw === null ? initial : (JSON.parse(raw) as T));
} catch {
/* ignore */
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [value, set];
}

37
src/hooks/usePlans.ts Normal file
View File

@@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
import { api, ApiError } from '../api/client';
import type { PricingPlans } from '../types';
interface UsePlansState {
data: PricingPlans | null;
loading: boolean;
error: string | null;
}
export function usePlans(): UsePlansState {
const [state, setState] = useState<UsePlansState>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let active = true;
setState((s) => ({ ...s, loading: true, error: null }));
api
.getPlans()
.then((plans) => {
if (active) setState({ data: plans, loading: false, error: null });
})
.catch((err: unknown) => {
if (!active) return;
const message = err instanceof ApiError ? err.message : 'Не удалось загрузить тарифы';
setState({ data: null, loading: false, error: message });
});
return () => {
active = false;
};
}, []);
return state;
}

109
src/lib/format.test.ts Normal file
View File

@@ -0,0 +1,109 @@
import { describe, it, expect } from 'vitest';
import {
DURATION_OPTIONS,
calculatePricing,
durationByDays,
pluralize,
round2,
sumAddonPrices,
devicesLabel,
formatMoney,
} from './format';
import type { AddonData, PricingPlans } from '../types';
const pricing: PricingPlans = {
device_price: 100,
addons: [
{ id: 'a', name: 'A', price: 50, free_threshold: 0 },
{ id: 'b', name: 'B', price: 30, free_threshold: 0 },
],
};
describe('round2', () => {
it('rounds to two decimals', () => {
expect(round2(1.005)).toBe(1.01);
expect(round2(2.345)).toBe(2.35);
expect(round2(10)).toBe(10);
});
});
describe('sumAddonPrices', () => {
const addons: AddonData[] = pricing.addons;
it('sums selected addon prices', () => {
expect(sumAddonPrices(addons, ['a', 'b'])).toBe(80);
expect(sumAddonPrices(addons, ['a'])).toBe(50);
expect(sumAddonPrices(addons, ['unknown'])).toBe(0);
expect(sumAddonPrices(addons, [])).toBe(0);
});
});
describe('calculatePricing', () => {
it('computes linear device cost + addons + duration total', () => {
const r = calculatePricing({
pricing,
devices: 6,
addonIds: ['a'],
duration: DURATION_OPTIONS[1]!, // 90 days
});
// monthly = 6*100 + 50 = 650
// total = 650 * 90 / 30 = 1950
expect(r.deviceCost).toBe(600);
expect(r.addonCost).toBe(50);
expect(r.monthly).toBe(650);
expect(r.periodTotal).toBe(1950);
});
it('handles 1 month / 30 days', () => {
const r = calculatePricing({
pricing,
devices: 3,
addonIds: [],
duration: DURATION_OPTIONS[0]!,
});
expect(r.monthly).toBe(300);
expect(r.periodTotal).toBe(300);
});
it('12 months / 365 days yields near 12x', () => {
const r = calculatePricing({
pricing,
devices: 1,
addonIds: [],
duration: DURATION_OPTIONS[3]!,
});
// monthly=100, total = 100 * 365/30 = 1216.67
expect(r.periodTotal).toBeCloseTo(1216.67, 1);
});
});
describe('durationByDays', () => {
it('finds duration by days', () => {
expect(durationByDays(30).months).toBe(1);
expect(durationByDays(90).months).toBe(3);
expect(durationByDays(180).months).toBe(6);
expect(durationByDays(365).months).toBe(12);
});
it('falls back to first option for unknown days', () => {
expect(durationByDays(7).months).toBe(1);
});
});
describe('pluralize / devicesLabel', () => {
it('picks correct form', () => {
expect(pluralize(1, 'устройство', 'устройства', 'устройств')).toBe('устройство');
expect(pluralize(2, 'устройство', 'устройства', 'устройств')).toBe('устройства');
expect(pluralize(5, 'устройство', 'устройства', 'устройств')).toBe('устройств');
expect(pluralize(11, 'устройство', 'устройства', 'устройств')).toBe('устройств');
});
it('devicesLabel combines number and noun', () => {
expect(devicesLabel(3)).toBe('3 устройства');
expect(devicesLabel(6)).toBe('6 устройств');
});
});
describe('formatMoney', () => {
it('formats rubles', () => {
expect(formatMoney(100)).toMatch(/100/);
expect(formatMoney(1950)).toMatch(/1\s950/);
});
});

103
src/lib/format.ts Normal file
View File

@@ -0,0 +1,103 @@
import type { AddonData, PricingPlans } from '../types';
/* ── Money ───────────────────────────────────────────────── */
const RUB = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
export function formatMoney(value: number): string {
return RUB.format(round2(value));
}
export function formatMoneyPlain(value: number): string {
return String(round2(value));
}
export function round2(value: number): number {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
/* ── Duration ───────────────────────────────────────────── */
export interface DurationOption {
months: number;
days: number;
tag: string;
}
export const DURATION_OPTIONS: DurationOption[] = [
{ months: 1, days: 30, tag: '1 месяц' },
{ months: 3, days: 90, tag: '3 месяца' },
{ months: 6, days: 180, tag: '6 месяцев' },
{ months: 12, days: 365, tag: '1 год' },
];
export function durationByDays(days: number): DurationOption {
return DURATION_OPTIONS.find((d) => d.days === days) ?? DURATION_OPTIONS[0]!;
}
/* ── Pricing ────────────────────────────────────────────── */
export interface PricingInput {
pricing: PricingPlans;
devices: number;
addonIds: string[];
duration: DurationOption;
}
export interface PricingBreakdown {
deviceCost: number;
addonCost: number;
monthly: number;
periodTotal: number;
}
/**
* Mirrors the backend `services/subscriptions.py`:
* monthly = devices * device_price + sum(selected addon prices)
* total = monthly * duration_days / 30
*/
export function calculatePricing({
pricing,
devices,
addonIds,
duration,
}: PricingInput): PricingBreakdown {
const deviceCost = pricing.device_price * devices;
const addonCost = sumAddonPrices(pricing.addons, addonIds);
const monthly = deviceCost + addonCost;
const periodTotal = round2((monthly * duration.days) / 30);
return {
deviceCost: round2(deviceCost),
addonCost: round2(addonCost),
monthly: round2(monthly),
periodTotal,
};
}
export function sumAddonPrices(addons: AddonData[], selected: string[]): number {
const priceById = new Map(addons.map((a) => [a.id, a.price] as const));
return selected.reduce<number>((sum, id) => sum + (priceById.get(id) ?? 0), 0);
}
/* ── Date ────────────────────────────────────────────────── */
export function formatDate(value: string | number | Date): string {
const d = new Date(value);
return new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })
.format(d)
.replace(' г.', '');
}
/* ── Pluralization (Russian) ─────────────────────────────── */
export function pluralize(n: number, one: string, few: string, many: string): string {
const mod10 = n % 10;
const mod100 = n % 100;
if (mod10 === 1 && mod100 !== 11) return one;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
return many;
}
export function devicesLabel(n: number): string {
return `${n} ${pluralize(n, 'устройство', 'устройства', 'устройств')}`;
}

View File

@@ -0,0 +1,48 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { PLAN_CONFIG_KEY, readPlanConfig, resolvePlanConfig, savePlanConfig } from './planConfig';
describe('plan configuration storage', () => {
beforeEach(() => localStorage.clear());
it('saves and restores a complete calculator configuration', () => {
savePlanConfig({ devices: 8, durationDays: 180, addons: ['ip', 'multi-hop'] });
expect(readPlanConfig()).toEqual({ devices: 8, durationDays: 180, addons: ['ip', 'multi-hop'] });
});
it('ignores malformed or invalid saved data', () => {
localStorage.setItem(PLAN_CONFIG_KEY, '{bad json');
expect(readPlanConfig()).toBeNull();
localStorage.setItem(
PLAN_CONFIG_KEY,
JSON.stringify({ devices: 20, durationDays: 7, addons: [12] }),
);
expect(readPlanConfig()).toBeNull();
});
it('restores saved data when URL does not contain a configuration', () => {
savePlanConfig({ devices: 7, durationDays: 90, addons: ['ip'] });
expect(resolvePlanConfig(new URLSearchParams())).toEqual({
devices: 7,
durationDays: 90,
addons: ['ip'],
});
});
it('gives valid URL parameters priority over saved data', () => {
savePlanConfig({ devices: 7, durationDays: 90, addons: ['ip'] });
expect(resolvePlanConfig(new URLSearchParams('devices=10&duration=365&addons=multi-hop'))).toEqual({
devices: 10,
durationDays: 365,
addons: ['multi-hop'],
});
});
it('uses defaults when no valid configuration is available', () => {
expect(resolvePlanConfig(new URLSearchParams('devices=1&duration=10'))).toEqual({
devices: 6,
durationDays: 30,
addons: [],
});
});
});

80
src/lib/planConfig.ts Normal file
View File

@@ -0,0 +1,80 @@
import { DURATION_OPTIONS } from './format';
export const PLAN_CONFIG_KEY = 'malenia.plan_config';
export const MIN_DEVICES = 3;
export const MAX_DEVICES = 12;
export interface PlanConfig {
devices: number;
durationDays: number;
addons: string[];
}
const DEFAULT_CONFIG: PlanConfig = {
devices: 6,
durationDays: DURATION_OPTIONS[0]!.days,
addons: [],
};
function validDevices(value: unknown): value is number {
return (
typeof value === 'number' &&
Number.isInteger(value) &&
value >= MIN_DEVICES &&
value <= MAX_DEVICES
);
}
function validDuration(value: unknown): value is number {
return (
typeof value === 'number' &&
Number.isInteger(value) &&
DURATION_OPTIONS.some((option) => option.days === value)
);
}
function validAddons(value: unknown): value is string[] {
if (!Array.isArray(value)) return false;
return value.every((addon) => typeof addon === 'string' && addon.length > 0);
}
function unique(values: string[]): string[] {
return [...new Set(values)];
}
export function readPlanConfig(): PlanConfig | null {
try {
const raw = localStorage.getItem(PLAN_CONFIG_KEY);
if (!raw) return null;
const value: unknown = JSON.parse(raw);
if (typeof value !== 'object' || value === null) return null;
const config = value as Partial<PlanConfig>;
if (!validDevices(config.devices) || !validDuration(config.durationDays) || !validAddons(config.addons)) {
return null;
}
return { devices: config.devices, durationDays: config.durationDays, addons: unique(config.addons) };
} catch {
return null;
}
}
export function savePlanConfig(config: PlanConfig): void {
try {
localStorage.setItem(PLAN_CONFIG_KEY, JSON.stringify(config));
} catch {
/* Storage may be unavailable in private browsing modes. */
}
}
export function resolvePlanConfig(search: URLSearchParams): PlanConfig {
const saved = readPlanConfig() ?? DEFAULT_CONFIG;
const urlDevices = Number(search.get('devices'));
const urlDuration = Number(search.get('duration'));
const urlAddons = search.get('addons')?.split(',').filter(Boolean);
return {
devices: validDevices(urlDevices) ? urlDevices : saved.devices,
durationDays: validDuration(urlDuration) ? urlDuration : saved.durationDays,
addons: search.has('addons') ? unique(urlAddons ?? []) : saved.addons,
};
}

3
src/lib/redirect.ts Normal file
View File

@@ -0,0 +1,3 @@
export function redirectTo(url: string): void {
window.location.assign(url);
}

12
src/main.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles/base.css';
import './styles/components.css';
import './styles/pages.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,150 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { AccountPage } from './AccountPage';
import { api } from '../api/client';
import { useAuthStore } from '../store/useStore';
vi.mock('../api/client', () => ({
api: {
getPlans: vi.fn(),
checkout: vi.fn(),
getMe: vi.fn(),
getSubscription: vi.fn(),
getDevices: vi.fn(),
deleteDevice: vi.fn(),
login: vi.fn(),
signup: vi.fn(),
},
ApiError: class ApiError extends Error {
constructor(m: string, public status: number) {
super(m);
}
},
setStoredTokens: vi.fn(),
clearStoredTokens: vi.fn(),
}));
function renderAccount(authed = true) {
if (authed) {
useAuthStore.setState({
access: 'a',
refresh: 'r',
expiresAt: 9999999999,
user: { username: 'alice', telegram_id: 'alice_id', referal_code: 'c' },
remember: true,
});
} else {
useAuthStore.setState({
access: null,
refresh: null,
expiresAt: null,
user: null,
remember: true,
});
}
return render(
<MemoryRouter initialEntries={['/account']}>
<Routes>
<Route path="/account" element={<AccountPage />} />
<Route path="/login" element={<div>login page</div>} />
<Route path="/" element={<div>landing</div>} />
</Routes>
</MemoryRouter>,
);
}
const ME = { username: 'alice', telegram_id: 'alice_id', referal_code: 'c' };
const SUB = {
devices: 6,
expires_at: '2026-03-14T00:00:00Z',
addon_ids: [],
subscription_link: 'link',
has_subscription: true,
};
const DEVICES = [
{ os: 'macos', model: 'MacBook Pro', client: 'Malenia', hwid: 'h1' },
{ os: 'ios', model: 'iPhone 15', client: 'Malenia', hwid: 'h2' },
];
describe('AccountPage', () => {
beforeEach(() => {
vi.mocked(api.getMe).mockReset();
vi.mocked(api.getSubscription).mockReset();
vi.mocked(api.getDevices).mockReset();
vi.mocked(api.deleteDevice).mockReset();
});
afterEach(() => vi.restoreAllMocks());
it('redirects to /login when not authenticated', async () => {
renderAccount(false);
await waitFor(() => expect(screen.getByText('login page')).toBeInTheDocument());
});
it('renders hero username and stat strip', async () => {
vi.mocked(api.getMe).mockResolvedValue(ME);
vi.mocked(api.getSubscription).mockResolvedValue(SUB);
vi.mocked(api.getDevices).mockResolvedValue(DEVICES);
renderAccount();
expect(await screen.findByText('alice')).toBeInTheDocument();
expect(screen.getByText('@alice_id')).toBeInTheDocument();
expect(screen.getAllByText(/iPhone 15/)).toHaveLength(1);
});
it('does not render a subscription status before subscription data loads', async () => {
vi.mocked(api.getMe).mockResolvedValue(ME);
vi.mocked(api.getSubscription).mockResolvedValue(SUB);
vi.mocked(api.getDevices).mockResolvedValue(DEVICES);
renderAccount();
expect(screen.queryByText(/активная подписка отсутствует/i)).not.toBeInTheDocument();
expect(await screen.findByText(/подписка до/i)).toBeInTheDocument();
});
it('renders the subscription link and copy action', async () => {
vi.mocked(api.getMe).mockResolvedValue(ME);
vi.mocked(api.getSubscription).mockResolvedValue(SUB);
vi.mocked(api.getDevices).mockResolvedValue(DEVICES);
renderAccount();
expect(await screen.findByText(SUB.subscription_link)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /копировать/i })).toBeEnabled();
});
it('renders empty state when no devices', async () => {
vi.mocked(api.getMe).mockResolvedValue(ME);
vi.mocked(api.getSubscription).mockResolvedValue(SUB);
vi.mocked(api.getDevices).mockResolvedValue([]);
renderAccount();
expect(await screen.findByText(/нет подключённых устройств/i)).toBeInTheDocument();
});
it('opens confirmation modal and deletes device on confirm', async () => {
vi.mocked(api.getMe).mockResolvedValue(ME);
vi.mocked(api.getSubscription).mockResolvedValue(SUB);
vi.mocked(api.getDevices).mockResolvedValue(DEVICES);
vi.mocked(api.deleteDevice).mockResolvedValue({ success: true });
const user = userEvent.setup();
renderAccount();
await screen.findByText(/iPhone 15/);
await user.click(screen.getByRole('button', { name: /удалить iPhone 15/i }));
expect(await screen.findByText(/удалить это устройство\?/i)).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /^удалить$/i });
await user.click(confirmBtns[confirmBtns.length - 1]!);
await waitFor(() => expect(api.deleteDevice).toHaveBeenCalledWith('h2'));
await waitFor(() => expect(screen.queryByText(/iPhone 15/)).not.toBeInTheDocument());
});
it('signs out and navigates to landing', async () => {
vi.mocked(api.getMe).mockResolvedValue(ME);
vi.mocked(api.getSubscription).mockResolvedValue(SUB);
vi.mocked(api.getDevices).mockResolvedValue(DEVICES);
const user = userEvent.setup();
renderAccount();
await screen.findByText(/alice/);
await user.click(screen.getByRole('button', { name: /выйти/i }));
await waitFor(() => expect(screen.getByText('landing')).toBeInTheDocument());
expect(useAuthStore.getState().access).toBeNull();
});
});

215
src/pages/AccountPage.tsx Normal file
View File

@@ -0,0 +1,215 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api, ApiError } from '../api/client';
import { useAuth } from '../hooks/useAuth';
import { useDevices } from '../hooks/useDevices';
import { useToastStore } from '../store/useToast';
import { Shell } from '../components/Shell';
import { Reveal } from '../components/Reveal';
import { Skeleton } from '../components/Skeleton';
import { StatStrip } from '../components/StatStrip';
import { DeviceCard } from '../components/DeviceCard';
import { Modal } from '../components/Modal';
import type { Device, SubscriptionData, UserInfo } from '../types';
export function AccountPage() {
const navigate = useNavigate();
const { user, isAuthenticated, clear } = useAuth();
const showToast = useToastStore((s) => s.show);
const { data: devices, loading: devicesLoading, remove } = useDevices();
const [userInfo, setUserInfo] = useState<UserInfo | null>(user);
const [subscription, setSubscription] = useState<SubscriptionData | null>(null);
const [loadingMeta, setLoadingMeta] = useState(true);
const [pending, setPending] = useState<Device | null>(null);
useEffect(() => {
if (!isAuthenticated) {
navigate('/login', { replace: true, state: { returnTo: '/account' } });
}
}, [isAuthenticated, navigate]);
const loadMeta = useCallback(async () => {
setLoadingMeta(true);
try {
const [me, sub] = await Promise.all([api.getMe(), api.getSubscription()]);
setUserInfo(me);
setSubscription(sub);
} catch (err: unknown) {
const message = err instanceof ApiError ? err.message : 'Не удалось загрузить аккаунт';
showToast(message);
if (err instanceof ApiError && err.status === 401) clear();
} finally {
setLoadingMeta(false);
}
}, [clear, showToast]);
useEffect(() => {
if (!isAuthenticated) return;
void loadMeta();
}, [isAuthenticated, loadMeta]);
const handleDelete = (device: Device) => setPending(device);
const copySubscriptionLink = async () => {
const link = subscription?.subscription_link;
if (!link) return;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(link);
} else {
const input = document.createElement('textarea');
input.value = link;
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
input.select();
document.execCommand('copy');
input.remove();
}
showToast('Ссылка подписки скопирована');
} catch {
showToast('Не удалось скопировать ссылку');
}
};
const confirmDelete = async () => {
if (!pending) return;
const ok = await remove(pending.hwid);
setPending(null);
if (ok) {
showToast(`${pending.model ?? 'Устройство'} отключено`);
} else {
showToast('Не удалось удалить устройство');
}
};
const signOut = () => {
clear();
showToast('Сессия завершена');
navigate('/', { replace: true });
};
return (
<Shell active="account">
<section className="account-hero">
<Reveal>
<div className="kicker">Ваш аккаунт</div>
<h1 className="hero-title">{userInfo?.username ?? 'Аккаунт'}</h1>
{!loadingMeta ? (
<p className="hero-sub">
{subscription?.has_subscription && subscription.expires_at
? `Подписка до ${new Date(subscription.expires_at).toLocaleDateString('ru-RU')}`
: 'Активная подписка отсутствует'}
</p>
) : null}
</Reveal>
</section>
{loadingMeta ? (
<div className="profile">
{Array.from({ length: 4 }).map((_, i) => (
<div className="stat" key={i}>
<Skeleton width="34px" height="34px" radius="50%" />
<Skeleton width="80px" height="10px" />
<Skeleton width="120px" height="26px" />
</div>
))}
</div>
) : (
<Reveal>
<StatStrip user={userInfo} subscription={subscription} deviceCount={devices.length} />
</Reveal>
)}
{!loadingMeta && subscription?.subscription_link ? (
<Reveal>
<section className="subscription-link" aria-label="Ссылка подписки">
<div className="subscription-emblem" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1L11 5.1" />
<path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1" />
</svg>
</div>
<div className="subscription-copy">
<div className="subscription-label">Ссылка подписки</div>
<span className="subscription-url" title={subscription.subscription_link}>
{subscription.subscription_link}
</span>
</div>
<button type="button" className="copy-link" onClick={() => void copySubscriptionLink()}>
Копировать
</button>
</section>
</Reveal>
) : null}
<section>
<Reveal>
<div className="devices-head">
<h2 className="devices-title">Подключённые устройства</h2>
<div className="devices-count">
<strong>{devices.length}</strong>активно
</div>
</div>
</Reveal>
{devicesLoading ? (
<div className="device-grid">
{Array.from({ length: 2 }).map((_, i) => (
<div className="device" key={i}>
<Skeleton width="48px" height="48px" />
<div style={{ flex: 1 }}>
<Skeleton width="70%" height="20px" />
<Skeleton width="50%" height="12px" style={{ marginTop: 8 }} />
</div>
<Skeleton width="36px" height="36px" radius="50%" />
</div>
))}
</div>
) : devices.length === 0 ? (
<div className="devices-empty">
<div className="devices-empty-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="4" width="20" height="13" rx="1.5" />
<path d="M1 20h22M9 20l1-3h4l1 3" />
</svg>
</div>
<div className="devices-empty-title">Нет подключённых устройств</div>
<div className="devices-empty-sub">
Установите Malenia на устройство и подключите его к этому аккаунту.
</div>
</div>
) : (
<div className="device-grid">
{devices.map((d) => (
<DeviceCard key={d.hwid} device={d} onDelete={handleDelete} />
))}
</div>
)}
</section>
<footer className="account-foot">
<span>MaleniaVPN</span>
<button type="button" onClick={signOut}>
Выйти
</button>
</footer>
<Modal
open={pending !== null}
onClose={() => setPending(null)}
kicker="Подтверждение"
title="Удалить это устройство?"
body={
<>
Отключение <strong>{pending?.model ?? 'устройства'}</strong> немедленно завершит его
VPN-сессию. Вы сможете подключить его снова в любой момент.
</>
}
confirmLabel="Удалить"
onConfirm={confirmDelete}
/>
</Shell>
);
}

View File

@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { Landing } from './Landing';
describe('Landing', () => {
it('renders logo, navigation and CTAs', () => {
render(
<MemoryRouter>
<Landing />
</MemoryRouter>,
);
expect(screen.getByRole('link', { name: /Malenia/ })).toHaveAttribute('href', '/');
expect(screen.getAllByRole('link', { name: /Оформить/i }).length).toBeGreaterThan(0);
expect(screen.getAllByRole('link', { name: /Канал/i })[0]).toHaveAttribute(
'href',
'https://t.me/maleniavpn',
);
expect(screen.getAllByRole('link', { name: /Поддержка/i })[0]).toHaveAttribute(
'href',
'https://t.me/maleniasupportbot',
);
});
});

153
src/pages/Landing.tsx Normal file
View File

@@ -0,0 +1,153 @@
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Emblem } from '../components/Emblem';
import { Logo } from '../components/Logo';
const QUOTES: string[] = [
'Верните себе\nсвободный интернет.',
'Свобода должна\nбыть доступной.',
'Интернет без границ —\nне привилегия, а норма.',
'Ваш трафик.\nВаши правила.',
'Тишина в сети —\nэто тоже выбор.',
'Открытый мир\nначинается здесь.',
'Без цензуры.\nБез компромиссов.',
'Простота —\nлучшая форма свободы.',
'Один клик\nмежду вами и миром.',
'Malenia —\nсвобода в форме привычки.',
'Будьте везде.\nОставайтесь невидимым.',
'Приватность — это право,\nа не роскошь.',
'Интернет таким,\nкаким он задумывался.',
'Ваши данные —\nтолько ваше дело.',
'Закрытых дверей\nбольше не существует.',
'Любой сайт.\nИз любой точки мира.',
'Оставьте след в истории,\nа не в браузере.',
'Malenia —\nключ к открытой сети.',
'Фильтры придумали они.\nРешение выбрали вы.',
'Безопасность, которую\nвы не замечаете.',
'Сотрите цифровые границы.\nОстаньтесь собой.',
'Скорость без компромиссов.\nСеть без преград.',
'Включили и забыли.\nВы в безопасности.',
'Блокировки иллюзорны.\nСвобода реальна.',
'Никакой слежки.\nТолько вы и интернет.',
'Доступ к информации —\nваше базовое право.',
'Malenia.\nИнтернет снова ваш.',
'Контроль над сетью\nв ваших руках.',
'Невидимый щит\nдля вашей свободы.',
'Весь мир на экране.\nБез лишних вопросов.',
'Никогда не поздно\nвыбрать свободу.',
'Привычка к запретам —\nповод проснуться.',
'Откройте глаза.\nСтен больше нет.',
'Вспомните, как выглядит\nнастоящая сеть.',
'Запреты стали нормой.\nПора это менять.',
'Свобода начинается\nс вашего решения.',
'Выход из спящего режима.\nСделайте первый шаг.',
'Лучший момент для\nсвободы — сейчас.',
'Иллюзия границ рушится\nв один клик.',
'Хватит мириться с рамками.\nМир шире, чем кажется.',
'Не ждите свободы.\nВключите её.',
'Цифровая амнезия окончена.\nВерните своё.',
'Осознанность начинается\nтам, где кончается цензура.',
'Первый шаг к свободе —\nперестать верить в стены.',
'Malenia.\nВаше цифровое пробуждение.',
];
function TelegramIcon() {
return (
<svg className="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.265 2.428a1.523 1.523 0 0 0-1.547-.232L1.91 9.9a1.523 1.523 0 0 0 .089 2.844l4.498 1.498 1.815 5.784a.761.761 0 0 0 1.295.3l2.617-2.818 4.898 3.674a1.523 1.523 0 0 0 2.389-.98l3.047-16.762a1.523 1.523 0 0 0-.293-1.012zM9.94 13.88l-.98 3.267-.98-3.143L18.5 6.04z" />
</svg>
);
}
function SupportIcon() {
return (
<svg className="btn-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-1 1.732V7h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h3V5.732A2 2 0 0 1 10 4a2 2 0 0 1 2-2zm-3 9a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm6 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z" />
</svg>
);
}
function renderQuote(raw: string): { head: string; tail: string | null } {
const lines = raw.split('\n');
if (lines.length === 2) return { head: lines[0] ?? raw, tail: lines[1] ?? null };
return { head: raw, tail: null };
}
export function Landing() {
const quote = useMemo(() => {
const chosen = QUOTES[Math.floor(Math.random() * QUOTES.length)];
if (!chosen) return null;
return renderQuote(chosen);
}, []);
return (
<div className="landing-page">
<header className="landing-header">
<Logo />
<div className="header-rule" />
<nav aria-label="Верхняя навигация">
<Link to="/plan">Оформить</Link>
<a href="https://t.me/maleniavpn" target="_blank" rel="noopener noreferrer">
Канал
</a>
<a href="https://t.me/maleniasupportbot" target="_blank" rel="noopener noreferrer">
Поддержка
</a>
</nav>
</header>
<main className="landing-hero">
<Emblem size={90} />
{quote ? (
<div className="quote-block">
<p className="quote-text">
{quote.head}
{quote.tail ? (
<>
<br />
<em>{quote.tail}</em>
</>
) : null}
</p>
<p className="quote-author">malenia.space</p>
</div>
) : null}
<div className="landing-divider" aria-hidden="true">
<div className="divider-diamond" />
</div>
<div className="cta-group">
<Link to="/plan" className="btn btn-primary">
<span>Оформить</span>
</Link>
<a
className="btn btn-secondary"
href="https://t.me/maleniavpn"
target="_blank"
rel="noopener noreferrer"
>
<TelegramIcon />
<span>Канал</span>
</a>
<a
className="btn btn-secondary"
href="https://t.me/maleniasupportbot"
target="_blank"
rel="noopener noreferrer"
>
<SupportIcon />
<span>Поддержка</span>
</a>
</div>
</main>
<footer className="landing-footer">
<span className="footer-copy">© 2026 Malenia</span>
<div className="footer-dot" />
<span className="footer-copy">malenia.space</span>
</footer>
</div>
);
}

View File

@@ -0,0 +1,99 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { LoginPage } from './LoginPage';
import { api, ApiError } from '../api/client';
import { useAuthStore } from '../store/useStore';
vi.mock('../api/client', () => ({
api: { login: vi.fn(), signup: vi.fn(), getPlans: vi.fn() },
ApiError: class ApiError extends Error {
constructor(m: string, public status: number) {
super(m);
}
},
setStoredTokens: vi.fn(),
clearStoredTokens: vi.fn(),
}));
function renderLogin() {
return render(
<MemoryRouter initialEntries={['/login']}>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/account" element={<div>account page</div>} />
</Routes>
</MemoryRouter>,
);
}
describe('LoginPage', () => {
beforeEach(() => {
useAuthStore.setState({
access: null,
refresh: null,
expiresAt: null,
user: null,
remember: true,
});
vi.mocked(api.login).mockReset();
vi.mocked(api.signup).mockReset();
});
afterEach(() => vi.restoreAllMocks());
it('shows validation error when fields empty', async () => {
const user = userEvent.setup();
renderLogin();
await user.click(screen.getByRole('button', { name: /Войти/i }));
expect(await screen.findByText(/обязательны/i)).toBeInTheDocument();
expect(api.login).not.toHaveBeenCalled();
}); it('submits login and redirects to /account by default', async () => {
const user = userEvent.setup();
vi.mocked(api.login).mockResolvedValue({
access_token: 'a',
refresh_token: 'r',
expires_at: 9999999999,
user: { username: 'alice', telegram_id: null, referal_code: 'c' },
});
renderLogin();
await user.type(screen.getByLabelText('Имя пользователя'), 'alice');
await user.type(screen.getByLabelText('Пароль'), 'secret');
await user.click(screen.getByRole('button', { name: /Войти/i }));
await waitFor(() => expect(api.login).toHaveBeenCalledWith({
provider: 'credentials',
username: 'alice',
password: 'secret',
}));
await waitFor(() => expect(screen.getByText('account page')).toBeInTheDocument());
});
it('toggles password visibility', async () => {
const user = userEvent.setup();
renderLogin();
const pass = screen.getByLabelText('Пароль') as HTMLInputElement;
await user.type(pass, 'secret');
expect(pass.type).toBe('password');
await user.click(screen.getByRole('button', { name: /показать пароль/i }));
expect(pass.type).toBe('text');
});
it('switches to signup and shows referral field', async () => {
const user = userEvent.setup();
renderLogin();
await user.click(screen.getByRole('tab', { name: /Регистрация/i }));
expect(screen.getByRole('heading', { name: /создать аккаунт/i })).toBeInTheDocument();
expect(screen.getByLabelText(/реферальный код/i)).toBeInTheDocument();
});
it('shows backend error on login failure', async () => {
const user = userEvent.setup();
vi.mocked(api.login).mockRejectedValue(new ApiError('неверные данные', 401));
renderLogin();
await user.type(screen.getByLabelText('Имя пользователя'), 'a');
await user.type(screen.getByLabelText('Пароль'), 'secret');
await user.click(screen.getByRole('button', { name: /Войти/i }));
expect(await screen.findByText('неверные данные')).toBeInTheDocument();
});
});

278
src/pages/LoginPage.tsx Normal file
View File

@@ -0,0 +1,278 @@
import { useEffect, useState, type FormEvent } from 'react';
import { useLocation, useNavigate, Link } from 'react-router-dom';
import { api, ApiError } from '../api/client';
import { useAuth } from '../hooks/useAuth';
import { useToastStore } from '../store/useToast';
import { Emblem } from '../components/Emblem';
import { Logo } from '../components/Logo';
import { Reveal } from '../components/Reveal';
type Mode = 'signin' | 'signup';
function EyeIcon({ open }: { open: boolean }) {
if (open) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 3l18 18M10.5 10.7a3 3 0 0 0 4 4M9.4 5.2A10.6 10.6 0 0 1 12 5c6.5 0 10 7 10 7a17.3 17.3 0 0 1-3.2 4.1M6.1 6.2A17.3 17.3 0 0 0 2 12s3.5 7 10 7c1.4 0 2.7-.3 3.8-.7" />
</svg>
);
}
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
interface LocationState {
returnTo?: string;
}
function returnPath(value: string | null | undefined): string | null {
if (!value || !value.startsWith('/') || value.startsWith('//')) return null;
return value;
}
export function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { isAuthenticated, setSession } = useAuth();
const showToast = useToastStore((s) => s.show);
const [mode, setMode] = useState<Mode>('signin');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [referral, setReferral] = useState('');
const [remember, setRemember] = useState(true);
const [showPass, setShowPass] = useState(false);
const [status, setStatus] = useState<string>('');
const [statusKind, setStatusKind] = useState<'idle' | 'error' | 'info'>('idle');
const [submitting, setSubmitting] = useState(false);
const queryReturnTo = new URLSearchParams(location.search).get('returnTo');
const returnTo =
returnPath(queryReturnTo) ?? returnPath((location.state as LocationState | null)?.returnTo) ?? '/account';
useEffect(() => {
if (isAuthenticated) navigate(returnTo, { replace: true });
}, [isAuthenticated, navigate, returnTo]);
const setStatusMsg = (msg: string, kind: 'error' | 'info') => {
setStatus(msg);
setStatusKind(kind);
};
const switchMode = (next: Mode) => {
setMode(next);
setStatus('');
setStatusKind('idle');
};
const submit = async (e: FormEvent) => {
e.preventDefault();
if (!username.trim() || !password) {
setStatusMsg('Имя пользователя и пароль обязательны.', 'error');
return;
}
if (mode === 'signup' && password.length < 4) {
setStatusMsg('Пароль слишком короткий (минимум 4 символа).', 'error');
return;
}
setSubmitting(true);
setStatusMsg('Проверяем…', 'info');
try {
if (mode === 'signin') {
const res = await api.login({
provider: 'credentials',
username: username.trim(),
password,
});
setSession(res, remember);
showToast(`Здравствуйте, ${res.user.username ?? username.trim()}`);
navigate(returnTo, { replace: true });
} else {
const res = await api.signup({
provider: 'credentials',
username: username.trim(),
password,
referal_code: referral.trim() || null,
});
setSession(res, remember);
showToast('Аккаунт создан');
navigate(returnTo, { replace: true });
}
} catch (err: unknown) {
const message = err instanceof ApiError ? err.message : 'Не удалось связаться с сервером';
setStatusMsg(message, 'error');
} finally {
setSubmitting(false);
}
};
return (
<div className="auth-shell">
<Reveal className="auth-topbar">
<Logo to="/" />
<div className="kicker">Private access</div>
</Reveal>
<main className="auth-card">
<Reveal>
<div className="auth-emblem">
<Emblem size={44} />
</div>
</Reveal>
<Reveal>
<div className="auth-tabs" role="tablist" aria-label="Режим авторизации">
<button
type="button"
role="tab"
aria-selected={mode === 'signin'}
className={`auth-tab ${mode === 'signin' ? 'is-active' : ''}`.trim()}
onClick={() => switchMode('signin')}
>
Вход
</button>
<button
type="button"
role="tab"
aria-selected={mode === 'signup'}
className={`auth-tab ${mode === 'signup' ? 'is-active' : ''}`.trim()}
onClick={() => switchMode('signup')}
>
Регистрация
</button>
</div>
</Reveal>
<Reveal>
<h1 className="auth-title">{mode === 'signin' ? 'С возвращением' : 'Создать аккаунт'}</h1>
<p className="auth-sub">
{mode === 'signin'
? 'Введите данные для продолжения сессии.'
: 'Логин и пароль — этого достаточно.'}
</p>
</Reveal>
<Reveal>
<form className="auth-form" onSubmit={submit} noValidate>
<div className="field">
<label className="field-label" htmlFor="username">
Имя пользователя
</label>
<input
id="username"
name="username"
className="field-input"
type="text"
autoComplete="username"
placeholder="ваш логин"
spellCheck={false}
required
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<span className="field-line" />
</div>
<div className="field">
<label className="field-label" htmlFor="password">
Пароль
</label>
<input
id="password"
name="password"
className="field-input"
type={showPass ? 'text' : 'password'}
autoComplete={mode === 'signin' ? 'current-password' : 'new-password'}
placeholder="••••••••"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<span className="field-line" />
<button
type="button"
className="field-icon"
aria-label={showPass ? 'Скрыть пароль' : 'Показать пароль'}
aria-pressed={showPass}
onClick={() => setShowPass((v) => !v)}
>
<EyeIcon open={showPass} />
</button>
</div>
{mode === 'signup' ? (
<div className="field">
<label className="field-label" htmlFor="referral">
Реферальный код (необязательно)
</label>
<input
id="referral"
name="referral"
className="field-input"
type="text"
autoComplete="off"
placeholder="код от друга"
spellCheck={false}
value={referral}
onChange={(e) => setReferral(e.target.value)}
/>
<span className="field-line" />
</div>
) : (
<div className="auth-row">
<label className="check">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
/>
<span>Запомнить меня</span>
</label>
</div>
)}
<button type="submit" className="btn btn-primary btn-block" disabled={submitting}>
{submitting ? 'Сохранение…' : mode === 'signin' ? 'Войти' : 'Создать аккаунт'}
</button>
<div
className={`auth-status ${statusKind === 'error' ? 'is-error' : ''} ${statusKind === 'info' ? 'is-info' : ''}`.trim()}
role="status"
aria-live="polite"
>
{status}
</div>
</form>
</Reveal>
<Reveal>
<p className="auth-signup">
{mode === 'signin' ? (
<>
Нет аккаунта?{' '}
<Link to="/login" className="link" onClick={() => switchMode('signup')}>
Создать
</Link>
</>
) : (
<>
Уже зарегистрированы?{' '}
<Link to="/login" className="link" onClick={() => switchMode('signin')}>
Войти
</Link>
</>
)}
</p>
</Reveal>
</main>
<Reveal className="auth-foot">
Защищено MaleniaVPN
</Reveal>
</div>
);
}

133
src/pages/PlanPage.test.tsx Normal file
View File

@@ -0,0 +1,133 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { PlanPage } from './PlanPage';
import { api } from '../api/client';
import { useAuthStore } from '../store/useStore';
import { redirectTo } from '../lib/redirect';
vi.mock('../api/client', () => ({
api: {
getPlans: vi.fn(),
checkout: vi.fn(),
getMe: vi.fn(),
getSubscription: vi.fn(),
getDevices: vi.fn(),
deleteDevice: vi.fn(),
login: vi.fn(),
signup: vi.fn(),
},
ApiError: class ApiError extends Error {
constructor(m: string, public status: number) {
super(m);
}
},
setStoredTokens: vi.fn(),
clearStoredTokens: vi.fn(),
}));
vi.mock('../lib/redirect', () => ({ redirectTo: vi.fn() }));
function renderPlan(initialEntries: string[] = ['/plan']) {
return render(
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="/plan" element={<PlanPage />} />
<Route path="/login" element={<div>login page</div>} />
</Routes>
</MemoryRouter>,
);
}
const PLANS = {
device_price: 100,
addons: [
{ id: 'dedicated-ip', name: 'Dedicated IP', price: 50, free_threshold: 0 },
{ id: 'multi-hop', name: 'Multi-Hop', price: 30, free_threshold: 0 },
],
};
describe('PlanPage', () => {
beforeEach(() => {
vi.mocked(api.getPlans).mockReset();
vi.mocked(api.checkout).mockReset();
useAuthStore.setState({
access: null,
refresh: null,
expiresAt: null,
user: null,
remember: true,
});
});
afterEach(() => vi.restoreAllMocks());
it('renders readout and breakdown from backend pricing', async () => {
vi.mocked(api.getPlans).mockResolvedValue(PLANS);
renderPlan();
// default 6 devices -> monthly 600
expect((await screen.findAllByText(/600/)).length).toBeGreaterThan(0);
expect(api.getPlans).toHaveBeenCalledOnce();
});
it('redirects to /login when confirming without auth', async () => {
vi.mocked(api.getPlans).mockResolvedValue(PLANS);
const user = userEvent.setup();
renderPlan();
await screen.findAllByText(/600/);
await user.click(screen.getByRole('button', { name: /^Оформить$/i }));
await waitFor(() => expect(screen.getByText('login page')).toBeInTheDocument());
});
it('creates checkout with chosen config when authed', async () => {
vi.mocked(api.getPlans).mockResolvedValue(PLANS);
vi.mocked(api.checkout).mockResolvedValue({
order_id: 'ord-9',
total_amount: 650,
bonus_paid: 0,
amount_to_pay: 650,
payment_link: null,
});
useAuthStore.setState({
access: 'a',
refresh: 'r',
expiresAt: 9999999999,
user: { username: 'u', telegram_id: null, referal_code: 'c' },
remember: true,
});
const user = userEvent.setup();
renderPlan();
await screen.findAllByText(/600/);
// select an addon -> 600 + 50 = 650
await user.click(screen.getByRole('button', { name: /Dedicated IP/i }));
await user.click(screen.getByRole('button', { name: /^Оформить$/i }));
await waitFor(() => expect(api.checkout).toHaveBeenCalledOnce());
const arg = vi.mocked(api.checkout).mock.calls[0]?.[0];
expect(arg).toMatchObject({ devices: 6, addons: ['dedicated-ip'], duration_days: 30 });
});
it('redirects to payment link when checkout returns one', async () => {
vi.mocked(api.getPlans).mockResolvedValue(PLANS);
vi.mocked(api.checkout).mockResolvedValue({
order_id: 'ord-9',
total_amount: 600,
bonus_paid: 0,
amount_to_pay: 600,
payment_link: 'https://pay.example/abc',
});
useAuthStore.setState({
access: 'a',
refresh: 'r',
expiresAt: 9999999999,
user: { username: 'u', telegram_id: null, referal_code: 'c' },
remember: true,
});
const user = userEvent.setup();
renderPlan();
await screen.findAllByText(/600/);
await user.click(screen.getByRole('button', { name: /^Оформить$/i }));
await waitFor(() => expect(api.checkout).toHaveBeenCalledOnce());
expect(redirectTo).toHaveBeenCalledWith('https://pay.example/abc');
});
});

229
src/pages/PlanPage.tsx Normal file
View File

@@ -0,0 +1,229 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { api, ApiError } from '../api/client';
import { useAuth } from '../hooks/useAuth';
import { usePlans } from '../hooks/usePlans';
import { useToastStore } from '../store/useToast';
import { Shell } from '../components/Shell';
import { Reveal } from '../components/Reveal';
import { Skeleton } from '../components/Skeleton';
import { DeviceSlider } from '../components/DeviceSlider';
import { DurationSelect } from '../components/DurationSelect';
import { AddonList } from '../components/AddonList';
import { Breakdown } from '../components/Breakdown';
import {
calculatePricing,
devicesLabel,
durationByDays,
formatMoney,
} from '../lib/format';
import { MAX_DEVICES, MIN_DEVICES, resolvePlanConfig, savePlanConfig } from '../lib/planConfig';
import { redirectTo } from '../lib/redirect';
import type { CheckoutResponse } from '../types';
export function PlanPage() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { isAuthenticated } = useAuth();
const showToast = useToastStore((s) => s.show);
const { data: plans, loading, error } = usePlans();
const initialConfig = useMemo(() => resolvePlanConfig(searchParams), [searchParams]);
const [devices, setDevices] = useState<number>(initialConfig.devices);
const [durationDays, setDurationDays] = useState<number>(initialConfig.durationDays);
const [selectedAddons, setSelectedAddons] = useState<string[]>(initialConfig.addons);
const [submitting, setSubmitting] = useState(false);
const [confirmation, setConfirmation] = useState<CheckoutResponse | null>(null);
useEffect(() => {
if (!plans) return;
const available = new Set(plans.addons.map((addon) => addon.id));
setSelectedAddons((current) => current.filter((addon) => available.has(addon)));
}, [plans]);
useEffect(() => {
const next = new URLSearchParams();
next.set('devices', String(devices));
next.set('duration', String(durationDays));
if (selectedAddons.length > 0) next.set('addons', selectedAddons.join(','));
setSearchParams(next, { replace: true });
savePlanConfig({ devices, durationDays, addons: selectedAddons });
}, [devices, durationDays, selectedAddons, setSearchParams]);
const duration = useMemo(() => durationByDays(durationDays), [durationDays]);
const pricing = useMemo(() => {
if (!plans) return null;
return calculatePricing({
pricing: plans,
devices,
addonIds: selectedAddons,
duration,
});
}, [plans, devices, selectedAddons, duration]);
const toggleAddon = (id: string) => {
setSelectedAddons((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
};
const handleConfirm = async () => {
if (!plans || !pricing) return;
if (!isAuthenticated) {
const returnTo = `/plan?${searchParams.toString()}`;
navigate(`/login?returnTo=${encodeURIComponent(returnTo)}`);
return;
}
setSubmitting(true);
try {
const res = await api.checkout({
devices,
addons: selectedAddons,
duration_days: durationDays,
});
if (res.payment_link) {
redirectTo(res.payment_link);
return;
}
setConfirmation(res);
showToast('Заказ оформлен');
} catch (err: unknown) {
const message = err instanceof ApiError ? err.message : 'Не удалось оформить заказ';
showToast(message);
} finally {
setSubmitting(false);
}
};
return (
<Shell active="plan">
<section className="plan-hero">
<Reveal>
<div className="kicker">Соберите свой план</div>
<h1 className="hero-title">Круг, подогнанный под вас.</h1>
<p className="hero-sub">
Выберите, сколько устройств разделят вашу свободу. Цена меняется тихо — фиксированная
ставка за каждое, без уровней и наценок.
</p>
</Reveal>
</section>
{confirmation ? (
<section className="checkout-confirmation">
<Reveal>
<div className="kicker">Заказ оформлен</div>
<h2 className="title">Оплата получена</h2>
<p className="body">
Сумма списана из бонусного баланса. Подписка активируется автоматически — мы пришлём
уведомление в канал.
</p>
<div className="checkout-amount">{formatMoney(confirmation.amount_to_pay)}</div>
<div className="checkout-order">Заказ №{confirmation.order_id}</div>
</Reveal>
</section>
) : (
<section className="plan-calc">
{loading || !plans || !pricing ? (
<PlanSkeleton loading={loading} error={error} />
) : (
<>
<Reveal>
<div className="plan-calc-stage">
<div className="calc-readout">
<div className="readout-label">Оценка · за месяц</div>
<div className="readout-value">
<span>{formatMoney(pricing.monthly)}</span>
<span className="unit">₽ / мес</span>
</div>
<div className="readout-note">
{devicesLabel(devices)} · {formatMoney(plans.device_price)} за устройство
</div>
</div>
<DeviceSlider
min={MIN_DEVICES}
max={MAX_DEVICES}
value={devices}
onChange={setDevices}
/>
</div>
</Reveal>
<Reveal>
<DurationSelect days={durationDays} onChange={setDurationDays} />
</Reveal>
<Reveal>
<Breakdown
devices={devices}
devicePrice={pricing.deviceCost}
addonCount={selectedAddons.length}
addonSum={pricing.addonCost}
duration={duration}
monthly={pricing.monthly}
periodTotal={pricing.periodTotal}
/>
</Reveal>
<Reveal>
<AddonList
addons={plans.addons}
selected={selectedAddons}
onToggle={toggleAddon}
/>
</Reveal>
<Reveal>
<div className="plan-cta">
<button
type="button"
className="btn btn-primary"
disabled={submitting}
onClick={handleConfirm}
>
{submitting ? 'Сохранение…' : 'Оформить'}
</button>
<div className="cta-note">
{isAuthenticated
? 'План вступит в силу при продлении · без блокировки'
: 'Потребуется войти, чтобы оформить заказ'}
</div>
</div>
</Reveal>
</>
)}
</section>
)}
<footer className="plan-foot">
<span>MaleniaVPN</span>
<span>Предварительная оценка</span>
</footer>
</Shell>
);
}
function PlanSkeleton({ loading, error }: { loading: boolean; error: string | null }) {
if (error) {
return (
<div className="plan-calc-stage" style={{ textAlign: 'center', padding: '64px 0' }}>
<p className="readout-note">{error}</p>
</div>
);
}
if (!loading) return null;
return (
<div className="plan-calc-stage" style={{ gap: 56 }}>
<div className="calc-readout">
<Skeleton width="200px" height="14px" />
<Skeleton width="280px" height="96px" style={{ margin: '18px auto' }} />
<Skeleton width="220px" height="14px" />
</div>
<div>
<Skeleton width="100%" height="14px" />
<Skeleton width="100%" height="88px" style={{ margin: '22px 0' }} />
<Skeleton width="100%" height="14px" />
</div>
<Skeleton width="100%" height="120px" />
<Skeleton width="100%" height="220px" />
</div>
);
}

106
src/store/useStore.ts Normal file
View File

@@ -0,0 +1,106 @@
import { create } from 'zustand';
import type { LoginResponse, Tokens, UserInfo } from '../types';
import { clearStoredTokens, setStoredTokens } from '../api/client';
interface AuthState {
access: string | null;
refresh: string | null;
expiresAt: number | null;
user: UserInfo | null;
remember: boolean;
setSession: (session: LoginResponse, remember: boolean) => void;
setTokens: (tokens: Tokens) => void;
updateUser: (user: UserInfo) => void;
setRemember: (remember: boolean) => void;
hydrate: () => void;
clear: () => void;
}
const ACCESS_KEY = 'malenia.access';
const REFRESH_KEY = 'malenia.refresh';
const EXPIRES_KEY = 'malenia.expires_at';
const USER_KEY = 'malenia.user';
const REMEMBER_KEY = 'malenia.remember';
export const useAuthStore = create<AuthState>((set, get) => ({
access: null,
refresh: null,
expiresAt: null,
user: null,
remember: true,
setSession: (session, remember) => {
setStoredTokens({
access_token: session.access_token,
refresh_token: session.refresh_token,
expires_at: session.expires_at,
});
try {
localStorage.setItem(USER_KEY, JSON.stringify(session.user));
localStorage.setItem(REMEMBER_KEY, remember ? '1' : '0');
} catch {
/* ignore */
}
set({
access: session.access_token,
refresh: session.refresh_token,
expiresAt: session.expires_at,
user: session.user,
remember,
});
},
setTokens: (tokens) => {
set({
access: tokens.access_token,
refresh: tokens.refresh_token,
expiresAt: tokens.expires_at,
});
},
updateUser: (user) => {
try {
localStorage.setItem(USER_KEY, JSON.stringify(user));
} catch {
/* ignore */
}
set({ user });
},
setRemember: (remember) => set({ remember }),
hydrate: () => {
try {
const access = localStorage.getItem(ACCESS_KEY);
const refresh = localStorage.getItem(REFRESH_KEY);
const expiresRaw = localStorage.getItem(EXPIRES_KEY);
const userRaw = localStorage.getItem(USER_KEY);
const rememberRaw = localStorage.getItem(REMEMBER_KEY);
const expiresAt = expiresRaw ? Number(expiresRaw) : null;
const user = userRaw ? (JSON.parse(userRaw) as UserInfo) : null;
set({
access,
refresh,
expiresAt,
user,
remember: rememberRaw !== '0',
});
// If "remember" is off and tokens are expired, drop them.
if (rememberRaw === '0' && expiresAt !== null && expiresAt * 1000 < Date.now()) {
get().clear();
}
} catch {
/* ignore */
}
},
clear: () => {
clearStoredTokens();
try {
localStorage.removeItem(USER_KEY);
} catch {
/* ignore */
}
set({ access: null, refresh: null, expiresAt: null, user: null });
},
}));

24
src/store/useToast.ts Normal file
View File

@@ -0,0 +1,24 @@
import { create } from 'zustand';
interface ToastItem {
id: number;
message: string;
}
interface ToastState {
toasts: ToastItem[];
show: (message: string) => void;
dismiss: (id: number) => void;
}
let counter = 0;
export const useToastStore = create<ToastState>((set, get) => ({
toasts: [],
show: (message) => {
const id = ++counter;
set((s) => ({ toasts: [...s.toasts, { id, message }] }));
window.setTimeout(() => get().dismiss(id), 2200);
},
dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })),
}));

93
src/styles/base.css Normal file
View File

@@ -0,0 +1,93 @@
@import './tokens.css';
/* ── Grain overlay ───────────────────────────────────── */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 9998;
pointer-events: none;
opacity: 0.035;
mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 180px;
}
/* ── Custom cursor ───────────────────────────────────── */
.cursor-dot,
.cursor-ring {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: opacity 0.3s;
}
.cursor-dot {
width: 6px;
height: 6px;
background: var(--accent);
border-radius: 50%;
}
.cursor-ring {
width: 32px;
height: 32px;
border: 1px solid var(--accent);
border-radius: 50%;
opacity: 0.6;
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), height 0.25s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.25s, opacity 0.25s;
}
.cursor-ring.is-active {
width: 48px;
height: 48px;
border-color: var(--accent-light);
opacity: 0.9;
}
body.is-hovering .cursor-ring {
width: 48px;
height: 48px;
border-color: var(--accent-light);
}
@media (hover: none), (pointer: coarse) {
.cursor-dot,
.cursor-ring {
display: none;
}
* {
cursor: auto !important;
}
}
@media (hover: hover) and (pointer: fine) {
body,
button,
a,
input,
[data-cursor] {
cursor: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cursor-ring.is-active {
transition: none;
}
}
/* ── Reveal animation ──────────────────────────────────── */
.reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.is-in {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
transform: none;
}
}

1177
src/styles/components.css Normal file

File diff suppressed because it is too large Load Diff

535
src/styles/pages.css Normal file
View File

@@ -0,0 +1,535 @@
/* ── Landing ─────────────────────────────────────────────── */
.landing-page {
position: relative;
z-index: 1;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
.landing-header {
padding: 2.5rem 4rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.landing-header .header-rule {
flex: 1;
height: 1px;
background: var(--border);
margin: 0 2rem;
}
.landing-header nav {
display: flex;
gap: 28px;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
.landing-header nav a {
position: relative;
padding: 4px 0;
transition: color 0.3s;
}
.landing-header nav a:hover {
color: var(--fg);
}
.header-tag {
font-size: 0.65rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--fg-muted);
}
.landing-hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6rem 2rem;
gap: 3.5rem;
}
.landing-hero .emblem {
width: 90px;
height: 90px;
}
.quote-block {
max-width: 640px;
}
.quote-text {
font-family: var(--font-display);
font-size: clamp(2rem, 5vw, 3.4rem);
font-weight: 300;
line-height: 1.25;
color: var(--fg);
letter-spacing: 0.01em;
}
.quote-text em {
font-style: italic;
color: var(--accent-dim);
}
.quote-author {
margin-top: 1.25rem;
font-size: 0.7rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: var(--fg-muted);
}
.landing-divider {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 480px;
}
.landing-divider::before,
.landing-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.cta-group {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
justify-content: center;
}
.landing-footer {
padding: 2rem 4rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-copy {
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--fg-muted);
}
.footer-dot {
width: 4px;
height: 4px;
background: var(--accent);
border-radius: 50%;
}
@media (max-width: 640px) {
.landing-header {
padding: 1.75rem 1.5rem;
flex-direction: column;
justify-content: center;
gap: 1.5rem;
}
.landing-header .header-rule {
display: none;
}
.landing-header .header-tag {
display: none;
}
.landing-header nav {
width: 100%;
justify-content: space-between;
gap: 16px;
font-size: 10px;
letter-spacing: 0.14em;
}
.landing-hero {
padding: 4rem 1.5rem;
gap: 2.5rem;
}
.landing-footer {
padding: 1.5rem;
flex-direction: column;
align-items: flex-start;
}
.cta-group {
flex-direction: column;
align-items: stretch;
}
.cta-group .btn {
justify-content: center;
}
}
/* ── Auth (login) ───────────────────────────────────────── */
.auth-shell {
max-width: 460px;
margin: 0 auto;
padding: 56px 32px 48px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.auth-topbar {
text-align: center;
}
.auth-topbar .kicker {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-top: 12px;
}
.auth-emblem {
display: flex;
justify-content: center;
margin-bottom: 28px;
color: var(--accent);
}
.auth-emblem svg {
width: 44px;
height: 44px;
}
.auth-card {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
}
.auth-kicker {
text-align: center;
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 18px;
}
.auth-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 36px;
line-height: 1.2;
letter-spacing: -0.01em;
text-align: center;
color: var(--fg);
margin-bottom: 8px;
}
.auth-sub {
text-align: center;
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
margin-bottom: 40px;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 22px;
}
.auth-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
font-size: 13px;
}
.auth-divider {
display: flex;
align-items: center;
gap: 14px;
margin: 32px 0 22px;
color: var(--muted);
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
}
.auth-divider::before,
.auth-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--hairline);
}
.auth-signup {
text-align: center;
margin-top: 36px;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
.auth-signup .link {
color: var(--fg);
}
.auth-status {
text-align: center;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--muted);
margin-top: 16px;
opacity: 0;
transition: opacity 0.25s;
min-height: 14px;
}
.auth-status.is-error {
color: var(--fg);
opacity: 1;
}
.auth-status.is-info {
opacity: 1;
}
.auth-foot {
text-align: center;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
opacity: 0.6;
padding-top: 40px;
}
.auth-tabs {
display: flex;
margin-bottom: 32px;
border: 1px solid var(--hairline);
border-radius: var(--radius);
overflow: hidden;
}
.auth-tab {
flex: 1;
padding: 12px 10px;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
background: var(--surface);
transition: background 0.25s, color 0.25s;
}
.auth-tab.is-active {
background: var(--fg);
color: var(--bg);
}
@media (max-width: 480px) {
.auth-shell {
padding: 40px 22px 36px;
}
.auth-title {
font-size: 30px;
}
}
/* ── Plan shell ─────────────────────────────────────────── */
.plan-shell,
.account-shell {
max-width: 1080px;
margin: 0 auto;
padding: 64px 32px 96px;
}
.plan-hero,
.account-hero {
padding: 64px 0 40px;
text-align: center;
border-bottom: 1px solid var(--hairline);
}
.plan-hero .kicker,
.account-hero .kicker {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 22px;
}
.plan-hero .hero-title,
.account-hero .hero-title {
font-family: var(--font-display);
font-weight: 500;
font-size: 52px;
line-height: 1.12;
letter-spacing: -0.02em;
color: var(--fg);
}
.plan-hero .hero-sub,
.account-hero .hero-sub {
margin: 18px auto 0;
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
max-width: 46ch;
line-height: 1.7;
}
.plan-calc {
padding: 72px 0 64px;
}
.plan-calc-stage {
display: grid;
grid-template-columns: 1fr;
gap: 56px;
align-items: center;
}
.calc-readout {
text-align: center;
}
.readout-label {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 18px;
}
.readout-value {
font-family: var(--font-display);
font-weight: 500;
font-size: 96px;
line-height: 1;
letter-spacing: -0.03em;
color: var(--fg);
display: inline-flex;
align-items: baseline;
gap: 4px;
}
.readout-value .unit {
font-family: var(--font-body);
font-size: 16px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
margin-left: 14px;
align-self: center;
}
.readout-note {
margin-top: 16px;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.02em;
}
.plan-duration {
margin-top: 56px;
}
.plan-breakdown {
margin-top: 64px;
border-top: 1px solid var(--hairline);
}
.plan-addons {
margin-top: 64px;
}
.plan-cta {
margin-top: 56px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
text-align: center;
}
.cta-note {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
.plan-foot,
.account-foot {
margin-top: 64px;
padding-top: 28px;
border-top: 1px solid var(--hairline);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}
.plan-foot button {
font: inherit;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
transition: color 0.25s;
}
.plan-foot button:hover {
color: var(--fg);
}
@media (max-width: 860px) {
.readout-value {
font-size: 76px;
}
.plan-hero .hero-title,
.account-hero .hero-title {
font-size: 42px;
}
}
@media (max-width: 560px) {
.plan-shell,
.account-shell {
padding: 40px 20px 64px;
}
.plan-hero,
.account-hero {
padding: 44px 0 32px;
}
.plan-hero .hero-title,
.account-hero .hero-title {
font-size: 34px;
}
.plan-calc {
padding: 48px 0 40px;
}
.readout-value {
font-size: 60px;
}
.readout-value .unit {
font-size: 13px;
margin-left: 10px;
}
.breakdown-total .breakdown-val {
font-size: 30px;
}
.plan-foot,
.account-foot {
flex-direction: column;
gap: 10px;
text-align: center;
}
.plan-cta .btn {
width: 100%;
}
}
/* ── Account hero specifics ─────────────────────────────── */
.account-hero .hero-sub {
max-width: none;
}
.checkout-confirmation {
text-align: center;
padding: 96px 0 64px;
}
.checkout-confirmation .kicker {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 22px;
}
.checkout-confirmation .title {
font-family: var(--font-display);
font-weight: 500;
font-size: 44px;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--fg);
margin-bottom: 14px;
}
.checkout-confirmation .body {
font-size: 14px;
color: var(--muted);
letter-spacing: 0.02em;
max-width: 46ch;
margin: 0 auto 32px;
}
.checkout-amount {
font-family: var(--font-display);
font-weight: 500;
font-size: 56px;
line-height: 1;
color: var(--fg);
letter-spacing: -0.02em;
margin-bottom: 32px;
}
.checkout-order {
font-size: 12px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
}

79
src/styles/tokens.css Normal file
View File

@@ -0,0 +1,79 @@
:root {
--bg: #faf7f2;
--surface: #f5f0e8;
--fg: #1a1610;
--muted: #3d3428;
--fg-muted: #7a6e5f;
--accent: #c9a84c;
--accent-light: #e8ca7a;
--accent-dim: #8b6914;
--hairline: rgba(26, 22, 16, 0.1);
--hairline-strong: rgba(26, 22, 16, 0.18);
--border: rgba(201, 168, 76, 0.25);
--font-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
--font-body: 'Tenor Sans', 'Helvetica Neue', Arial, sans-serif;
--radius: 8px;
--baseline: 8px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
body {
overflow-x: hidden;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
}
input {
font: inherit;
color: inherit;
}
img,
svg {
display: block;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
}
}

8
src/test/setup.ts Normal file
View File

@@ -0,0 +1,8 @@
import '@testing-library/jest-dom/vitest';
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup();
localStorage.clear();
});

78
src/types.ts Normal file
View File

@@ -0,0 +1,78 @@
export type Provider = 'credentials' | 'telegram' | 'api';
export interface UserInfo {
username: string | null;
telegram_id: string | null;
referal_code: string;
}
export interface LoginResponse {
access_token: string;
refresh_token: string;
expires_at: number;
user: UserInfo;
}
export interface Tokens {
access_token: string;
refresh_token: string;
expires_at: number;
}
export interface AddonData {
id: string;
name: string;
price: number;
free_threshold: number;
}
export interface PricingPlans {
device_price: number;
addons: AddonData[];
}
export interface OrderDetails {
devices: number;
addons: string[];
duration_days: number;
}
export interface CheckoutResponse {
order_id: string;
total_amount: number;
bonus_paid: number;
amount_to_pay: number;
payment_link: string | null;
}
export interface Device {
os: string | null;
model: string | null;
client: string | null;
hwid: string;
}
export interface SubscriptionData {
devices: number | null;
expires_at: string | null;
addon_ids: string[];
subscription_link: string | null;
has_subscription?: boolean;
}
export interface OperationResult {
success: boolean;
}
export interface SignupPayload {
provider: 'credentials';
username: string;
password: string;
referal_code?: string | null;
}
export interface LoginPayload {
provider: 'credentials';
username: string;
password: string;
}

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": false,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client", "vitest/globals", "vitest/config", "@testing-library/jest-dom"]
},
"include": ["src"]
}

File diff suppressed because one or more lines are too long

1
tsconfig.tsbuildinfo Normal file
View File

@@ -0,0 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/types.ts","./src/api/client.test.ts","./src/api/client.ts","./src/components/AddonList.tsx","./src/components/Breakdown.tsx","./src/components/Cursor.tsx","./src/components/DeviceCard.tsx","./src/components/DeviceSlider.tsx","./src/components/DurationSelect.tsx","./src/components/Emblem.tsx","./src/components/Logo.tsx","./src/components/Modal.tsx","./src/components/Reveal.tsx","./src/components/Shell.tsx","./src/components/Skeleton.tsx","./src/components/StatStrip.tsx","./src/components/Toast.tsx","./src/components/TopBar.tsx","./src/hooks/useAuth.ts","./src/hooks/useDevices.ts","./src/hooks/useLocalStorage.ts","./src/hooks/usePlans.ts","./src/lib/format.test.ts","./src/lib/format.ts","./src/lib/planConfig.test.ts","./src/lib/planConfig.ts","./src/lib/redirect.ts","./src/pages/AccountPage.test.tsx","./src/pages/AccountPage.tsx","./src/pages/Landing.test.tsx","./src/pages/Landing.tsx","./src/pages/LoginPage.test.tsx","./src/pages/LoginPage.tsx","./src/pages/PlanPage.test.tsx","./src/pages/PlanPage.tsx","./src/store/useStore.ts","./src/store/useToast.ts","./src/test/setup.ts"],"version":"5.9.3"}

22
vite.config.ts Normal file
View File

@@ -0,0 +1,22 @@
/// <reference types="vitest/config" />
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/auth': { target: 'http://127.0.0.1:8000', changeOrigin: true },
'/plans': { target: 'http://127.0.0.1:8000', changeOrigin: true },
'/orders': { target: 'http://127.0.0.1:8000', changeOrigin: true },
'/users': { target: 'http://127.0.0.1:8000', changeOrigin: true },
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
css: false,
},
});