Fry3DX 0.4: an easy 3D modeller
Shapes with live settings, shape editing, knife and hole tools, drawing shapes onto other objects, combining objects (add / subtract / overlap), copies, thickness, painting with stickers and layers, a Layout tab, a Print tab with checks, packed .fry files with autosave, export to GLB, glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash screen, and a website for GitHub Pages in docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
77
docs/content.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Everything the page says. To update the site, change this file only:
|
||||
// add a feature to `features`, a picture to `shots`, a version to `versions` (newest first).
|
||||
window.FRY = {
|
||||
name: "Fry3DX",
|
||||
long: "Frying 3D Extreme",
|
||||
author: "Nexoniarz",
|
||||
authorUrl: "https://github.com/Nexoniarz",
|
||||
repo: "https://github.com/Nexoniarz/Fry3DX",
|
||||
tagline: "A 3D modeller that does what you'd expect.",
|
||||
intro: "Pick a tool, click, drag. Plain words instead of jargon, a toolbox like GIMP's, and the useful stuff from the big modellers without the hundred menus around it.",
|
||||
|
||||
why: {
|
||||
title: "Why does this exist?",
|
||||
text: [
|
||||
"Because the well-known modellers suck at being easy. You open them and get a wall of panels, shortcuts nobody would ever guess, and modes inside modes. Making a box with a hole for a screw shouldn't need a tutorial.",
|
||||
"So Nexoniarz made one that's built around how people actually think: pick a tool, click on the thing, drag. Every button says what it does, every tool has a tooltip, and the clever parts (rounding, mirroring, combining, checking a print) are one click away.",
|
||||
],
|
||||
},
|
||||
|
||||
features: [
|
||||
{ icon: "▣", title: "Shapes that stay editable", text: "Cube, cylinder, sphere, torus, stairs, arch and more, plus starter models like a table, chair or barrel. Change their sizes any time in Properties." },
|
||||
{ icon: "✎", title: "Draw a shape anywhere", text: "Click an outline on the ground or straight on the face of another object: it becomes a new part, gets added to it, or cuts into it." },
|
||||
{ icon: "⊕", title: "Add, subtract, overlap", text: "Combine objects: add them together, take one out of another, or keep only where they overlap. The result is always closed and ready to print." },
|
||||
{ icon: "◫", title: "Real shape editing", text: "Points, edges and faces. Extrude, inset, round edges, loop cut, knife, merge. Shift+click picks a whole loop, Shift+Ctrl+click a ring." },
|
||||
{ icon: "◎", title: "Holes for screws and nuts", text: "Round, rectangle, triangle or hexagon. M2 to M8 screw and nut sizes ready, with a little room added so the print actually fits." },
|
||||
{ icon: "▤", title: "Copies and thickness", text: "Copies in a row or round in a circle. Make a surface thick, or a solid hollow with walls exactly as thick as you need." },
|
||||
{ icon: "🖌", title: "Paint and stickers", text: "Paint on the model or on its flat picture. Paste any picture (Ctrl+V) and it goes on like a sticker that bends over edges and corners." },
|
||||
{ icon: "▦", title: "Layout tab", text: "Move, turn and size the flat frames of the surface by hand. The paint follows, so the model looks the same." },
|
||||
{ icon: "⎙", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." },
|
||||
{ icon: "☀", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." },
|
||||
{ icon: "🗀", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." },
|
||||
{ icon: "💾", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." },
|
||||
],
|
||||
|
||||
shots: [
|
||||
{ img: "img/model.webp", title: "Model", text: "Starter models, a folder of parts, a shape drawn onto a cube's face, rendered with sky and shadows." },
|
||||
{ img: "img/paint.webp", title: "Paint", text: "A picture pasted on as a sticker: it bends over the cube's edge like a real one." },
|
||||
{ img: "img/print.webp", title: "Print", text: "The printer's plate, sizes in millimetres, checks and overhangs before you slice." },
|
||||
{ img: "img/combine.webp", title: "Combine", text: "A cylinder taken out of a cube, copies in a row in a folder, a plane made thick." },
|
||||
{ img: "img/crash.webp", title: "Even the crash screen", text: "If something does get burnt, your work comes back from the last autosave." },
|
||||
],
|
||||
|
||||
formats: {
|
||||
open: [".fry"],
|
||||
save: [".fry (packed, with pictures)"],
|
||||
export: ["GLB", "glTF", "FBX", "OBJ + MTL", "3MF", "STL", "PLY"],
|
||||
note: "FBX opens in Blender, Unity, Unreal and Maya. 3MF and STL are in millimetres for slicers (PrusaSlicer, Bambu Studio, Cura, OrcaSlicer).",
|
||||
},
|
||||
|
||||
keys: [
|
||||
["Q W E R", "Select, move, turn, scale"],
|
||||
["Tab", "Edit an object's shape"],
|
||||
["D", "Draw a shape"],
|
||||
["O", "Make a hole"],
|
||||
["Ctrl+G", "Group into a folder"],
|
||||
["Ctrl+Shift + / −", "Add together / take out"],
|
||||
["Ctrl+1 … 5", "Model, Paint, Layout, Render, Print"],
|
||||
["F1", "Every key, in the app"],
|
||||
],
|
||||
|
||||
build: {
|
||||
text: "Fry3DX builds from source on Linux with CMake. Everything else it needs is downloaded while building. On NixOS, shell.nix has the tools.",
|
||||
steps: [
|
||||
"git clone https://github.com/Nexoniarz/Fry3DX.git",
|
||||
"cd Fry3DX",
|
||||
"nix-shell --run 'cmake --preset release && cmake --build build/release'",
|
||||
"build/release/bin/fry3dx",
|
||||
],
|
||||
needs: "Needs: a Vulkan graphics card, CMake, Ninja, a C++20 compiler, glslc (shaderc) and SDL3.",
|
||||
},
|
||||
|
||||
versions: [
|
||||
{ v: "0.4", date: "2026-10-19", items: ["Combine objects: add, subtract, overlap", "Draw shapes onto other objects", "Copies in a row or circle, thicken and hollow", "FBX export", "Folders and groups in Layers, go-to buttons", "Shift scales both sides, Shift+click loops and rings", "A crash screen of its own"] },
|
||||
{ v: "0.3", date: "2026-10-19", items: ["Stickers that bend over edges, picture layers", "Packed .fry files, autosave and recovery", "Layout tab, Print tab, 3MF / STL / OBJ / PLY / GLB export", "Hole tool: hexagon nut pockets, rectangles, outlines"] },
|
||||
{ v: "0.1", date: "2026-10-19", items: ["First version: shapes, shape editing, knife, painting, rendering"] },
|
||||
],
|
||||
};
|
||||
BIN
docs/img/combine.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/img/crash.webp
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/img/icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
34
docs/img/logo.svg
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#2a2a2f"/><stop offset="1" stop-color="#151518"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="pan" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#3b3b42"/><stop offset="1" stop-color="#1d1d21"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="flame" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0" stop-color="#ff4a1c"/><stop offset="1" stop-color="#ffc24a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="16" y="16" width="480" height="480" rx="96" fill="url(#bg)"/>
|
||||
<!-- flames under the pan -->
|
||||
<path d="M164 462 c -34 -14 -30 -60 0 -96 c 30 36 34 82 0 96 z" fill="url(#flame)"/>
|
||||
<path d="M236 462 c -34 -14 -30 -60 0 -96 c 30 36 34 82 0 96 z" fill="url(#flame)"/>
|
||||
<path d="M308 462 c -34 -14 -30 -60 0 -96 c 30 36 34 82 0 96 z" fill="url(#flame)"/>
|
||||
<!-- handle -->
|
||||
<path d="M372 300 L470 250" stroke="#ff7828" stroke-width="30" stroke-linecap="round"/>
|
||||
<!-- the pan, seen a little from above -->
|
||||
<ellipse cx="236" cy="318" rx="170" ry="72" fill="#ff7828"/>
|
||||
<ellipse cx="236" cy="306" rx="150" ry="58" fill="url(#pan)"/>
|
||||
<!-- the cube frying in it -->
|
||||
<g stroke="#1a1a1d" stroke-width="8" stroke-linejoin="round">
|
||||
<path d="M236 92 L330 140 L236 188 L142 140 Z" fill="#ffd2a0"/>
|
||||
<path d="M142 140 L236 188 L236 300 L142 252 Z" fill="#ff8a3a"/>
|
||||
<path d="M330 140 L236 188 L236 300 L330 252 Z" fill="#c9501a"/>
|
||||
</g>
|
||||
<!-- sizzle -->
|
||||
<g stroke="#ffb070" stroke-width="10" stroke-linecap="round" fill="none" opacity="0.9">
|
||||
<path d="M100 210 q-14 -18 0 -36 q14 -18 0 -36"/>
|
||||
<path d="M378 200 q-14 -18 0 -36 q14 -18 0 -36"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
docs/img/model.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/img/paint.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
docs/img/print.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
86
docs/index.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fry3DX</title>
|
||||
<meta name="description" content="Fry3DX (Frying 3D Extreme): a 3D modeller that does what you'd expect. Made by Nexoniarz.">
|
||||
<meta property="og:title" content="Fry3DX">
|
||||
<meta property="og:description" content="A 3D modeller that does what you'd expect. Pick a tool, click, drag.">
|
||||
<meta property="og:image" content="img/model.webp">
|
||||
<link rel="icon" href="img/icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- The page is built from content.js by site.js: edit content.js to update it. -->
|
||||
<header class="top">
|
||||
<a class="brand" href="#"><img src="img/logo.svg" alt=""><span>Fry3DX</span></a>
|
||||
<nav>
|
||||
<a href="#features">Features</a>
|
||||
<a href="#look">Look</a>
|
||||
<a href="#get">Get it</a>
|
||||
<a href="#versions">Versions</a>
|
||||
<a class="gh" id="gh-top" href="#">GitHub</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="hero-text">
|
||||
<p class="kicker" id="long"></p>
|
||||
<h1 id="tagline"></h1>
|
||||
<p class="lead" id="intro"></p>
|
||||
<div class="buttons">
|
||||
<a class="btn hot" href="#get">Get Fry3DX</a>
|
||||
<a class="btn" id="gh-hero" href="#">Source on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
<figure class="hero-shot"><img src="img/model.webp" alt="Fry3DX with a table, chair, barrel and crate"></figure>
|
||||
</section>
|
||||
|
||||
<section class="why" id="why"></section>
|
||||
|
||||
<section id="features">
|
||||
<h2>What it does</h2>
|
||||
<div class="grid" id="feature-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="look">
|
||||
<h2>How it looks</h2>
|
||||
<div class="tabs" id="shot-tabs" role="tablist"></div>
|
||||
<figure class="shot"><img id="shot-img" alt=""><figcaption id="shot-text"></figcaption></figure>
|
||||
</section>
|
||||
|
||||
<section class="two">
|
||||
<div>
|
||||
<h2>Files</h2>
|
||||
<div id="formats"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Keys that make sense</h2>
|
||||
<table class="keys" id="keys"></table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="get">
|
||||
<h2>Get it</h2>
|
||||
<p id="build-text"></p>
|
||||
<div class="code"><button class="copy" id="copy">Copy</button><pre id="build-steps"></pre></div>
|
||||
<p class="muted" id="build-needs"></p>
|
||||
</section>
|
||||
|
||||
<section id="versions">
|
||||
<h2>Versions</h2>
|
||||
<div id="version-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Fry3DX, made by <a id="author" href="#"></a>.</p>
|
||||
<p class="muted">Sky pictures from Poly Haven (CC0).</p>
|
||||
</footer>
|
||||
|
||||
<script src="content.js"></script>
|
||||
<script src="site.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
91
docs/site.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Builds the page from window.FRY (content.js). Nothing here needs changing for a content update.
|
||||
(function () {
|
||||
const F = window.FRY;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const el = (tag, cls, text) => {
|
||||
const e = document.createElement(tag);
|
||||
if (cls) e.className = cls;
|
||||
if (text != null) e.textContent = text;
|
||||
return e;
|
||||
};
|
||||
|
||||
$("long").textContent = F.long;
|
||||
$("tagline").textContent = F.tagline;
|
||||
$("intro").textContent = F.intro;
|
||||
for (const id of ["gh-top", "gh-hero"]) $(id).href = F.repo;
|
||||
$("author").textContent = F.author;
|
||||
$("author").href = F.authorUrl;
|
||||
|
||||
const why = $("why");
|
||||
why.append(el("h2", null, F.why.title));
|
||||
for (const p of F.why.text) why.append(el("p", null, p));
|
||||
|
||||
const grid = $("feature-grid");
|
||||
for (const f of F.features) {
|
||||
const card = el("article", "card");
|
||||
card.append(el("div", "icon", f.icon), el("h3", null, f.title), el("p", null, f.text));
|
||||
grid.append(card);
|
||||
}
|
||||
|
||||
// Screenshots: a tab each.
|
||||
const tabs = $("shot-tabs");
|
||||
const show = (i) => {
|
||||
const s = F.shots[i];
|
||||
$("shot-img").src = s.img;
|
||||
$("shot-img").alt = s.title;
|
||||
$("shot-text").textContent = s.text;
|
||||
[...tabs.children].forEach((b, j) => b.setAttribute("aria-selected", i === j));
|
||||
};
|
||||
F.shots.forEach((s, i) => {
|
||||
const b = el("button", "tab", s.title);
|
||||
b.setAttribute("role", "tab");
|
||||
b.onclick = () => show(i);
|
||||
tabs.append(b);
|
||||
new Image().src = s.img; // (loaded ahead, so switching is instant)
|
||||
});
|
||||
show(0);
|
||||
|
||||
const fm = $("formats");
|
||||
const row = (label, items) => {
|
||||
const d = el("div", "fmt");
|
||||
d.append(el("span", "fmt-label", label));
|
||||
for (const x of items) d.append(el("span", "chip", x));
|
||||
fm.append(d);
|
||||
};
|
||||
row("Save", F.formats.save);
|
||||
row("Export", F.formats.export);
|
||||
fm.append(el("p", "muted", F.formats.note));
|
||||
|
||||
const keys = $("keys");
|
||||
for (const [k, what] of F.keys) {
|
||||
const tr = el("tr");
|
||||
const td = el("td");
|
||||
td.append(el("kbd", null, k));
|
||||
tr.append(td, el("td", null, what));
|
||||
keys.append(tr);
|
||||
}
|
||||
|
||||
$("build-text").textContent = F.build.text;
|
||||
$("build-steps").textContent = F.build.steps.join("\n");
|
||||
$("build-needs").textContent = F.build.needs;
|
||||
$("copy").onclick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(F.build.steps.join("\n"));
|
||||
$("copy").textContent = "Copied";
|
||||
} catch {
|
||||
$("copy").textContent = "Select and copy";
|
||||
}
|
||||
setTimeout(() => ($("copy").textContent = "Copy"), 1800);
|
||||
};
|
||||
|
||||
const vl = $("version-list");
|
||||
for (const v of F.versions) {
|
||||
const d = el("div", "version");
|
||||
const h = el("h3");
|
||||
h.append(el("span", "v", "v" + v.v), el("span", "muted", " " + v.date));
|
||||
const ul = el("ul");
|
||||
for (const it of v.items) ul.append(el("li", null, it));
|
||||
d.append(h, ul);
|
||||
vl.append(d);
|
||||
}
|
||||
})();
|
||||
103
docs/style.css
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
:root {
|
||||
--bg: #161618;
|
||||
--panel: #1f1f23;
|
||||
--panel2: #26262b;
|
||||
--line: #34343b;
|
||||
--ink: #ecebe6;
|
||||
--dim: #9c9a94;
|
||||
--hot: #ff7828;
|
||||
--hot2: #ffc24a;
|
||||
--radius: 10px;
|
||||
color-scheme: dark;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
a { color: var(--hot); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
img { max-width: 100%; display: block; }
|
||||
h1, h2, h3 { line-height: 1.2; margin: 0 0 .6em; }
|
||||
h2 { font-size: 1.7rem; }
|
||||
h2::after { content: ""; display: block; width: 48px; height: 4px; margin-top: .4em; border-radius: 2px; background: linear-gradient(90deg, var(--hot), var(--hot2)); }
|
||||
.muted { color: var(--dim); }
|
||||
|
||||
.top {
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
||||
padding: 10px max(16px, calc((100% - 1120px) / 2));
|
||||
background: rgba(22, 22, 24, .9); backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); font-weight: 700; font-size: 1.15rem; }
|
||||
.brand img { width: 32px; height: 32px; }
|
||||
.brand:hover { text-decoration: none; }
|
||||
nav { display: flex; gap: 18px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
nav a { color: var(--dim); font-size: .95rem; }
|
||||
nav a:hover { color: var(--ink); text-decoration: none; }
|
||||
nav .gh { color: var(--hot); }
|
||||
|
||||
main { max-width: 1120px; margin: 0 auto; padding: 0 16px; }
|
||||
section { padding: 56px 0; border-bottom: 1px solid var(--line); }
|
||||
section:last-child { border-bottom: 0; }
|
||||
|
||||
.hero { display: grid; grid-template-columns: 1fr 1.25fr; gap: 40px; align-items: center; padding-top: 64px; }
|
||||
.kicker { color: var(--hot); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; font-size: .85rem; margin: 0 0 .6em; }
|
||||
.hero h1 { font-size: clamp(2rem, 4.5vw, 3.2rem); }
|
||||
.lead { color: var(--dim); font-size: 1.1rem; }
|
||||
.buttons { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 24px; }
|
||||
.btn { display: inline-block; padding: 12px 20px; border-radius: var(--radius); background: var(--panel2); color: var(--ink); font-weight: 600; border: 1px solid var(--line); }
|
||||
.btn:hover { text-decoration: none; border-color: var(--hot); }
|
||||
.btn.hot { background: linear-gradient(135deg, var(--hot), #ff4a1c); border-color: transparent; color: #fff; }
|
||||
.hero-shot { margin: 0; }
|
||||
.hero-shot img, .shot img { border-radius: var(--radius); border: 1px solid var(--line); box-shadow: 0 20px 60px rgba(0, 0, 0, .45); }
|
||||
|
||||
.why { max-width: 760px; }
|
||||
.why p { font-size: 1.08rem; color: #cfcdc7; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; }
|
||||
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; transition: border-color .15s, transform .15s; }
|
||||
.card:hover { border-color: var(--hot); transform: translateY(-2px); }
|
||||
.card .icon { font-size: 1.5rem; color: var(--hot); margin-bottom: 6px; }
|
||||
.card h3 { font-size: 1.05rem; }
|
||||
.card p { margin: 0; color: var(--dim); font-size: .95rem; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.tab { font: inherit; color: var(--dim); background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 6px 16px; cursor: pointer; }
|
||||
.tab:hover { color: var(--ink); }
|
||||
.tab[aria-selected="true"] { color: #fff; background: var(--hot); border-color: var(--hot); }
|
||||
.shot { margin: 0; }
|
||||
.shot figcaption { color: var(--dim); margin-top: 12px; }
|
||||
|
||||
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
|
||||
.fmt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
|
||||
.fmt-label { width: 64px; color: var(--dim); }
|
||||
.chip { background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 2px 10px; font-size: .9rem; }
|
||||
.keys { border-collapse: collapse; width: 100%; }
|
||||
.keys td { padding: 7px 0; border-bottom: 1px solid var(--line); }
|
||||
.keys td:first-child { width: 45%; }
|
||||
kbd { font: .85rem ui-monospace, "DejaVu Sans Mono", monospace; background: var(--panel2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 2px 7px; }
|
||||
|
||||
.code { position: relative; background: #0f0f11; border: 1px solid var(--line); border-left: 3px solid var(--hot); border-radius: var(--radius); }
|
||||
.code pre { margin: 0; padding: 18px 20px; overflow-x: auto; font: .92rem/1.7 ui-monospace, "DejaVu Sans Mono", monospace; color: #d6d4cd; }
|
||||
.copy { position: absolute; top: 10px; right: 10px; font: inherit; font-size: .85rem; color: var(--ink); background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 4px 12px; cursor: pointer; }
|
||||
.copy:hover { border-color: var(--hot); }
|
||||
|
||||
.version { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px 22px; margin-bottom: 12px; }
|
||||
.version h3 { margin-bottom: .3em; }
|
||||
.version .v { color: var(--hot); }
|
||||
.version ul { margin: 0; padding-left: 20px; color: #cfcdc7; }
|
||||
|
||||
footer { text-align: center; padding: 40px 16px; border-top: 1px solid var(--line); color: var(--dim); }
|
||||
footer p { margin: 4px 0; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.hero, .two { grid-template-columns: 1fr; }
|
||||
.hero { padding-top: 32px; gap: 28px; }
|
||||
nav a:not(.gh) { display: none; }
|
||||
section { padding: 40px 0; }
|
||||
}
|
||||