v0.0.01b
This commit is contained in:
69
.design/DESIGN.md
Normal file
69
.design/DESIGN.md
Normal 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
106
.design/brand.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
.design/fonts/cormorant-garamond-300-italic.woff2
Normal file
BIN
.design/fonts/cormorant-garamond-300-italic.woff2
Normal file
Binary file not shown.
BIN
.design/fonts/cormorant-garamond-300.woff2
Normal file
BIN
.design/fonts/cormorant-garamond-300.woff2
Normal file
Binary file not shown.
BIN
.design/fonts/cormorant-garamond-400-italic.woff2
Normal file
BIN
.design/fonts/cormorant-garamond-400-italic.woff2
Normal file
Binary file not shown.
BIN
.design/fonts/cormorant-garamond-400.woff2
Normal file
BIN
.design/fonts/cormorant-garamond-400.woff2
Normal file
Binary file not shown.
BIN
.design/fonts/cormorant-garamond-600.woff2
Normal file
BIN
.design/fonts/cormorant-garamond-600.woff2
Normal file
Binary file not shown.
48
.design/fonts/fonts.css
Normal file
48
.design/fonts/fonts.css
Normal 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;
|
||||||
|
}
|
||||||
65
.design/fonts/manifest.json
Normal file
65
.design/fonts/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
.design/fonts/tenor-sans-400.woff2
Normal file
BIN
.design/fonts/tenor-sans-400.woff2
Normal file
Binary file not shown.
BIN
.design/logos/favicon-0.ico
Normal file
BIN
.design/logos/favicon-0.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
44
.design/prefetch/material.md
Normal file
44
.design/prefetch/material.md
Normal 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)
|
||||||
94
.design/prefetch/page.html
Normal file
94
.design/prefetch/page.html
Normal 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
531
.design/prefetch/styles.css
Normal 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;
|
||||||
|
}
|
||||||
53
.design/system/BRAND-SYSTEM.md
Normal file
53
.design/system/BRAND-SYSTEM.md
Normal 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.
|
||||||
782
.design/system/artifacts/deck.html
Normal file
782
.design/system/artifacts/deck.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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 & audit logs · Dedicated success manager · Custom contracts & 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 & MOAT</span></div>
|
||||||
|
<div class="f-body">
|
||||||
|
<div class="f-fit">
|
||||||
|
<span class="s-kicker">Competition & 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 · 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">←</button>
|
||||||
|
<span class="deck-counter">1 / 11</span>
|
||||||
|
<button class="deck-next" aria-label="Next slide">→</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>
|
||||||
439
.design/system/artifacts/email.html
Normal file
439
.design/system/artifacts/email.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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;">͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ </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);">“Malenia is the first tool here that actually feels Sophisticated. It was part of our daily flow inside a week.”</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> · 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> · <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 · 100 Market Street, Suite 200 · malenia.space</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
442
.design/system/artifacts/form.html
Normal file
442
.design/system/artifacts/form.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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>
|
||||||
822
.design/system/artifacts/landing.html
Normal file
822
.design/system/artifacts/landing.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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)">“Malenia is the first tool in this space that actually feels Sophisticated. It became part of our daily flow within a week.”</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)">“We were skeptical, then we shipped our first project — genuinely Quiet — in a single afternoon. The team never looked back.”</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)">“The quality bar out of the box is what sold us. Свобода. Практика. Простота is not just a slogan here.”</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;">✓</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;">✓</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;">✓</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;">✓</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;">✓</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;">✓</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;">✓</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;">✓</span><span>SSO & 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;">✓</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;">✓</span><span>Custom contracts & 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 →</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>© Malenia · malenia.space</span>
|
||||||
|
<span>Sophisticated · Quiet · Essential</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
427
.design/system/artifacts/newsletter.html
Normal file
427
.design/system/artifacts/newsletter.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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 →</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 →</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 →</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> · <a href="#" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">Preferences</a> · <a href="#" style="color:var(--brand-color-text-tertiary);text-decoration:underline;">Unsubscribe</a></p>
|
||||||
|
<p style="margin-top:var(--brand-size-xs);">© Malenia · malenia.space</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
434
.design/system/artifacts/poster.html
Normal file
434
.design/system/artifacts/poster.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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
3769
.design/system/index.html
Normal file
File diff suppressed because it is too large
Load Diff
587
.design/system/kit.dark.html
Normal file
587
.design/system/kit.dark.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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)">‹</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)">›</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">✓</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 & 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)">“We replaced three hand-maintained style guides with one generated system.”</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;">✓</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;">✓</span><span>Dark & 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;">✓</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 & 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">✓</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">ℹ</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">⚠</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">✕</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)">✓</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 & 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
587
.design/system/kit.html
Normal 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&display=swap" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tenor+Sans&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)">‹</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)">›</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">✓</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 & 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)">“We replaced three hand-maintained style guides with one generated system.”</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;">✓</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;">✓</span><span>Dark & 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;">✓</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 & 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">✓</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">ℹ</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">⚠</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">✕</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)">✓</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 & 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>
|
||||||
17
.design/system/scripts/apply-design-tokens.mjs
Normal file
17
.design/system/scripts/apply-design-tokens.mjs
Normal 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
22
.design/system/seed.json
Normal 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
24
.design/system/theme.json
Normal 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"
|
||||||
|
}
|
||||||
251
.design/system/tokens.compact.json
Normal file
251
.design/system/tokens.compact.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.design/system/tokens.dark.json
Normal file
251
.design/system/tokens.dark.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.design/system/tokens.default.json
Normal file
251
.design/system/tokens.default.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
441
.design/system/variables.css
Normal file
441
.design/system/variables.css
Normal 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;
|
||||||
|
}
|
||||||
220
.design/system/variables.dark.css
Normal file
220
.design/system/variables.dark.css
Normal 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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.vite
|
||||||
|
coverage
|
||||||
18
.info/ToV.md
Normal file
18
.info/ToV.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Malenia
|
||||||
|
|
||||||
|
## Основная информация
|
||||||
|
|
||||||
|
MaleniaVPN (malenia, маления) — маленькая студия из двух, разрабатывающая VPN решения. Наш имидж строится на критичной насмешке над неспособностью цензоров внедрить серьезные меры против свободного доступа в интернет.
|
||||||
|
|
||||||
|
## Ценности
|
||||||
|
|
||||||
|
Мы ценим честность, и именно поэтому мы ведём максимально честный диалог с нашими пользователями. Инциденты, недочёты, ошибки и совершенствование — мы выносим в свет, так как отказываемся считать, что зрители являются просто потребителями.
|
||||||
|
Мы ценим надёжность, из-за чего стремимся к максимальной простоте и устойчивости для наших клиентов. Принцип включил и забыл стоит в основе нашей деятельности.
|
||||||
|
Мы ценим свободу слова, поэтому мы продолжаем работать, даже когда блокировки становятся сложнее и моральная давка становится сильнее.
|
||||||
|
Для нас это больше чем сервис. Для нас Malenia — это способ борьбы с несправедливыми репрессиями. Мы будем стоять против, не смотря ни на что.
|
||||||
|
|
||||||
|
## Сторонний отзыв об имидже компании
|
||||||
|
|
||||||
|
Бренд Malenia строится вокруг образа независимой, небольшой, но технически компетентной команды, которая противопоставляет себя крупным игрокам рынка и бюрократическим структурам. В коммуникации постоянно подчеркивается происхождение проекта: «два программиста-самоучки», отсутствие маркетингового отдела, работа руками инженеров, а не продавцов. Центральные ценности бренда — свобода доступа к информации, прозрачность, инженерный подход и практическая польза для пользователя. Malenia продаёт не просто VPN, а идею «возвращения интернета пользователю», где технологии должны работать незаметно и без сложной настройки. Компания стремится выглядеть союзником аудитории, регулярно вовлекает пользователей в принятие решений через голосования, признаёт ошибки публично и демонстрирует открытость даже в кризисных ситуациях. Имидж бренда — это сочетание технического андеграунда, цифрового активизма и честного сервиса без корпоративного лоска.
|
||||||
|
|
||||||
|
Tone of Voice построен на разговорном, ироничном и местами дерзком стиле. Malenia не говорит языком пресс-релизов: в текстах много сарказма, мемов, бытовых сравнений и намеренно простых формулировок, которые делают сложные сетевые темы понятными широкой аудитории. При этом за шутками всегда прослеживается позиция эксперта, который готов спорить с конкурентами и публично разбирать их заявления. Отношение к оппонентам критическое и даже насмешливое: компания активно высмеивает маркетинговые манипуляции, драматизацию и попытки играть на страхах пользователей. Однако главным объектом критики становятся не конкретные конкуренты, а некомпетентность, непрозрачность и искусственное нагнетание паники. В отношении к работе прослеживается принцип «сначала решаем проблему, потом рассказываем о ней»: команда демонстрирует готовность быстро адаптироваться к изменениям, экспериментировать, исправлять собственные ошибки и честно признавать просчёты. В результате бренд воспринимается как «свои люди в индустрии» — немного хулиганы, немного инженеры, но прежде всего защитники открытого интернета и интересов своих пользователей.
|
||||||
366
.info/index.html
Normal file
366
.info/index.html
Normal 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&family=Tenor+Sans&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
1
.info/openapi.json
Normal file
File diff suppressed because one or more lines are too long
9
.prettierrc.json
Normal file
9
.prettierrc.json
Normal 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
921
.ref/account.html
Normal 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
1212
.ref/calculator.html
Normal file
File diff suppressed because it is too large
Load Diff
467
.ref/landing.html
Normal file
467
.ref/landing.html
Normal 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&family=Tenor+Sans&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
561
.ref/login.html
Normal 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&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, 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
423
AGENTS.md
Normal 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
27
eslint.config.js
Normal 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
19
index.html
Normal 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
7763
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
44
src/App.tsx
Normal file
44
src/App.tsx
Normal 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
146
src/api/client.test.ts
Normal 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
184
src/api/client.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
43
src/components/AddonList.tsx
Normal file
43
src/components/AddonList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/components/Breakdown.tsx
Normal file
59
src/components/Breakdown.tsx
Normal 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
74
src/components/Cursor.tsx
Normal 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;
|
||||||
|
}
|
||||||
76
src/components/DeviceCard.tsx
Normal file
76
src/components/DeviceCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
165
src/components/DeviceSlider.tsx
Normal file
165
src/components/DeviceSlider.tsx
Normal 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> мест
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/components/DurationSelect.tsx
Normal file
36
src/components/DurationSelect.tsx
Normal 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
16
src/components/Emblem.tsx
Normal 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
9
src/components/Logo.tsx
Normal 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
77
src/components/Modal.tsx
Normal 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
43
src/components/Reveal.tsx
Normal 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
16
src/components/Shell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/components/Skeleton.tsx
Normal file
19
src/components/Skeleton.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
src/components/StatStrip.tsx
Normal file
94
src/components/StatStrip.tsx
Normal 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
17
src/components/Toast.tsx
Normal 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
38
src/components/TopBar.tsx
Normal 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
25
src/hooks/useAuth.ts
Normal 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
54
src/hooks/useDevices.ts
Normal 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 };
|
||||||
|
}
|
||||||
39
src/hooks/useLocalStorage.ts
Normal file
39
src/hooks/useLocalStorage.ts
Normal 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
37
src/hooks/usePlans.ts
Normal 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
109
src/lib/format.test.ts
Normal 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
103
src/lib/format.ts
Normal 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, 'устройство', 'устройства', 'устройств')}`;
|
||||||
|
}
|
||||||
48
src/lib/planConfig.test.ts
Normal file
48
src/lib/planConfig.test.ts
Normal 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
80
src/lib/planConfig.ts
Normal 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
3
src/lib/redirect.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function redirectTo(url: string): void {
|
||||||
|
window.location.assign(url);
|
||||||
|
}
|
||||||
12
src/main.tsx
Normal file
12
src/main.tsx
Normal 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>,
|
||||||
|
);
|
||||||
150
src/pages/AccountPage.test.tsx
Normal file
150
src/pages/AccountPage.test.tsx
Normal 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
215
src/pages/AccountPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/pages/Landing.test.tsx
Normal file
24
src/pages/Landing.test.tsx
Normal 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
153
src/pages/Landing.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/pages/LoginPage.test.tsx
Normal file
99
src/pages/LoginPage.test.tsx
Normal 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
278
src/pages/LoginPage.tsx
Normal 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
133
src/pages/PlanPage.test.tsx
Normal 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
229
src/pages/PlanPage.tsx
Normal 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
106
src/store/useStore.ts
Normal 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
24
src/store/useToast.ts
Normal 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
93
src/styles/base.css
Normal 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
1177
src/styles/components.css
Normal file
File diff suppressed because it is too large
Load Diff
535
src/styles/pages.css
Normal file
535
src/styles/pages.css
Normal 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
79
src/styles/tokens.css
Normal 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
8
src/test/setup.ts
Normal 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
78
src/types.ts
Normal 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
34
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
1
tsconfig.node.tsbuildinfo
Normal file
1
tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal 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
22
vite.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user