From 15db9e90eba9f5fbfc112264ba809fba4c7a74ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 23 May 2026 00:25:27 -0300 Subject: [PATCH] Body height miscalculation fix. --- .../html/body_height_miscalculation.css | 3196 +++++++++++++++++ .../html/body_height_miscalculation.html | 466 +++ include/eepp/ui/uihtmlwidget.hpp | 2 + src/eepp/ui/blocklayouter.cpp | 10 +- src/eepp/ui/uihtmlwidget.cpp | 9 + src/eepp/ui/uirichtext.cpp | 5 +- src/tests/unit_tests/uihtml_tests.cpp | 29 + 7 files changed, 3714 insertions(+), 3 deletions(-) create mode 100644 bin/unit_tests/assets/html/body_height_miscalculation.css create mode 100644 bin/unit_tests/assets/html/body_height_miscalculation.html diff --git a/bin/unit_tests/assets/html/body_height_miscalculation.css b/bin/unit_tests/assets/html/body_height_miscalculation.css new file mode 100644 index 000000000..fcfb3df1e --- /dev/null +++ b/bin/unit_tests/assets/html/body_height_miscalculation.css @@ -0,0 +1,3196 @@ +:root { + --bg: #0c1520; + --bg-surface: #141f2e; + --bg-surface-2: #1a2637; + --text: #bfc8d2; + --text-muted: #566a7c; + --text-bright: #e4e9ee; + --accent: #d4a853; + --accent-hover: #e4bc6a; + --tag-color: #7eb8a8; + --eng-color: #3d7b8a; + --danger: #e07a5f; + --border: rgba(255, 255, 255, 0.07); + --border-strong: rgba(255, 255, 255, 0.12); + --max-width: 54rem; + --read-width: 54rem; + --font-text: text, "Roboto", -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: code, "JetBrains Mono", ui-monospace, SFMono-Regular, monospace; + --font-serif: serif, "Fraunces", "Iowan Old Style", Georgia, serif; +} + +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 17.5px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scroll-behavior: smooth; +} + +@media (max-width: 560px) { + html { + font-size: 16px; + } +} + +body { + font-family: var(--font-text); + font-weight: 300; + line-height: 1.65; + color: var(--text); + background: var(--bg); + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-x: clip; +} + +::selection { + background: rgba(212, 168, 83, 0.25); + color: var(--text-bright); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 0.15s; +} +a:hover { + color: var(--accent-hover); +} + +img { + max-width: 100%; + height: auto; +} + +/* ============ Nav ============ */ +.site-nav { + padding: 1.25rem 0; + border-bottom: 1px solid var(--border); +} +.site-nav .wrap { + max-width: var(--max-width); + margin: 0 auto; + padding: 0 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} +.site-nav .brand { + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-bright); + letter-spacing: -0.02em; +} +.site-nav .links { + display: flex; + gap: 1.5rem; + font-family: var(--font-mono); + font-size: 0.8rem; +} +.site-nav .links a { + color: var(--text-muted); +} +.site-nav .links a:hover, +.site-nav .links a[aria-current="page"] { + color: var(--text-bright); +} + +/* ============ Shell ============ */ +main.shell { + flex: 1; + max-width: var(--max-width); + margin: 0 auto; + width: 100%; + padding: 3rem 1.5rem 5rem; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(0.4rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ============ Footer ============ */ +footer.site-footer { + margin-top: auto; + border-top: 1px solid var(--border); + padding: 2rem 0; +} +footer.site-footer .wrap { + max-width: var(--max-width); + margin: 0 auto; + padding: 0 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--text-muted); +} +footer.site-footer a { + color: var(--text-muted); +} +footer.site-footer a:hover { + color: var(--accent); +} + +/* ============ Masthead (home / about) ============ */ +.masthead { + display: grid; + grid-template-columns: minmax(0, 1fr) 12.5rem; + gap: 3rem; + align-items: start; + padding-bottom: 2.5rem; + margin-bottom: 2.5rem; + border-bottom: 1px solid var(--border-strong); +} +.masthead:has(.portrait-wrap.has-outline) { + grid-template-columns: minmax(0, 1fr) 21rem; +} +.mh-eyebrow { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem; +} +.mh-eyebrow .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 6px var(--accent); +} +.mh-eyebrow .sep { + flex: 1; + height: 1px; + background: var(--border); +} + +.masthead h1 { + font-family: var(--font-serif); + font-weight: 300; + font-variation-settings: "opsz" 144; + font-size: clamp(2.3rem, 5vw, 4rem); + line-height: 1; + letter-spacing: -0.03em; + color: var(--text-bright); + text-wrap: balance; + margin-bottom: 1rem; +} +.masthead h1 em { + font-style: italic; + font-variation-settings: "opsz" 144; + color: var(--accent); +} +.masthead h1 .accent { + font-style: normal; + color: var(--accent); +} +.masthead .dek { + font-family: var(--font-text); + font-weight: 300; + font-style: normal; + font-size: 1.1rem; + line-height: 1.55; + color: var(--text); + max-width: 38rem; + margin-bottom: 1.5rem; +} + +.mh-stats { + display: flex; + gap: 2rem; + flex-wrap: wrap; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; + padding-top: 1rem; + border-top: 1px solid var(--border); +} +.mh-stats div b { + display: block; + font-family: var(--font-serif); + font-style: normal; + font-weight: 400; + font-size: 1.5rem; + color: var(--text-bright); + margin-bottom: 0.15rem; + letter-spacing: -0.02em; + text-transform: none; +} + +.masthead-right { + position: relative; +} +.portrait-wrap { + position: relative; + aspect-ratio: 1; + border-radius: 0.35rem; + overflow: hidden; + display: block; +} +.portrait-wrap.has-outline { + border: 1px solid var(--border); +} +.portrait-art { + position: absolute; + inset: 0; +} +.portrait-art img { + width: 100%; + height: 100%; + display: block; +} +.portrait-art img[src$=".jpeg"], +.portrait-art img[src$=".jpg"], +.portrait-art img[src$=".png"] { + object-fit: cover; + filter: grayscale(0.15) contrast(1.02); +} +.portrait-art img[src$=".svg"] { + object-fit: contain; + padding: 8%; + opacity: 0.92; +} + +/* White line-art overlay on photo portrait (about page) — body only, face masked out */ +.portrait-outline { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + pointer-events: none; + opacity: 0; + filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.45)); + animation: + portrait-outline-in 1.4s ease-out 0.3s forwards, + portrait-outline-breathe 6s ease-in-out 1.7s infinite; + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0%, + transparent 38%, + rgba(0, 0, 0, 0.4) 46%, + black 54%, + black 100% + ); + mask-image: linear-gradient( + to bottom, + transparent 0%, + transparent 38%, + rgba(0, 0, 0, 0.4) 46%, + black 54%, + black 100% + ); +} +@keyframes portrait-outline-in { + 0% { + opacity: 0; + transform: scale(0.985); + } + 100% { + opacity: 0.85; + transform: scale(1); + } +} +@keyframes portrait-outline-breathe { + 0%, 100% { + opacity: 0.78; + } + 50% { + opacity: 0.95; + } +} +.portrait-wrap.has-outline .portrait-art img { + filter: grayscale(0.15) contrast(1) brightness(0.95); +} + +/* Floating dots overlay on portrait */ +.portrait-dot { + position: absolute; + width: 3px; + height: 3px; + background: var(--text-bright); + border-radius: 50%; + box-shadow: 0 0 6px var(--text-bright); + animation: portrait-float var(--dur, 5s) ease-in-out var(--del, 0s) infinite + alternate; + pointer-events: none; +} +@keyframes portrait-float { + 0% { + left: var(--x1); + top: var(--y1); + opacity: 0; + } + 15% { + opacity: 0.7; + } + 85% { + opacity: 0.7; + } + 100% { + left: var(--x2); + top: var(--y2); + opacity: 0; + } +} +.portrait-caption { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-muted); + letter-spacing: 0.1em; + text-transform: uppercase; + margin-top: 0.75rem; + display: flex; + justify-content: space-between; +} +.portrait-caption .loc { + color: var(--accent); +} + +/* ============ Section bar ============ */ +.section-bar { + display: grid; + grid-template-columns: auto 1fr auto auto; + align-items: baseline; + gap: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-strong); + margin-bottom: 1.75rem; + font-family: var(--font-mono); +} +.section-bar .sb-num { + font-size: 0.68rem; + color: var(--accent); + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 0.2rem 0.55rem; + border: 1px solid rgba(212, 168, 83, 0.3); + border-radius: 0.15rem; + background: rgba(212, 168, 83, 0.04); +} +.section-bar .sb-label { + font-family: var(--font-serif); + font-weight: 400; + font-size: 1.4rem; + color: var(--text-bright); + letter-spacing: -0.015em; +} +.section-bar .sb-label em { + font-style: italic; + color: var(--accent); +} +.section-bar .sb-count { + font-size: 0.7rem; + color: var(--text-muted); + letter-spacing: 0.06em; +} +.section-bar .sb-filter { + display: flex; + gap: 0.35rem; + flex-wrap: wrap; +} +.section-bar .sb-filter button { + background: transparent; + border: 1px solid var(--border); + color: var(--text-muted); + font-family: inherit; + font-size: 0.62rem; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 0.28rem 0.55rem; + border-radius: 0.15rem; + cursor: pointer; + transition: color 0.15s, border-color 0.15s, background 0.15s; +} +.section-bar .sb-filter button:hover { + color: var(--text-bright); + border-color: var(--border-strong); +} +.section-bar .sb-filter button.on { + color: var(--accent); + border-color: rgba(212, 168, 83, 0.4); + background: rgba(212, 168, 83, 0.08); +} + +/* ============ Featured block (home) ============ */ +.featured { + display: grid; + grid-template-columns: minmax(0, 1fr) 12.5rem; + gap: 3rem; + padding: 0.5rem 0 2.5rem; + border-bottom: 1px solid var(--border); + margin-bottom: 2.5rem; + align-items: center; +} +.feat-main .f-eyebrow { + font-family: var(--font-mono); + font-size: 0.65rem; + color: var(--accent); + letter-spacing: 0.14em; + text-transform: uppercase; + margin-bottom: 0.9rem; + display: flex; + align-items: center; + gap: 0.6rem; +} +.feat-main .f-eyebrow .pulse { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); + animation: pulse 2s ease-in-out infinite; +} +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.35; + } +} + +.feat-main h2 { + font-family: var(--font-serif); + font-weight: 400; + font-variation-settings: "opsz" 96; + font-size: clamp(1.6rem, 3vw, 2.3rem); + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--text-bright); + text-wrap: balance; + margin-bottom: 1rem; +} +.feat-main h2 a { + color: inherit; +} +.feat-main h2 a:hover { + color: var(--accent); +} +.feat-main .dek { + font-family: var(--font-text); + font-weight: 300; + font-size: 1.05rem; + line-height: 1.6; + color: var(--text); + max-width: 40rem; + margin-bottom: 1.25rem; + text-wrap: pretty; +} +.feat-main .meta { + display: flex; + gap: 1.25rem; + flex-wrap: wrap; + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; +} +.feat-main .meta .tag { + color: var(--tag-color); +} +.feat-main .meta .tag::before { + content: "#"; + opacity: 0.6; +} + +.feat-art { + aspect-ratio: 1; + border: 1px solid var(--border); + border-radius: 0.35rem; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} +.feat-art > div { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} +.feat-art svg { + width: 100%; + height: 100%; + transform: scale(0.49); + transform-origin: center center; + display: block; +} +.feat-art.has-cover img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.feat-art:not(.has-cover) .art-spin, +.feat-art:not(.has-cover) .art-orbit { + animation: none !important; +} +.feat-art .art-draw { + animation: art-build-slow 18s ease-in-out calc(var(--d, 0s) * 4) infinite; +} +.feat-art .art-dot, +.feat-art .art-pulse-dot { + animation: art-build-dot-slow 18s ease-in-out calc(var(--d, 0s) * 4) infinite; +} +@keyframes art-build-slow { + 0% { + stroke-dashoffset: 1; + opacity: 0; + } + 8% { + opacity: 1; + } + 55% { + stroke-dashoffset: 0; + opacity: 1; + } + 85% { + stroke-dashoffset: 0; + opacity: 1; + } + 100% { + stroke-dashoffset: 0; + opacity: 0; + } +} +@keyframes art-build-dot-slow { + 0%, 100% { + opacity: 0; + } + 15%, 85% { + opacity: var(--op, 0.6); + } +} +.feat-art::after { + content: ""; + position: absolute; + inset: 0; + background: radial-gradient( + ellipse at center, + transparent 60%, + rgba(12, 21, 32, 0.3) 100% + ); + pointer-events: none; +} + +/* ============ Post list (home) ============ */ +.post-list { + list-style: none; +} +.post-list > li { + display: grid; + grid-template-columns: 5rem minmax(0, 1fr) 4.5rem; + gap: 1.5rem; + padding: 1.5rem 0; + border-bottom: 1px solid var(--border); + align-items: start; + transition: background 0.2s ease; +} +.post-list > li:first-child { + padding-top: 0.5rem; +} +.post-list > li:hover { + background: linear-gradient( + to right, + rgba(212, 168, 83, 0.02), + transparent 80% + ); +} +.post-list .p-art { + width: 4.75rem; + height: 4.75rem; + border: 1px solid var(--border); + border-radius: 0.25rem; + background: transparent; + overflow: hidden; +} +.post-list .p-art svg { + width: 100%; + height: 100%; + display: block; +} +.post-list .p-body { + min-width: 0; +} +.post-list .p-date { + font-family: var(--font-mono); + font-size: 0.64rem; + color: var(--text-muted); + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 0.35rem; + display: flex; + gap: 0.75rem; + align-items: center; +} +.post-list .p-date .n { + color: var(--accent); + opacity: 0.7; + font-size: 0.6rem; +} +.post-list .p-title { + font-family: var(--font-serif); + font-weight: 400; + font-variation-settings: "opsz" 48; + font-size: 1.35rem; + line-height: 1.2; + color: var(--text-bright); + text-wrap: balance; + margin-bottom: 0.35rem; + letter-spacing: -0.01em; +} +.post-list .p-title a { + color: inherit; +} +.post-list > li:hover .p-title a { + color: var(--accent); +} +.post-list .p-desc { + font-family: var(--font-text); + font-weight: 300; + font-style: normal; + font-size: 0.98rem; + line-height: 1.55; + color: var(--text); + margin: 0 0 0.5rem; + max-width: 48rem; + text-wrap: pretty; +} +.post-list .p-tags { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + font-family: var(--font-mono); + font-size: 0.64rem; +} +.post-list .p-tags a { + color: var(--tag-color); + letter-spacing: 0.04em; +} +.post-list .p-tags a::before { + content: "#"; + opacity: 0.5; +} +.post-list .p-read { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-muted); + letter-spacing: 0.06em; + text-align: right; + padding-top: 0.35rem; +} +.post-list .p-read b { + display: block; + font-family: var(--font-serif); + font-style: normal; + font-weight: 400; + font-size: 1.1rem; + color: var(--text-bright); + letter-spacing: -0.02em; + margin-bottom: 0.1rem; +} +.post-list .p-read .mini { + display: block; + margin-top: 0.25rem; + font-size: 0.6rem; + color: var(--text-muted); +} + +/* Series row */ +.post-list > li.is-series { + align-items: stretch; +} +.series-wrap { + grid-column: 2 / span 2; + padding: 1.1rem 1.2rem; + border: 1px solid rgba(212, 168, 83, 0.2); + background: rgba(212, 168, 83, 0.03); + border-radius: 0.3rem; +} +.series-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0; + gap: 1rem; +} +.series-wrap.is-expanded .series-head { + margin-bottom: 0.4rem; +} +.series-head .s-name { + font-family: var(--font-serif); + font-weight: 500; + font-size: 1.2rem; + color: var(--text-bright); + letter-spacing: -0.01em; +} +.series-head .s-badge { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--accent); + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 0.2rem 0.55rem; + border: 1px solid rgba(212, 168, 83, 0.35); + border-radius: 0.15rem; + white-space: nowrap; +} +.series-desc { + font-family: var(--font-text); + font-style: normal; + font-size: 0.95rem; + line-height: 1.55; + color: var(--text-muted); + margin-bottom: 0.9rem; + display: none; +} +.series-parts { + list-style: none; + display: none; + flex-direction: column; + gap: 0.1rem; +} +.series-wrap.is-expanded .series-desc { + display: block; +} +.series-wrap.is-expanded .series-parts { + display: flex; +} +.series-parts li { + border-top: 1px solid var(--border); +} +.series-parts li:first-child { + border-top: none; +} +.series-parts a { + display: grid; + grid-template-columns: 4.5rem 1fr auto; + gap: 1rem; + align-items: baseline; + padding: 0.55rem 0; + color: var(--text); +} +.series-parts a:hover { + color: var(--accent); +} +.series-parts .sp-num { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-muted); + letter-spacing: 0.08em; + text-transform: uppercase; +} +.series-parts .sp-title { + font-family: var(--font-text); + font-weight: 400; + font-size: 0.95rem; + color: var(--text-bright); + text-wrap: balance; +} +.series-parts a:hover .sp-title { + color: var(--accent); +} +.series-parts .sp-date { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-muted); + letter-spacing: 0.06em; +} +.series-toggle { + display: flex; + align-items: center; + background: none; + border: 1px solid rgba(212, 168, 83, 0.35); + border-radius: 0.15rem; + padding: 0.25rem 0.4rem; + color: var(--accent); + cursor: pointer; + transition: background 0.15s, transform 0.2s; + margin-left: auto; +} +.series-toggle:hover { + background: rgba(212, 168, 83, 0.1); +} +.series-toggle svg { + display: block; + transition: transform 0.2s; +} +.series-wrap.is-expanded .series-toggle svg { + transform: rotate(180deg); +} + +/* ============ Bottom grid: tags + archive ============ */ +.bottom-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 3rem; + align-items: start; + margin-top: 3rem; + padding-top: 2.5rem; + border-top: 1px solid var(--border-strong); +} +.bg-left, .bg-right { + min-width: 0; +} + +.card { + margin-bottom: 2.5rem; +} +.card-title { + font-family: var(--font-mono); + font-size: 0.65rem; + color: var(--text-muted); + letter-spacing: 0.15em; + text-transform: uppercase; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: baseline; +} +.card-title .c { + color: var(--accent); +} + +.tagcloud { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} +.tagcloud a { + display: inline-flex; + align-items: baseline; + gap: 0.35rem; + font-family: var(--font-mono); + font-size: 0.74rem; + color: var(--tag-color); + padding: 0.25rem 0.55rem; + border: 1px solid rgba(126, 184, 168, 0.2); + border-radius: 0.15rem; + transition: color 0.15s, border-color 0.15s, background 0.15s; +} +.tagcloud a::before { + content: "#"; + opacity: 0.5; +} +.tagcloud a .ct { + font-size: 0.62rem; + color: var(--text-muted); +} +.tagcloud a:hover { + color: var(--accent); + border-color: rgba(212, 168, 83, 0.35); + background: rgba(212, 168, 83, 0.05); +} + +.archive-list { + list-style: none; + font-family: var(--font-mono); + font-size: 0.78rem; +} +.archive-list li { + display: flex; + justify-content: space-between; + padding: 0.4rem 0; + border-bottom: 1px solid var(--border); + color: var(--text-muted); +} +.archive-list li:last-child { + border-bottom: none; +} +.archive-list li b { + color: var(--text-bright); + font-weight: 400; +} +.archive-list li a { + color: var(--text-muted); +} +.archive-list li a:hover { + color: var(--accent); +} + +/* ============ Newsletter ============ */ +.newsletter { + margin-top: 3.5rem; + padding: 2.25rem 2rem; + border: 1px solid var(--border); + border-radius: 0.35rem; + background: rgba(255, 255, 255, 0.015); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); + gap: 2.5rem; + align-items: center; +} +.newsletter .nl-title { + font-family: var(--font-serif); + font-weight: 400; + font-size: 1.75rem; + color: var(--text-bright); + line-height: 1.15; + margin-bottom: 0.5rem; + letter-spacing: -0.01em; +} +.newsletter .nl-title em { + font-style: italic; + color: var(--accent); +} +.newsletter .nl-desc { + font-family: var(--font-text); + font-style: normal; + font-size: 1rem; + color: var(--text-muted); + line-height: 1.6; +} +.nl-form-wrap { + min-width: 0; +} +.nl-form { + display: flex; + flex-direction: row; + gap: 0.5rem; + min-width: 0; +} +.nl-form input[type="email"] { + flex: 1 1 auto; + min-width: 0; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 0.2rem; + padding: 0.6rem 0.8rem; + color: var(--text); + font-family: var(--font-mono); + font-size: 0.82rem; + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; +} +.nl-form input[type="email"]:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(212, 168, 83, 0.2); +} +.nl-form input[type="email"]::placeholder { + color: var(--text-muted); +} +.nl-form button { + flex: 0 0 auto; + background: var(--accent); + color: var(--bg); + border: none; + border-radius: 0.2rem; + padding: 0.6rem 1.1rem; + font-family: var(--font-mono); + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} +.nl-form button:hover { + background: var(--accent-hover); +} +.nl-form button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.nl-count { + font-family: var(--font-mono); + font-size: 0.64rem; + color: var(--text-muted); + letter-spacing: 0.08em; + text-transform: uppercase; + margin-top: 0.7rem; + padding-top: 0.7rem; + border-top: 1px dashed var(--border); + display: flex; + justify-content: space-between; +} +.nl-count b { + color: var(--accent); + font-weight: 400; +} + +.nl-spinner { + display: none; + animation: spin 0.8s linear infinite; +} +.newsletter.is-loading .nl-btn-text { + display: none; +} +.newsletter.is-loading .nl-spinner { + display: inline-block; +} +.newsletter.is-success .nl-form { + display: none; +} +.nl-success, .nl-error { + display: none; + font-size: 0.88rem; + margin-top: 0.75rem; +} +.newsletter.is-success .nl-success { + display: block; + color: var(--tag-color); +} +.newsletter.is-error .nl-error { + display: block; + color: var(--danger); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ============ Post page ============ */ +.progress-top { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 2px; + background: transparent; + z-index: 50; + pointer-events: none; +} +.progress-top .bar { + height: 100%; + width: 0; + background: var(--accent); + transition: width 0.08s linear; + box-shadow: 0 0 8px rgba(212, 168, 83, 0.4); +} + +main.post-shell { + max-width: var(--max-width); + margin: 0 auto; + width: 100%; + padding: 2.5rem 1.5rem 4rem; +} + +.mm-art { + width: 8rem; + height: 8rem; + opacity: 0.9; + flex-shrink: 0; +} +.mm-art svg { + width: 100%; + height: 100%; + display: block; +} + +/* Header KV box (post meta) */ +.header-box { + border: 1px solid var(--border); + border-radius: 0.25rem; + padding: 0.9rem 1.1rem; + background: rgba(255, 255, 255, 0.015); + margin-bottom: 2.5rem; + font-family: var(--font-mono); + font-size: 0.72rem; +} +.header-box .hb-row { + display: grid; + grid-template-columns: 6.5rem minmax(0, 1fr); + gap: 1rem; + padding: 0.2rem 0; +} +.header-box .hb-k { + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.64rem; + padding-top: 0.15rem; +} +.header-box .hb-v { + color: var(--text-bright); + text-wrap: pretty; + overflow-wrap: anywhere; + min-width: 0; +} +.header-box .hb-v .tag { + color: var(--tag-color); + margin-right: 0.6rem; +} +.header-box .hb-v .tag::before { + content: "#"; + opacity: 0.5; +} +.header-box .hb-v a { + color: var(--text-bright); + border-bottom: 1px dashed var(--border-strong); +} +.header-box .hb-v a:hover { + color: var(--accent); + border-bottom-color: var(--accent); +} +.header-box .hb-v .muted { + color: var(--text-muted); +} + +/* Hero */ +.post-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 2.5rem; + align-items: start; + margin-bottom: 3rem; +} +.post-hero .hero-text { + min-width: 0; +} +.post-hero .eyebrow { + font-family: var(--font-mono); + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--text-muted); + margin-bottom: 1.25rem; + display: flex; + align-items: center; + gap: 0.75rem; +} +.post-hero .eyebrow::after { + content: ""; + flex: 1; + height: 1px; + background: var(--border); +} +.post-hero .eyebrow b { + color: var(--accent); + font-weight: 400; +} +.post-hero h1 { + font-family: var(--font-serif); + font-weight: 300; + font-variation-settings: "opsz" 144; + font-size: clamp(1.9rem, 4.3vw, 3.15rem); + line-height: 1.15; + letter-spacing: -0.02em; + color: var(--text-bright); + text-wrap: balance; + overflow-wrap: break-word; + margin-bottom: 1.75rem; +} +.post-hero .dek { + font-family: var(--font-serif); + font-weight: 300; + font-style: italic; + font-size: 1.2rem; + line-height: 1.5; + color: var(--text); + text-wrap: balance; + margin-bottom: 1.5rem; +} +.post-hero .byline { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.12em; + display: flex; + gap: 1.5rem; + flex-wrap: wrap; + padding-top: 1rem; + border-top: 1px solid var(--border); +} +.post-hero .byline b { + color: var(--text-bright); + font-weight: 400; +} +.post-hero .byline a { + color: var(--text-bright); +} +.post-hero .byline a:hover { + color: var(--accent); +} + +/* Series strip */ +.series-strip { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 2rem; + padding: 0.75rem 0.9rem; + background: rgba(212, 168, 83, 0.04); + border: 1px solid rgba(212, 168, 83, 0.2); + border-radius: 0.25rem; + font-family: var(--font-mono); + font-size: 0.7rem; +} +.series-strip .label { + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-right: 0.5rem; +} +.series-strip .parts { + display: flex; + gap: 0.25rem; + flex: 1; + min-width: 0; +} +.series-strip .parts a, .series-strip .parts .cur { + flex: 1; + min-width: 0; + text-align: center; + padding: 0.35rem 0.5rem; + border-radius: 0.15rem; + color: var(--text-muted); + background: rgba(255, 255, 255, 0.02); + transition: color 0.15s, background 0.15s; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.series-strip .parts a:hover { + color: var(--text-bright); + background: rgba(255, 255, 255, 0.04); +} +.series-strip .parts .cur { + color: var(--text-bright); + background: rgba(212, 168, 83, 0.15); +} +.series-strip .progress-pct { + color: var(--text-muted); + white-space: nowrap; +} +.series-strip .progress-pct b { + color: var(--accent); + font-weight: 400; +} + +/* Post content (serif editorial body) */ +.post-content { + font-family: var(--font-serif); + font-weight: 400; + font-size: 1.15rem; + line-height: 1.6; + color: #d4dce4; +} +.post-content p { + margin-bottom: 1.35em; + text-wrap: pretty; +} +.post-content > p:first-of-type .first-word { + color: var(--accent); +} + +.post-content h2 { + font-family: var(--font-serif); + font-weight: 400; + font-variation-settings: "opsz" 72; + font-size: 1.6rem; + color: var(--text-bright); + line-height: 1.15; + letter-spacing: -0.015em; + text-wrap: balance; + margin: 3.5rem 0 1.25rem; + padding-top: 1.25rem; + border-top: 1px solid var(--border-strong); + scroll-margin-top: 1.5rem; +} + +.post-content h3 { + font-family: var(--font-serif); + font-weight: 500; + font-style: italic; + font-size: 1.25rem; + color: var(--text-bright); + margin: 2.25rem 0 0.5rem; + line-height: 1.2; +} +.post-content h3::before { + content: "—"; + color: var(--accent); + margin-right: 0.5rem; + opacity: 0.7; +} + +.post-content em { + font-style: italic; + color: var(--text); +} +.post-content strong { + font-weight: 500; + color: var(--text-bright); +} +.post-content a { + color: var(--accent); + border-bottom: 1px solid rgba(212, 168, 83, 0.3); + transition: border-bottom-color 0.15s, color 0.15s; +} +.post-content a:hover { + color: var(--accent-hover); + border-bottom-color: var(--accent-hover); +} + +.post-content ul, .post-content ol { + margin: 1.5rem 0 1.5rem 1.5rem; + padding-left: 0.75rem; +} +.post-content li { + margin-bottom: 0.5rem; +} + +.post-content code { + font-family: var(--font-mono); + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border); + padding: 0.08em 0.35em; + border-radius: 0.2rem; + font-size: 0.82em; + color: var(--text-bright); +} +.post-content pre { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 0.3rem; + margin: 1.75rem 0; + overflow: hidden; + font-family: var(--font-mono); +} +.post-content pre code { + display: block; + background: none; + border: none; + padding: 1rem 1.25rem; + font-size: 0.83rem; + line-height: 1.65; + color: var(--text); + overflow-x: auto; + border-radius: 0; +} + +.post-content blockquote { + font-family: var(--font-serif); + font-weight: 300; + font-variation-settings: "opsz" 144; + font-size: 1.45rem; + line-height: 1.3; + color: var(--text-bright); + margin: 2.5rem 0; + padding: 1.25rem 0 1.25rem 1.5rem; + border-top: 1px solid var(--border-strong); + border-bottom: 1px solid var(--border-strong); + border-left: 2px solid var(--accent); + text-wrap: balance; + font-style: italic; +} + +.post-content img { + max-width: 100%; + border-radius: 0.375rem; + margin: 2rem 0; +} + +.post-content table { + width: 100%; + border-collapse: collapse; + margin: 2rem 0; + font-family: var(--font-text); + font-size: 0.9rem; +} +.post-content th, .post-content td { + padding: 0.6rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); +} +.post-content th { + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 400; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.post-content .mermaid { + margin: 2rem 0; + text-align: center; + font-family: var(--font-text); +} +.post-content .mermaid svg { + max-width: 100%; +} + +/* Margin notes rail (right) */ +.margin-notes { + position: sticky; + top: 1.5rem; + align-self: start; + font-family: var(--font-text); + font-size: 0.8rem; + line-height: 1.5; + color: var(--text-muted); + display: flex; + flex-direction: column; + gap: 0.2rem; +} +.mnote { + padding: 0.6rem 0 0.7rem 0.9rem; + border-left: 1px solid var(--border-strong); +} +.mnote .mref { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--accent); + letter-spacing: 0.1em; + text-transform: uppercase; + display: block; + margin-bottom: 0.3rem; +} + +/* Post footer / nav */ +.post-footer { + margin-top: 4rem; + padding-top: 2rem; + border-top: 1px solid var(--border); +} +.footer-title { + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 1rem; +} +.post-nav { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 3rem; +} +.post-nav .cell { + padding: 1rem 1.1rem; + border: 1px solid var(--border); + border-radius: 0.3rem; + background: rgba(255, 255, 255, 0.015); + transition: border-color 0.15s, background 0.15s; +} +.post-nav .cell:hover { + border-color: rgba(212, 168, 83, 0.3); + background: rgba(212, 168, 83, 0.03); +} +.post-nav .cell .dir { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 0.4rem; +} +.post-nav .cell .ttl { + font-family: var(--font-serif); + font-size: 1.02rem; + line-height: 1.35; + color: var(--text-bright); + font-weight: 400; + text-wrap: balance; +} +.post-nav .cell.right { + text-align: right; +} + +.related { + margin-bottom: 3rem; +} +.related-list { + list-style: none; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} +.related-list li a { + display: block; + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: 0.3rem; + color: var(--text); + transition: border-color 0.15s, color 0.15s; +} +.related-list li a:hover { + border-color: rgba(212, 168, 83, 0.3); + color: var(--accent); +} +.related-list li .rl-date { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 0.35rem; + display: block; +} +.related-list li .rl-title { + font-family: var(--font-serif); + font-size: 0.98rem; + line-height: 1.3; + color: var(--text-bright); +} +.related-list li a:hover .rl-title { + color: var(--accent); +} + +.comments { + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid var(--border); +} +.comments h2 { + font-family: var(--font-mono); + font-size: 0.68rem; + font-weight: 400; + color: var(--text-muted); + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 1.5rem; +} + +/* ============ About page ============ */ +.bio { + font-family: var(--font-text); + font-weight: 300; + font-size: 1.05rem; + line-height: 1.7; + color: var(--text); + max-width: 42rem; + text-wrap: pretty; +} +.bio p + p { + margin-top: 0.9rem; +} +.bio em { + color: var(--text-bright); + font-style: italic; +} + +.now-card { + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); + gap: 2.5rem; + align-items: start; + padding: 1.75rem; + border: 1px solid var(--border); + border-radius: 0.4rem; + background: linear-gradient( + to bottom right, + rgba(212, 168, 83, 0.03), + transparent 60% + ); +} +.now-head { + display: flex; + align-items: baseline; + gap: 0.75rem; + margin-bottom: 0.9rem; +} +.now-badge { + font-family: var(--font-mono); + font-size: 0.6rem; + color: var(--accent); + letter-spacing: 0.14em; + text-transform: uppercase; + padding: 0.2rem 0.5rem; + border: 1px solid rgba(212, 168, 83, 0.35); + border-radius: 0.15rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} +.now-badge .pulse { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + animation: pulse 2s ease-in-out infinite; +} +.now-role { + font-family: var(--font-serif); + font-weight: 400; + font-size: 1.6rem; + letter-spacing: -0.015em; + color: var(--text-bright); + line-height: 1.15; +} +.now-role em { + font-style: italic; + color: var(--accent); +} +.now-since { + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-muted); + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 1rem; +} +.now-desc { + font-family: var(--font-text); + font-weight: 300; + font-size: 1rem; + line-height: 1.65; + color: var(--text); + text-wrap: pretty; + margin-bottom: 1rem; +} +.now-tech { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.now-tech .tp { + font-family: var(--font-mono); + font-size: 0.64rem; + color: var(--text); + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + padding: 0.18rem 0.5rem; + border-radius: 0.15rem; +} +.now-facts { + display: grid; + gap: 0.65rem; + font-family: var(--font-mono); + font-size: 0.75rem; +} +.now-facts .row { + display: grid; + grid-template-columns: 5.5rem 1fr; + gap: 1rem; + padding: 0.4rem 0; + border-bottom: 1px dashed var(--border); +} +.now-facts .row:last-child { + border-bottom: none; +} +.now-facts .k { + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.64rem; + padding-top: 0.15rem; +} +.now-facts .v { + color: var(--text-bright); + text-wrap: balance; +} +.now-facts .v .accent { + color: var(--accent); +} +.now-facts .v a { + color: var(--text-bright); + border-bottom: 1px dashed var(--border-strong); +} +.now-facts .v a:hover { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* Timeline */ +.timeline-wrap { + display: grid; + grid-template-columns: 7rem minmax(0, 1fr); + gap: 2.5rem; + align-items: start; +} +.tl-axis { + position: sticky; + top: 2rem; + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-muted); +} +.tl-axis .tl-label { + font-size: 0.6rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 1rem; +} +.tl-axis .tl-bigyr { + font-family: var(--font-serif); + font-size: 2rem; + font-weight: 400; + color: var(--text-bright); + letter-spacing: -0.02em; + line-height: 1; +} +.tl-axis .tl-bigyr span { + color: var(--text-muted); + font-size: 1rem; + margin-left: 0.25rem; +} +.tl-axis .tl-bigyr-sub { + font-size: 0.62rem; + color: var(--text-muted); + letter-spacing: 0.08em; + text-transform: uppercase; + margin-top: 0.2rem; +} +.tl-axis .tl-key { + display: grid; + gap: 0.45rem; + font-size: 0.65rem; + margin-top: 1.2rem; + padding-top: 1rem; + border-top: 1px solid var(--border); +} +.tl-axis .tl-key .row { + display: flex; + align-items: center; + gap: 0.55rem; + color: var(--text-muted); +} +.tl-axis .tl-key .sw { + width: 8px; + height: 8px; + border-radius: 2px; + flex-shrink: 0; +} +.tl-axis .tl-key .sw.lead { + background: var(--accent); +} +.tl-axis .tl-key .sw.found { + background: var(--tag-color); +} +.tl-axis .tl-key .sw.eng { + background: var(--eng-color); +} +.tl-axis .tl-key .sw.other { + background: var(--text-muted); +} + +.tl-track { + position: relative; + padding-left: 1.5rem; + min-width: 0; +} +.tl-track::before { + content: ""; + position: absolute; + left: 0; + top: 0.5rem; + bottom: 0.5rem; + width: 1px; + background: linear-gradient( + to bottom, + transparent, + var(--border-strong) 3%, + var(--border-strong) 97%, + transparent + ); +} +.tl-track::after { + content: ""; + position: absolute; + left: -2px; + top: 0; + width: 5px; + height: var(--pct, 16%); + background: linear-gradient(to bottom, var(--accent), transparent); + opacity: 0.6; + border-radius: 2px; + transition: height 0.4s cubic-bezier(0.22, 1, 0.36, 1); +} +.tl-item { + position: relative; + padding: 0 0 2.5rem 0; + transition: opacity 0.2s; +} +.tl-item.hidden { + display: none; +} +.tl-dot { + position: absolute; + left: -1.75rem; + top: 0.55rem; + width: 0.65rem; + height: 0.65rem; + border-radius: 50%; + background: var(--bg); + border: 2px solid var(--text-muted); + transition: all 0.2s; +} +.tl-item[data-kind="lead"] .tl-dot { + border-color: var(--accent); +} +.tl-item[data-kind="found"] .tl-dot { + border-color: var(--tag-color); +} +.tl-item[data-kind="eng"] .tl-dot { + border-color: var(--eng-color); +} +.tl-item.expanded .tl-dot { + box-shadow: 0 0 0 4px rgba(212, 168, 83, 0.12); + transform: scale(1.15); +} +.tl-item[data-kind="lead"].expanded .tl-dot { + background: var(--accent); + box-shadow: 0 0 0 4px rgba(212, 168, 83, 0.12); +} +.tl-item[data-kind="found"].expanded .tl-dot { + background: var(--tag-color); + box-shadow: 0 0 0 4px rgba(126, 184, 168, 0.12); +} +.tl-item[data-kind="eng"].expanded .tl-dot { + background: var(--eng-color); + box-shadow: 0 0 0 4px rgba(61, 123, 138, 0.12); +} + +.tl-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + align-items: baseline; + cursor: pointer; + padding: 0.2rem 0; + user-select: none; +} +.tl-header:hover .tl-company { + color: var(--accent); +} +.tl-when { + font-family: var(--font-mono); + font-size: 0.64rem; + color: var(--text-muted); + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 0.2rem; +} +.tl-when .dur { + color: var(--accent); + opacity: 0.8; + margin-left: 0.4rem; +} +.tl-company { + font-family: var(--font-serif); + font-weight: 400; + font-size: 1.4rem; + letter-spacing: -0.015em; + color: var(--text-bright); + line-height: 1.2; + transition: color 0.15s; +} +.tl-role { + font-family: var(--font-serif); + font-style: italic; + font-weight: 300; + font-size: 1rem; + color: var(--text); + margin-top: 0.2rem; +} +.tl-toggle { + font-family: var(--font-mono); + font-size: 0.6rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); + padding: 0.2rem 0.5rem; + border: 1px solid var(--border); + border-radius: 0.15rem; + transition: all 0.15s; + white-space: nowrap; +} +.tl-header:hover .tl-toggle { + color: var(--accent); + border-color: rgba(212, 168, 83, 0.3); +} +.tl-item.expanded .tl-toggle { + color: var(--accent); + border-color: rgba(212, 168, 83, 0.4); + background: rgba(212, 168, 83, 0.05); +} +.tl-item.expanded .tl-toggle::after { + content: " −"; +} +.tl-item:not(.expanded) .tl-toggle::after { + content: " +"; +} + +.tl-detail { + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.35s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.25s; +} +.tl-item.expanded .tl-detail { + max-height: 60rem; + opacity: 1; + margin-top: 0.9rem; +} +.tl-detail-inner { + padding: 1rem 1.1rem; + border: 1px solid var(--border); + border-radius: 0.3rem; + background: rgba(255, 255, 255, 0.015); +} +.tl-detail p { + font-family: var(--font-text); + font-weight: 300; + font-size: 0.98rem; + line-height: 1.65; + color: var(--text); + text-wrap: pretty; + margin-bottom: 0.7rem; +} +.tl-detail p:last-child { + margin-bottom: 0; +} +.tl-detail em { + color: var(--text-bright); + font-style: italic; +} +.tl-highlights { + list-style: none; + display: grid; + gap: 0.5rem; + margin-top: 0.9rem; + padding-top: 0.9rem; + border-top: 1px dashed var(--border); +} +.tl-highlights li { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--text); + padding-left: 1rem; + position: relative; + line-height: 1.5; +} +.tl-highlights li::before { + content: "▸"; + position: absolute; + left: 0; + color: var(--accent); + opacity: 0.7; +} +.tl-highlights li b { + color: var(--accent); + font-weight: 400; +} +.tl-sub-role { + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 0.9rem 0 0.4rem; + padding-top: 0.6rem; + border-top: 1px dashed var(--border); + display: flex; + gap: 0.7rem; + align-items: baseline; + flex-wrap: wrap; +} +.tl-sub-role:first-of-type { + border-top: none; + padding-top: 0; + margin-top: 0; +} +.tl-sub-role .sr-title { + color: var(--text-bright); + letter-spacing: -0.005em; + font-size: 0.9rem; + text-transform: none; + font-family: var(--font-serif); + font-style: italic; + font-weight: 400; +} +.tl-sub-role .sr-when { + color: var(--accent); + font-size: 0.6rem; +} + +/* Contact CTA (shared by About + redirect) */ +.contact-cta { + margin-top: 3.5rem; + padding: 2.25rem 2rem; + border: 1px solid var(--border); + border-radius: 0.35rem; + background: rgba(255, 255, 255, 0.015); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); + gap: 2.5rem; + align-items: center; +} +.contact-cta .cc-title { + font-family: var(--font-serif); + font-weight: 400; + font-size: 1.75rem; + color: var(--text-bright); + line-height: 1.15; + margin-bottom: 0.5rem; + letter-spacing: -0.01em; +} +.contact-cta .cc-title em { + font-style: italic; + color: var(--accent); +} +.contact-cta .cc-sub { + font-family: var(--font-text); + font-style: normal; + font-size: 1rem; + color: var(--text-muted); + line-height: 1.6; +} +.cc-links { + display: grid; + gap: 0.5rem; +} +.cc-link { + display: grid; + grid-template-columns: 5.5rem 1fr auto; + gap: 1rem; + align-items: center; + padding: 0.75rem 0.9rem; + border: 1px solid var(--border); + border-radius: 0.25rem; + background: rgba(255, 255, 255, 0.015); + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text); + transition: all 0.15s; +} +.cc-link:hover { + border-color: var(--accent); + background: rgba(212, 168, 83, 0.04); + color: var(--text); +} +.cc-link .ck { + color: var(--text-muted); + text-transform: uppercase; + font-size: 0.62rem; + letter-spacing: 0.1em; +} +.cc-link .cv { + color: var(--text-bright); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cc-link .arr { + color: var(--accent); + opacity: 0.7; + font-family: var(--font-mono); + transition: transform 0.15s; +} +.cc-link:hover .arr { + transform: translate(2px, -2px); +} + +/* ============ Tag pages ============ */ +.tag-page { + margin-bottom: 2rem; +} +.tag-page h1 { + font-family: var(--font-serif); + font-weight: 400; + font-size: 2.15rem; + color: var(--text-bright); + letter-spacing: -0.02em; + margin-bottom: 0.5rem; +} +.tag-page .tag-label { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--text-muted); + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 1rem; +} +.back-to-tags { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-muted); +} +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +/* ============ Projects page (kept from old) ============ */ +.project-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.project { + position: relative; + padding: 1.75rem 0; + border-bottom: 1px solid var(--border); + display: grid; + grid-template-columns: minmax(0, 1fr) 14rem; + grid-template-areas: + "header shot" + "blurb shot" + "meta shot"; + column-gap: 3rem; + row-gap: 0.75rem; + align-items: start; +} +.project-art { + display: none; +} +.project-header { + grid-area: header; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1.5rem; +} +.project-name { + font-family: var(--font-serif); + font-size: 1.5rem; + font-weight: 400; + letter-spacing: -0.015em; + color: var(--text-bright); +} +.project-links { + display: flex; + gap: 1rem; +} +.project-link { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--accent); +} +.project-blurb { + grid-area: blurb; + font-family: var(--font-text); + color: var(--text); + font-size: 1.02rem; + line-height: 1.65; +} +.project-meta { + grid-area: meta; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.project-tech { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--tag-color); + padding: 0.2rem 0.5rem; + border: 1px solid rgba(126, 184, 168, 0.2); + border-radius: 0.2rem; +} +.project-screenshot { + grid-area: shot; + align-self: center; + width: 100%; + aspect-ratio: 1; + border: 1px solid var(--border); + border-radius: 0.35rem; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 0.75rem; + opacity: 0.4; + background: var(--bg-surface); + overflow: hidden; +} +.project-screenshot.has-image { + border: 1px solid var(--border); + opacity: 1; + background: #0d1117; +} +.project-screenshot.has-image img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* ============ Not found ============ */ +.not-found { + text-align: center; + padding: 4rem 0; +} +.not-found h1 { + font-family: var(--font-serif); + font-size: 4.5rem; + font-weight: 300; + color: var(--accent); + margin-bottom: 1rem; + letter-spacing: -0.05em; +} +.not-found p { + color: var(--text-muted); + font-size: 1.1rem; +} + +/* ============ Generative art keyframes ============ */ +@keyframes art-spin { + to { + transform: rotate(360deg); + } +} +@keyframes art-draw-in { + to { + stroke-dashoffset: 0; + } +} +@keyframes art-dot-in { + to { + opacity: var(--op, 0.6); + } +} +@keyframes art-pulse-scale { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(2); + } +} +.art-spin { + transform-origin: 50% 50%; + animation: art-spin var(--dur) linear infinite; + animation-direction: var(--dir); +} +.art-draw { + stroke-dasharray: 1; + stroke-dashoffset: 1; + animation: art-draw-in 0.8s ease-out var(--d, 0s) forwards; +} +.art-dot { + opacity: 0; + animation: art-dot-in 0.4s ease-out var(--d, 0s) forwards; +} +.art-pulse-dot { + opacity: 0; + transform-box: fill-box; + transform-origin: center; + animation: + art-dot-in 0.4s ease-out var(--d, 0s) forwards, + art-pulse-scale var(--pd, 3s) ease-in-out calc(var(--d, 0s) + 0.5s) + infinite; +} +.art-orbit { + transform-origin: 50px 50px; + animation: art-spin var(--dur, 4s) linear infinite; + animation-direction: var(--dir, normal); +} + +/* ============ Responsive ============ */ +@media (max-width: 1200px) { + .margin-notes { + display: none; + } +} +@media (max-width: 900px) { + .masthead { + grid-template-columns: 1fr; + gap: 1.75rem; + } + .masthead:has(.portrait-wrap.has-outline) { + grid-template-columns: 1fr; + } + .masthead-right { + max-width: 12rem; + } + .featured { + grid-template-columns: 1fr; + gap: 1.75rem; + } + .feat-art { + max-width: 12.5rem; + } + .project { + grid-template-columns: 1fr; + grid-template-areas: + "header" + "shot" + "blurb" + "meta"; + column-gap: 0; + row-gap: 1rem; + } + .project-screenshot { + max-width: 14rem; + align-self: start; + } + .bottom-grid { + grid-template-columns: 1fr; + gap: 0; + } + .now-card { + grid-template-columns: 1fr; + gap: 1.5rem; + } + .timeline-wrap { + grid-template-columns: 1fr; + gap: 1.5rem; + } + .tl-axis { + position: static; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); + } + .tl-axis .tl-key { + display: flex; + gap: 1rem; + margin-top: 0; + padding-top: 0; + border-top: none; + } + .contact-cta { + grid-template-columns: 1fr; + gap: 1.25rem; + padding: 1.5rem; + } + .newsletter { + grid-template-columns: 1fr; + gap: 1.25rem; + padding: 1.5rem; + } + .post-list > li { + grid-template-columns: 4rem 1fr; + gap: 1rem; + } + .post-list .p-art { + width: 4rem; + height: 4rem; + } + .post-list .p-read { + grid-column: 2; + text-align: left; + padding-top: 0.25rem; + } + .series-wrap { + grid-column: 1 / -1; + } +} +@media (max-width: 820px) { + main.post-shell { + padding: 2rem 1.25rem 3rem; + } + .post-hero { + grid-template-columns: 1fr; + gap: 1.25rem; + } + .post-hero .mm-art { + width: 5rem; + height: 5rem; + order: -1; + } + .progress-top { + display: block; + } + .post-content { + font-size: 1.05rem; + } + .post-hero h1 { + font-size: 2rem; + } + .post-hero .dek { + font-size: 1.05rem; + } + .post-content h2 { + font-size: 1.4rem; + } + .post-content h3 { + font-size: 1.15rem; + } + .post-content pre { + font-size: 0.78rem; + } + .post-content img, + .post-content video, + .post-content iframe { + max-width: 100%; + height: auto; + } + .header-box { + font-size: 0.78rem; + } + .header-box .hb-row { + flex-wrap: wrap; + gap: 0.35rem 0.65rem; + } + .byline { + flex-wrap: wrap; + gap: 0.5rem 1rem; + } + .series-strip { + flex-direction: column; + align-items: stretch; + gap: 0.6rem; + } + .series-strip .label { + margin-right: 0; + text-align: center; + } + .series-strip .parts { + flex-wrap: wrap; + gap: 0.35rem; + justify-content: center; + } + .series-strip .parts a, .series-strip .parts .cur { + flex: 0 0 auto; + min-width: 2.5rem; + padding: 0.4rem 0.6rem; + white-space: nowrap; + overflow: visible; + text-overflow: clip; + } + .series-strip .sp-sep, + .series-strip .sp-preview { + display: none; + } + .related-list { + grid-template-columns: 1fr; + } + .post-nav { + grid-template-columns: 1fr; + gap: 0.5rem; + } + .post-nav .cell.right { + text-align: left; + } +} +@media (max-width: 560px) { + main.shell { + padding: 1.5rem 1rem 2.5rem; + } + .site-nav { + padding: 0.85rem 0; + } + .site-nav .wrap { + padding: 0 1rem; + gap: 0.75rem; + } + .site-nav .brand { + font-size: 0.82rem; + } + .site-nav .links { + gap: 1rem; + font-size: 0.72rem; + } + + .section-bar { + grid-template-columns: 1fr; + row-gap: 0.5rem; + margin-bottom: 1.25rem; + text-align: center; + } + .section-bar .sb-label { + font-size: 1.15rem; + } + .section-bar .sb-filter { + gap: 0.3rem; + justify-content: center; + } + .section-bar .sb-filter button { + font-size: 0.6rem; + padding: 0.22rem 0.45rem; + } + + .masthead { + padding-bottom: 1.5rem; + margin-bottom: 1.75rem; + gap: 1.25rem; + text-align: center; + } + .masthead h1 { + font-size: 2rem; + margin-bottom: 0.75rem; + } + .masthead .dek { + font-size: 1rem; + margin-bottom: 1rem; + margin-left: auto; + margin-right: auto; + } + .mh-stats { + gap: 0.85rem 1.5rem; + padding-top: 0.75rem; + justify-content: center; + } + .mh-stats div b { + font-size: 1.2rem; + } + .masthead-right { + display: none; + } + + .featured { + padding: 0.25rem 0 1.75rem; + margin-bottom: 1.75rem; + gap: 1rem; + text-align: center; + } + .feat-art { + display: none; + } + .feat-main .f-eyebrow { + margin-bottom: 0.65rem; + font-size: 0.6rem; + justify-content: center; + } + .feat-main h2 { + font-size: clamp(1.4rem, 6vw, 1.8rem); + margin-bottom: 0.75rem; + } + .feat-main .dek { + font-size: 0.95rem; + margin-bottom: 0.9rem; + margin-left: auto; + margin-right: auto; + } + .feat-main .meta { + gap: 0.65rem 1rem; + font-size: 0.62rem; + justify-content: center; + } + + .post-list > li { + grid-template-columns: 3rem minmax(0, 1fr); + gap: 0.85rem; + padding: 1.1rem 0; + } + .post-list .p-art { + width: 3rem; + height: 3rem; + } + .post-list .p-date { + gap: 0.55rem; + font-size: 0.6rem; + margin-bottom: 0.3rem; + } + .post-list .p-title { + font-size: 1.15rem; + margin-bottom: 0.3rem; + } + .post-list .p-desc { + font-size: 0.92rem; + margin-bottom: 0.45rem; + } + .post-list .p-tags { + gap: 0.45rem; + font-size: 0.6rem; + } + .post-list .p-read { + grid-column: 2; + display: flex; + align-items: baseline; + gap: 0.5rem; + padding-top: 0.4rem; + font-size: 0.6rem; + } + .post-list .p-read b { + display: inline; + font-size: 0.82rem; + margin-bottom: 0; + } + .post-list .p-read .mini { + display: inline; + margin-top: 0; + font-size: 0.6rem; + } + + .series-wrap { + padding: 0.9rem 0.9rem; + } + .series-head { + flex-wrap: wrap; + gap: 0.35rem 0.75rem; + } + .series-head .s-name { + font-size: 1.05rem; + } + .series-desc { + font-size: 0.88rem; + margin-bottom: 0.7rem; + } + .series-wrap.is-expanded .series-head { + margin-bottom: 0.5rem; + } + .series-parts a { + grid-template-columns: 3.25rem minmax(0, 1fr); + gap: 0.6rem; + padding: 0.5rem 0; + } + .series-parts .sp-title { + font-size: 0.9rem; + grid-column: 2; + } + .series-parts .sp-date { + grid-column: 2; + font-size: 0.58rem; + } + + .bottom-grid { + margin-top: 2rem; + padding-top: 1.75rem; + } + .card { + margin-bottom: 1.75rem; + } + .card-title { + font-size: 0.6rem; + margin-bottom: 0.75rem; + } + .tagcloud a { + font-size: 0.7rem; + padding: 0.22rem 0.5rem; + } + .archive-list { + font-size: 0.72rem; + } + + .newsletter { + margin-top: 2.25rem; + padding: 1.25rem; + gap: 1rem; + border-radius: 0.25rem; + text-align: center; + } + .newsletter .nl-title { + font-size: 1.35rem; + } + .newsletter .nl-desc { + font-size: 0.92rem; + } + .nl-form { + flex-direction: column; + gap: 0.5rem; + } + .nl-form button { + width: 100%; + padding: 0.7rem 1rem; + } + .nl-count { + gap: 0.5rem; + flex-wrap: wrap; + justify-content: center; + } + .post-hero h1 { + font-size: 1.7rem; + } + .post-hero .dek { + font-size: 0.98rem; + } + .post-content { + font-size: 1rem; + } + .post-content h2 { + font-size: 1.25rem; + margin: 2.5rem 0 1rem; + } + .post-content h3 { + font-size: 1.1rem; + } + .post-content pre { + padding: 0.75rem; + font-size: 0.74rem; + border-radius: 0.25rem; + } + .post-content blockquote { + padding-left: 1rem; + } + .project-name { + font-size: 1.25rem; + } + .project-blurb { + font-size: 0.95rem; + } + .project-screenshot { + max-width: 11rem; + } + .project-header { + flex-wrap: wrap; + gap: 0.5rem 1rem; + } + .now-role { + font-size: 1.1rem; + } + .now-card { + padding: 1.25rem; + } + .now-tech { + gap: 0.35rem; + } + .tl-header { + grid-template-columns: 1fr; + } + .tl-toggle { + justify-self: start; + margin-top: 0.3rem; + } + .tl-company { + font-size: 1.15rem; + } + .tl-when { + font-size: 0.7rem; + flex-wrap: wrap; + } + .cc-link { + padding: 0.85rem 1rem; + gap: 0.4rem; + flex-wrap: wrap; + } + .cc-link .cv { + font-size: 0.85rem; + } + .site-nav .wrap { + flex-direction: column; + gap: 0.75rem; + } + .site-nav .links { + gap: 1rem; + flex-wrap: wrap; + justify-content: center; + } + main.shell { + padding: 2rem 1rem 3rem; + } + main.post-shell { + padding: 1.75rem 1rem 3rem; + } + .header-box { + padding: 0.85rem 1rem; + } + .post-nav .cell { + padding: 1rem; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + html { + scroll-behavior: auto; + } +} + +/* Visually hidden (a11y) */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ============ Skip link (a11y / SEO) ============ */ +.skip-link { + position: absolute; + left: -9999px; + top: 0; + background: var(--accent); + color: var(--bg); + padding: 0.5rem 1rem; + font-family: var(--font-mono); + font-size: 0.75rem; + z-index: 999; +} +.skip-link:focus { + left: 1rem; + top: 1rem; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-greek-ext.woff2') format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-greek.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-math.woff2') format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-symbols.woff2') format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-vietnamese.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; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-latin-ext.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; +} + +@font-face { + font-family: 'text'; + font-style: italic; + font-weight: 100 900; + src: url('fonts/text-italic-100_900-latin.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; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-greek-ext.woff2') format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-greek.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-math.woff2') format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-symbols.woff2') format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-vietnamese.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; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-latin-ext.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; +} + +@font-face { + font-family: 'text'; + font-style: normal; + font-weight: 100 900; + src: url('fonts/text-normal-100_900-latin.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; +} +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 400; + src: url('fonts/code-normal-400-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 400; + src: url('fonts/code-normal-400-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 400; + src: url('fonts/code-normal-400-greek.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 400; + src: url('fonts/code-normal-400-vietnamese.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; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 400; + src: url('fonts/code-normal-400-latin-ext.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; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 400; + src: url('fonts/code-normal-400-latin.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; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 500; + src: url('fonts/code-normal-500-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 500; + src: url('fonts/code-normal-500-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 500; + src: url('fonts/code-normal-500-greek.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 500; + src: url('fonts/code-normal-500-vietnamese.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; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 500; + src: url('fonts/code-normal-500-latin-ext.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; +} + +@font-face { + font-family: 'code'; + font-style: normal; + font-weight: 500; + src: url('fonts/code-normal-500-latin.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; +} +@font-face { + font-family: 'serif'; + font-style: italic; + font-weight: 300; + src: url('fonts/serif-italic-300-vietnamese.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; +} + +@font-face { + font-family: 'serif'; + font-style: italic; + font-weight: 300; + src: url('fonts/serif-italic-300-latin-ext.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; +} + +@font-face { + font-family: 'serif'; + font-style: italic; + font-weight: 300; + src: url('fonts/serif-italic-300-latin.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; +} + +@font-face { + font-family: 'serif'; + font-style: italic; + font-weight: 400; + src: url('fonts/serif-italic-400-vietnamese.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; +} + +@font-face { + font-family: 'serif'; + font-style: italic; + font-weight: 400; + src: url('fonts/serif-italic-400-latin-ext.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; +} + +@font-face { + font-family: 'serif'; + font-style: italic; + font-weight: 400; + src: url('fonts/serif-italic-400-latin.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 300; + src: url('fonts/serif-normal-300-vietnamese.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 300; + src: url('fonts/serif-normal-300-latin-ext.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 300; + src: url('fonts/serif-normal-300-latin.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 400; + src: url('fonts/serif-normal-400-vietnamese.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 400; + src: url('fonts/serif-normal-400-latin-ext.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 400; + src: url('fonts/serif-normal-400-latin.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 500; + src: url('fonts/serif-normal-500-vietnamese.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 500; + src: url('fonts/serif-normal-500-latin-ext.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; +} + +@font-face { + font-family: 'serif'; + font-style: normal; + font-weight: 500; + src: url('fonts/serif-normal-500-latin.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; +} diff --git a/bin/unit_tests/assets/html/body_height_miscalculation.html b/bin/unit_tests/assets/html/body_height_miscalculation.html new file mode 100644 index 000000000..8297798a9 --- /dev/null +++ b/bin/unit_tests/assets/html/body_height_miscalculation.html @@ -0,0 +1,466 @@ + + + + + + + + What if your browser built the UI for you? — jonno.nz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ published + + + + +
+ + +
+ reading + 5 min · 1,132 words +
+ + +
+ tags + + aiarchitectureengineering + +
+ +
+ +
+
+ +

What if your browser built the UI for you?

+

We're still shipping hand-crafted frontends while + AI can generate entire interfaces. What if the browser itself generated + the UI from an API manifest and your preferences?

+ +
+ +
+ + + +
+

We're at a genuinely weird inflection point in frontend development. AI can +generate entire interfaces now. LLMs can reason about data and layout. And yet — +most SaaS products still ship hand-crafted React apps, each building its own UI, +its own accessibility layer, its own theme system, its own responsive +breakpoints. Not every service, but the vast majority.

+

That's a lot of duplicated effort for what's essentially the same job — showing +a human some data and letting them do stuff with it.

+

I've been thinking about this a lot lately, and I built a proof of concept to +test an idea: what if the browser itself generated the UI?

+

Where we are right now

+

The industry is circling this idea from multiple angles, but nobody's quite +landed on it yet.

+

Server-driven UI +has been around for a while — Airbnb and others pioneered it for mobile, where +app store review cycles make shipping UI changes painful. The server sends down +a JSON tree describing what to render, and the client just follows instructions. +It's clever, but the server is still calling the shots. x.

+

Google recently shipped +Natively Adaptive Interfaces +— a framework that uses AI agents to make accessibility a default rather than an +afterthought. Really cool idea, and the right instinct. But it's still operating +within a single app's boundaries. Your accessibility preferences don't carry +between Google's products and, say, your project management tool.

+

Then there's the +generative UI +wave — CopilotKit, Vercel's AI SDK, and others building frameworks where LLMs +generate components on the fly. These are powerful developer tools, but they're +still developer tools. The generation happens at build time or on the server. +The service is still in control.

+

See the pattern? Every approach keeps the power on the service side.

+

Flip it

+

Here's the idea behind the +adaptive browser: what if the +generation happened on your side?

+

Instead of a service shipping you a finished frontend, it publishes a manifest — +a structured description of what it can do. Its capabilities, endpoints, data +shapes, what actions are available. Think of it like an API spec, but semantic. +Not just "here's a GET endpoint" but "here's a list of repositories, they're +sortable by stars and language, you can create, delete, star, or fork them."

+

Your browser takes that manifest, calls the actual APIs, gets real data back, +and then generates the UI based on your preferences. Your font size. Your colour +scheme. Your preferred layout (tables vs cards vs kanban). Your accessibility +needs. All applied universally, across every service.

+

The manifest for something like GitHub looks roughly like this — a service +describes its capabilities and the browser figures out the rest:

+
service:
+  name: "GitHub"
+  domain: "api.github.com"
+
+capabilities:
+  - id: "repositories"
+    endpoints:
+      - path: "/user/repos"
+        semantic: "list"
+        entity: "repository"
+        sortable_fields: [name, updated_at, stargazers_count]
+        actions: [create, delete, star, fork]
+
+

The browser takes that, fetches the data, and generates a bespoke interface — +using an LLM to reason about the best way to present it given who you are and +what you're trying to do.

+

Why this matters more than it sounds

+

When I was building the app store and integrations platforms at Xero, one of the +constant headaches was that every third-party integration had its own UI +patterns. Users had to learn a new interface for every app they connected. If +the browser was generating the UI from a shared set of preferences, that problem +just… goes away.

+

Accessibility is the big one though. Right now, accessibility is a feature that +gets bolted on — and often badly. When the browser generates the UI, +accessibility isn't a feature. It's the default. Your preferences — high +contrast, keyboard-first navigation, screen reader optimisation, larger text — +apply everywhere. Not because every developer remembered to implement them, but +because they're baked into how the UI gets generated in the first place.

+

Customisation becomes genuinely personal too. Not "pick from three themes the +developer made" but "this is how I interact with software, full stop."

+

The trade-off is real though

+

Frontend complexity drops dramatically, but the complexity doesn't disappear — +it moves behind the API. And honestly, it probably increases.

+

API design becomes way more important. You can't just throw together some REST +endpoints and call it a day. Your manifest needs to be semantic — describing +what the data means, not just what shape it is. Data contracts between services +matter more. Versioning matters more.

+

Publishes manifest + APIs

Generates

Service

Browser Agent

User Preferences

Org Guardrails

Bespoke UI

+

But here's the thing — this trade-off pushes us somewhere genuinely interesting. +If every service needs to describe itself semantically through APIs and +manifests, those APIs become the actual product surface. Not the frontend. The +APIs.

+

And once APIs are the product surface, sharing context between platforms becomes +the interesting problem. Your project management tool knows what you're working +on. Your email client knows who you're talking to. Your code editor knows what +you're building. Right now, none of these talk to each other in any meaningful +way because they're all locked behind their own UIs. In a manifest-driven world, +that context flows through the APIs — and your browser can stitch it all +together into something coherent.

+

Where this is headed (IMHO)

+

I reckon we're about 3-5 years from this being mainstream. The pieces are all +there — LLMs that can reason about UI, +standardisation efforts around +sending UI intent over APIs, and a growing expectation from users that software +should adapt to them, not the other way around.

+

The services that win in this world won't be the ones with the prettiest +hand-crafted UI. They'll be the ones with the best APIs, the richest manifests, +and the most useful data. The frontend becomes a generated output, not a +hand-crafted input.

+

Organisations will set preference guardrails — "our people can use dark or light +mode, must have destructive action confirmations, these fields are always +visible" — while individuals customise within those bounds. Your browser becomes +your agent, not just a renderer.

+

I built the adaptive browser as +a proof of concept to test this thinking — it uses Claude to generate UIs from a +GitHub manifest and user preferences defined in YAML. It's rough, but the +direction feels right.

+

The frontend isn't dying. But what we think of as "frontend development" is +about to change. The interesting work moves to API design, semantic data +contracts, and building browsers smart enough to be genuine user agents.

+ +
+ + +
+ +
+ + + + +
diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index d6a33b121..2c33cf833 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -46,6 +46,8 @@ class EE_API UIHTMLWidget : public UILayout { void setCSSClear( CSSClear cssClear ); + Rectf getNormalFlowLayoutPixelsMargin() const; + const CSSBaselineAlignValue& getBaselineAlign() const { return mBaselineAlign; } void setBaselineAlign( const CSSBaselineAlignValue& baselineAlign ); diff --git a/src/eepp/ui/blocklayouter.cpp b/src/eepp/ui/blocklayouter.cpp index e04f65bfb..064ec85f1 100644 --- a/src/eepp/ui/blocklayouter.cpp +++ b/src/eepp/ui/blocklayouter.cpp @@ -435,7 +435,10 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { curCharIdx += 1; Rectf atomicBounds( maxF, maxF, lowF, lowF ); if ( getAtomicWidgetFragmentBounds( widget, atomicBounds ) ) { - Rectf margin = widget->getLayoutPixelsMargin(); + Rectf margin = + widget->isType( UI_TYPE_HTML_WIDGET ) + ? widget->asType()->getNormalFlowLayoutPixelsMargin() + : widget->getLayoutPixelsMargin(); Vector2f targetPos( atomicBounds.Left + margin.Left, atomicBounds.Top + margin.Top ); @@ -459,7 +462,10 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { size_t lineIdx = currentSpan > 0 ? currentLine : currentLine - 1; Float lineY = lines[lineIdx].y; - Rectf margin = widget->getLayoutPixelsMargin(); + Rectf margin = + widget->isType( UI_TYPE_HTML_WIDGET ) + ? widget->asType()->getNormalFlowLayoutPixelsMargin() + : widget->getLayoutPixelsMargin(); Vector2f targetPos( contentOffset.Left + span->position.x + margin.Left, contentOffset.Top + lineY + span->position.y + margin.Top ); diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index f6183b40b..3e30ee932 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -154,6 +154,15 @@ void UIHTMLWidget::setCSSClear( CSSClear cssClear ) { } } +Rectf UIHTMLWidget::getNormalFlowLayoutPixelsMargin() const { + Rectf margin = getLayoutPixelsMargin(); + if ( hasLayoutMarginTopAuto() ) + margin.Top = 0.f; + if ( hasLayoutMarginBottomAuto() ) + margin.Bottom = 0.f; + return margin; +} + void UIHTMLWidget::setBaselineAlign( const CSSBaselineAlignValue& baselineAlign ) { if ( mBaselineAlign != baselineAlign ) { mBaselineAlign = baselineAlign; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 676569d5d..79a32372b 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1331,7 +1331,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri "\n", widget->asType()->getRichText().getFontStyleConfig() ); lastSpanEndsWithSpace = false; } else { - Rectf margin = widget->getLayoutPixelsMargin(); + Rectf margin = + widget->isType( UI_TYPE_HTML_WIDGET ) + ? widget->asType()->getNormalFlowLayoutPixelsMargin() + : widget->getLayoutPixelsMargin(); bool isBlock = widget->getLayoutWidthPolicy() == SizePolicy::MatchParent; if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) { CSSDisplay display = widget->asType()->getDisplay(); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 2dea0a8e4..4f1c5f7ca 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -1650,6 +1650,8 @@ static UISceneNode* init_test_inline_block() { font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); FontFamily::loadFromRegular( font ); + FontTrueType* monoFont = FontTrueType::New( "monospace" ); + monoFont->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); UISceneNode* sceneNode = UISceneNode::New(); SceneManager::instance()->add( sceneNode ); SceneManager::instance()->setCurrentUISceneNode( sceneNode ); @@ -2002,6 +2004,33 @@ UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) { Engine::destroySingleton(); } +UTEST( UIHTML, BodyHeightMiscalculationFixture ) { + Engine::instance()->createWindow( WindowSettings( 1024, 653, "Body Height Miscalculation Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UI::UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + + std::string html; + FileSystem::fileGet( "assets/html/body_height_miscalculation.html", html ); + + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + sceneNode->update( Seconds( 1 ) ); + sceneNode->updateDirtyLayouts(); + + auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); + ASSERT_TRUE( bodyNode != nullptr ); + + auto bodyWidget = bodyNode->asType(); + + EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 3000.f ); + EXPECT_LT( bodyWidget->getPixelsSize().getHeight(), 6000.f ); + + Engine::destroySingleton(); +} + UTEST( UIHTML, ContactFormLayout ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Contact Form Layout Test", WindowStyle::Default, WindowBackend::Default,