0.2, updated UI
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m44s Details

This commit is contained in:
Isaac Johnson 2026-10-29 21:56:22 -05:00
parent c2b80047ae
commit f8acc55f67
8 changed files with 677 additions and 237 deletions

5
.hallmark/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
shots/
before/
before.json
before.txt
compare.html

26
.hallmark/log.json Normal file
View File

@ -0,0 +1,26 @@
[
{
"mode": "redesign",
"depth": "full",
"scope": "page",
"shape": "Workbench",
"theme": "studio",
"axes": "light/roman-serif/warm",
"hero": "Stage",
"nav": "edge-aligned",
"footer": "letter close",
"imagery": "the product",
"photos": [],
"entry": "app.py",
"fingerprint": "asymmetric-grid/serif-display/proof-sheet/hairline-set/colophon-close/restrained",
"sections": [
"Hero (Stage)",
"Bench notes (Set)",
"Colophon (Footer)"
],
"signature": "crop-marked proof sheet with orange metric construction guides",
"waived": [],
"prompt": "Use the hallmark skill to give this web app a professional clean look for a more hip modern dad looking for a joke",
"date": "2026-10-30"
}
]

20
.hallmark/preflight.json Normal file
View File

@ -0,0 +1,20 @@
{
"scanned": "2026-10-29",
"findings": {
"fonts": "System sans (-apple-system, BlinkMacSystemFont, Segoe UI, Roboto) + Georgia serif quote mark (app.py L43, L174)",
"palette": "Hardcoded hex literals (#ff0844, #ff4b2b, #ffdd00, #00d2ff, #0052d4, Slate #0f172a..#f8fafc) in inline <style> (app.py L45-L270)",
"motion": "CSS keyframes (gradientShift, floatOrbs, spin) + transition: all (app.py L47, L62, L213, L241)",
"spacing": "Ad-hoc pixel values (24px, 44px, 38px, 26px, 14px, 28px) (app.py L42-L275)",
"framework": "Flask 3.0.0 with inline render_template_string (app.py L4, L24)"
},
"preserve": [
"Flask routes (/ and /api/joke)",
"Ollama integration and version.ini metadata",
"DOM hooks (#refreshBtn, #btnText, #spinner, #joke, fetchNewJoke())"
],
"introduce": [
"Hallmark token system",
"Editorial theme & typography",
"Accessible focus, reduced-motion, and control states"
]
}

1
.hallmark/salt Normal file
View File

@ -0,0 +1 @@
3aca28a0

800
app.py
View File

@ -1,7 +1,7 @@
import os import os
import configparser import configparser
import requests import requests
from flask import Flask, render_template_string, jsonify from flask import Flask, render_template_string, jsonify, send_from_directory
app = Flask(__name__) app = Flask(__name__)
@ -26,320 +26,652 @@ HTML_TEMPLATE = """<!DOCTYPE html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Programming Dad Jokes</title> <meta name="description" content="Dry programming dad jokes set on a typographic studio proof sheet, pulled live from your local Ollama model." />
<title>Programming Dad Joke</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Hedvig+Letters+Serif:opsz@12..24&family=Geist:wght@400;500&display=swap" rel="stylesheet" />
<style> <style>
* { /* Hallmark · shape: Workbench (draw 9cbbc9) · genre: editorial · theme: Studio (deal 3/3, seed programming-dad-joke-generator-254f·3aca28a0·2026-10-30·r0) · redesign: full
box-sizing: border-box; * hero: Stage · nav: edge-aligned · footer: letter close · imagery: the product · signature: crop-marked proof sheet with orange metric construction guides
margin: 0; * axes: light/roman-serif/warm · posture: restrained · contrast: pass · fold: pass · v2.0.0 */
padding: 0; /* Hallmark · pre-emit critique: P5 H5 E5 S4 R5 V5 · weakest: S, generic joke card could fit any trivia app; framed the joke as a crop-marked Studio proof sheet with live typographic metric guides */
} /* Hallmark · reworded "Tell Me Another Joke! 🚀" to "Pull another joke" · 30a bans emoji in controls */
/* Hallmark · reworded "Freshly compiled humor from our AI model" to "Dry humor for engineers after hours, pulled live from your local model." · 19 names generic AI filler */
/* Hallmark · dropped "Ollama • Gemma 4" · 54 and 63 ban middle-dot eyebrow badges */
@import url("./tokens.css");
@import url("./base.css");
body { .skip {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
/* Vibrant animated gradient with bright red, yellow, and blue */
background: linear-gradient(-45deg, #ff0844 0%, #ff4b2b 20%, #ffdd00 45%, #00d2ff 75%, #0052d4 100%);
background-size: 400% 400%;
animation: gradientShift 14s ease infinite;
position: relative;
overflow-x: hidden;
}
/* Ambient glowing gradient orbs for depth */
body::before,
body::after {
content: "";
position: fixed;
border-radius: 50%;
filter: blur(80px);
z-index: 0;
pointer-events: none;
opacity: 0.65;
animation: floatOrbs 10s ease-in-out infinite alternate;
}
body::before {
width: 380px;
height: 380px;
background: radial-gradient(circle, #ff0055 0%, #ffaa00 100%);
top: -80px;
left: -80px;
}
body::after {
width: 420px;
height: 420px;
background: radial-gradient(circle, #00d2ff 0%, #0044ff 100%);
bottom: -100px;
right: -100px;
animation-delay: -5s;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes floatOrbs {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(40px, 40px) scale(1.1); }
}
.container {
position: relative;
z-index: 1;
width: 100%;
max-width: 680px;
}
.card {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 28px;
border: 2px solid rgba(255, 255, 255, 0.85);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.22),
0 0 35px rgba(255, 255, 255, 0.35);
padding: 44px 38px;
text-align: center;
transition: transform 0.3s ease;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #1e293b;
background: linear-gradient(90deg, rgba(255, 75, 43, 0.15), rgba(255, 221, 0, 0.25), rgba(0, 210, 255, 0.2));
border: 1px solid rgba(255, 75, 43, 0.3);
margin-bottom: 20px;
}
.badge-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 8px #10b981;
}
.badge-divider {
opacity: 0.4;
margin: 0 2px;
}
h1 {
font-size: 2.2rem;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.02em;
margin-bottom: 8px;
}
.subtitle {
font-size: 1rem;
color: #64748b;
margin-bottom: 28px;
}
.joke-box {
background: #f8fafc;
border-radius: 18px;
padding: 26px 24px;
margin-bottom: 30px;
position: relative;
border: 1px solid #e2e8f0;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
text-align: left;
}
.joke-box::before {
content: '"';
position: absolute; position: absolute;
top: -12px; left: var(--space-md);
left: 18px; top: calc(-1 * var(--space-3xl));
font-size: 3.5rem; z-index: var(--z-toast);
line-height: 1; padding: var(--space-xs) var(--space-md);
font-family: Georgia, serif; background: var(--color-ink);
color: #ff0844; color: var(--color-paper);
opacity: 0.3; text-decoration: none;
pointer-events: none;
} }
.joke-text { .skip:focus-visible {
font-size: 1.25rem; top: var(--space-md);
line-height: 1.7; }
color: #1e293b;
font-weight: 500; .proof {
white-space: pre-wrap; background-image: radial-gradient(circle at 1px 1px, var(--color-dot) 1px, transparent 1.4px);
word-break: break-word; background-size: 24px 24px;
min-height: 80px; }
.crop {
position: relative;
--l: 8px;
--o: 12px;
}
.crop::before {
content: "";
position: absolute;
inset: calc(-1 * var(--o));
pointer-events: none;
background:
linear-gradient(var(--color-mark), var(--color-mark)) 0 var(--o) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) var(--o) 0 / 1px var(--l) no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) 100% var(--o) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) calc(100% - var(--o)) 0 / 1px var(--l) no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) 0 calc(100% - var(--o)) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) var(--o) 100% / 1px var(--l) no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) 100% calc(100% - var(--o)) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) calc(100% - var(--o)) 100% / 1px var(--l) no-repeat;
}
.site-header {
border-bottom: var(--rule-hair) solid var(--color-rule);
padding-block: var(--space-md);
}
.site-header__inner {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: var(--space-lg);
}
.wordmark {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: var(--display-weight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
text-decoration: none;
white-space: nowrap;
}
.wordmark:hover {
text-decoration: underline;
text-decoration-color: var(--color-accent);
}
.site-nav {
display: flex;
align-items: center;
gap: var(--space-lg);
font-size: var(--text-sm);
color: var(--color-ink-2);
}
.site-nav a {
text-decoration: none;
white-space: nowrap;
}
.site-nav a:hover {
text-decoration: underline;
text-decoration-color: var(--color-accent);
}
.hero {
padding-block: var(--space-3xl);
border-bottom: var(--rule-hair) solid var(--color-rule);
}
.hero__grid {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--gap-columns);
align-items: center;
}
.hero__lead {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-lg);
}
.hero__lead .lead {
color: var(--color-ink-2);
} }
.actions { .actions {
display: flex; display: flex;
justify-content: center; align-items: center;
gap: 14px; gap: var(--space-md);
margin-bottom: 24px; margin-top: var(--space-xs);
} }
.btn { .btn {
position: relative;
appearance: none; appearance: none;
border: none;
outline: none;
cursor: pointer;
font-family: inherit;
font-size: 1rem;
font-weight: 700;
padding: 14px 28px;
border-radius: 14px;
color: #ffffff;
background: linear-gradient(135deg, #ff0844 0%, #ff8c00 40%, #0099ff 100%);
background-size: 200% 200%;
box-shadow: 0 10px 25px rgba(255, 8, 68, 0.35);
transition: all 0.25s ease;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; justify-content: center;
gap: var(--space-xs);
min-height: 48px;
padding: 0 var(--space-lg);
border: var(--rule-hair) solid var(--color-ink);
border-radius: var(--radius-card);
background: var(--color-paper-hi);
color: var(--color-ink);
font-family: var(--font-body);
font-size: var(--text-md);
font-weight: 500;
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
transition: background-color var(--dur-short) var(--ease-out), transform var(--dur-micro) var(--ease-out);
}
.btn::after {
content: "";
position: absolute;
inset: var(--space-3xs);
border: var(--rule-hair) solid var(--color-mark);
pointer-events: none;
}
.btn--primary {
background: var(--color-accent);
color: var(--color-accent-ink);
} }
.btn:hover:not(:disabled) { .btn:hover:not(:disabled) {
transform: translateY(-2px); transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(0, 153, 255, 0.45);
background-position: 100% 100%;
} }
.btn:active:not(:disabled) { .btn:active:not(:disabled) {
transform: translateY(1px); transform: translateY(1px);
} }
.btn:disabled { .btn:focus-visible {
opacity: 0.65; outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.55;
cursor: not-allowed; cursor: not-allowed;
transform: none;
}
.btn[aria-busy="true"] {
cursor: progress;
} }
.spinner { .spinner {
display: none; display: none;
width: 18px; width: 16px;
height: 18px; height: 16px;
border: 3px solid rgba(255, 255, 255, 0.35); border: 2px solid var(--color-mark);
border-top-color: #ffffff; border-top-color: var(--color-ink);
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin var(--dur-long) linear infinite;
flex-shrink: 0;
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
.meta { .sheet {
font-size: 0.82rem; background: var(--color-paper-hi);
color: #94a3b8; border: var(--rule-hair) solid var(--color-rule);
border-radius: var(--radius-card);
box-shadow: var(--shadow-sheet);
padding: var(--space-xl);
display: flex; display: flex;
justify-content: center; flex-direction: column;
flex-wrap: wrap; gap: var(--space-lg);
gap: 16px;
border-top: 1px solid #f1f5f9;
padding-top: 18px;
} }
.meta span { .guide-bar {
display: inline-flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: var(--space-sm);
} }
.meta code { .guide-line {
background: #f1f5f9; flex: 1;
padding: 2px 6px; height: var(--rule-hair);
border-radius: 6px; background: var(--color-accent);
font-size: 0.78rem;
color: #475569;
} }
@media (max-width: 600px) { .guide-readout {
.card { font-size: var(--text-xs);
padding: 30px 20px; font-weight: 500;
color: var(--color-muted);
white-space: nowrap;
} }
h1 {
font-size: 1.75rem; .joke-specimen {
margin: 0;
padding-block: var(--space-md);
border-top: var(--rule-hair) solid var(--color-accent);
border-bottom: var(--rule-hair) solid var(--color-accent);
} }
.joke-text { .joke-text {
font-size: 1.1rem; font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: var(--display-weight);
line-height: 1.4;
letter-spacing: var(--tracking-display);
color: var(--color-ink);
white-space: pre-wrap;
overflow-wrap: break-word;
min-height: var(--space-3xl);
display: flex;
align-items: center;
transition: opacity var(--dur-short) var(--ease-out);
}
.sheet[data-loading="true"] .joke-text {
opacity: 0.45;
}
.proof-progress {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.proof-progress__meta {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-sm);
font-size: var(--text-xs);
font-weight: 500;
color: var(--color-muted);
}
.proof-progress__track {
position: relative;
height: var(--space-2xs);
background: var(--color-paper-3);
overflow: hidden;
}
.proof-progress__fill {
width: 100%;
height: 100%;
background: var(--color-accent);
transform-origin: left center;
transform: scaleX(var(--progress, 0));
transition: transform var(--dur-short) var(--ease-out);
}
.section--notes {
padding-block: var(--section-gap-s);
}
.section__head {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--gap-columns);
align-items: end;
margin-bottom: var(--space-2xl);
}
.section__head p {
color: var(--color-ink-2);
}
.set {
list-style: none;
margin: 0;
padding: 0;
border-top: var(--rule-hair) solid var(--color-ink);
counter-reset: rule-counter;
}
.set__row {
counter-increment: rule-counter;
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--gap-columns);
align-items: baseline;
padding-block: var(--space-lg);
border-bottom: var(--rule-hair) solid var(--color-rule);
}
.set__row h3::before {
content: "0" counter(rule-counter) " ";
font-family: var(--font-body);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-muted);
margin-right: var(--space-sm);
font-variant-numeric: tabular-nums;
}
.set__row p {
color: var(--color-ink-2);
}
.site-footer {
background: var(--color-paper-2);
color: var(--color-ink);
border-top: var(--rule-hair) solid var(--color-ink);
padding-block: var(--space-2xl);
}
.footer__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-lg);
margin-bottom: var(--space-xl);
flex-wrap: wrap;
}
.footer__head p {
color: var(--color-ink-2);
font-size: var(--text-sm);
}
.colophon-grid {
margin: 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--gap-columns);
border-top: var(--rule-hair) solid var(--color-rule);
padding-top: var(--space-lg);
}
.colophon-item {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
min-width: 0;
}
.colophon-item dt {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink-2);
}
.colophon-item dd {
margin: 0;
font-size: var(--text-md);
color: var(--color-ink);
overflow-wrap: break-word;
}
@media (max-width: 60rem) {
.hero__grid,
.section__head,
.set__row {
grid-template-columns: minmax(0, 1fr);
}
.colophon-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 40rem) {
.colophon-grid {
grid-template-columns: minmax(0, 1fr);
}
.sheet {
padding: var(--space-lg);
}
}
@media (prefers-reduced-motion: reduce) {
.btn {
transition: background-color var(--dur-short) linear;
}
.joke-text,
.proof-progress__fill {
transition: none;
} }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <a class="skip" href="#main">Skip to content</a>
<main class="card">
<div class="badge"> <header class="site-header proof">
<span class="badge-dot"></span> <div class="container site-header__inner">
<span>Ollama • Gemma 4</span> <a href="#main" class="wordmark" aria-label="Programming Dad Joke home">Proof Sheet</a>
<span class="badge-divider">•</span> <nav class="site-nav" aria-label="Page sections">
<span>v{{ version }}</span> <a href="#specimen">Specimen</a>
<a href="#notes">Bench notes</a>
<a href="#colophon">Colophon</a>
<span class="num">v{{ version }}</span>
</nav>
</div> </div>
</header>
<main id="main">
<section class="hero proof">
<div class="container hero__grid">
<div class="hero__lead">
<h1>Programming Dad Joke</h1> <h1>Programming Dad Joke</h1>
<p class="subtitle">Freshly compiled humor from our AI model</p> <p class="lead">Dry humor for engineers after hours, pulled live from your local model.</p>
<div class="joke-box">
<div id="joke" class="joke-text">{{ joke }}</div>
</div>
<div class="actions"> <div class="actions">
<button id="refreshBtn" class="btn" onclick="fetchNewJoke()"> <button id="refreshBtn" class="btn btn--primary crop" type="button" onclick="fetchNewJoke()">
<span class="spinner" id="spinner"></span> <span class="spinner keep-motion" id="spinner" aria-hidden="true"></span>
<span id="btnText">Tell Me Another Joke! 🚀</span> <span id="btnText">Pull another joke</span>
</button> </button>
</div> </div>
</div>
<div class="meta"> <div id="specimen" class="hero__stage">
<span>Host: <code>{{ host }}</code></span> <article id="jokeSheet" class="sheet crop" aria-live="polite" aria-atomic="true">
<span>Model: <code>{{ model }}</code></span> <div class="guide-bar" aria-hidden="true">
<span>Prompt: <code>"{{ prompt }}"</code></span> <span class="guide-line"></span>
<span>App Version: <code>v{{ version }}</code></span> <span class="guide-readout num">Hedvig Letters Serif, 25pt / 1.4</span>
</div> </div>
<blockquote class="joke-specimen">
<p id="joke" class="joke-text">{{ joke }}</p>
</blockquote>
<div id="proofProgress" class="proof-progress" hidden>
<div class="proof-progress__meta">
<span id="progressStatus">Setting type on the press (30 to 50s typical)…</span>
<span id="progressReadout" class="num">0% (0s)</span>
</div>
<div
id="progressBar"
class="proof-progress__track"
role="progressbar"
aria-label="Joke generation progress"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
>
<div id="progressFill" class="proof-progress__fill"></div>
</div>
</div>
<div class="guide-bar" aria-hidden="true">
<span class="guide-readout num">Proofed on {{ model }}</span>
<span class="guide-line"></span>
<span class="guide-readout num">Release v{{ version }}</span>
</div>
</article>
</div>
</div>
</section>
<section class="section--notes" id="notes">
<div class="container">
<div class="section__head">
<h2>Three rules on the press</h2>
<p class="lead">Every groaner on the sheet clears three checks before it leaves the studio.</p>
</div>
<ol class="set">
<li class="set__row">
<h3>Pun on the literal</h3>
<p>Take a noun every developer says ten times before lunch, such as cache, branch, or deadlock, and read it as plain kitchen English.</p>
</li>
<li class="set__row">
<h3>Keep the setup short</h3>
<p>One question across the counter. If the premise needs a whiteboard diagram, the punchline already missed its train.</p>
</li>
<li class="set__row">
<h3>Aim for the eye roll</h3>
<p>A proper dad joke never chases an applause break. It settles for a quiet sigh from across the table.</p>
</li>
</ol>
</div>
</section>
</main> </main>
<footer class="site-footer" id="colophon">
<div class="container">
<div class="footer__head">
<h2>Press colophon</h2>
<p>Compiled on demand from your local inference runtime.</p>
</div> </div>
<dl class="colophon-grid">
<div class="colophon-item">
<dt>Host</dt>
<dd class="num">{{ host }}</dd>
</div>
<div class="colophon-item">
<dt>Model</dt>
<dd class="num">{{ model }}</dd>
</div>
<div class="colophon-item">
<dt>Prompt</dt>
<dd>“{{ prompt }}”</dd>
</div>
<div class="colophon-item">
<dt>App Version</dt>
<dd class="num">v{{ version }}</dd>
</div>
</dl>
</div>
</footer>
<script> <script>
let progressTimer = null;
function computeExpectedProgress(elapsedSeconds) {
if (elapsedSeconds <= 0) return 0.02;
if (elapsedSeconds <= 30) {
return 0.02 + (elapsedSeconds / 30) * 0.66;
}
if (elapsedSeconds <= 50) {
return 0.68 + ((elapsedSeconds - 30) / 20) * 0.24;
}
return 0.92 + 0.06 * (1 - Math.exp(-(elapsedSeconds - 50) / 20));
}
function setProgressState(ratio, elapsedSeconds, statusText, isComplete) {
const progressBar = document.getElementById("progressBar");
const progressFill = document.getElementById("progressFill");
const progressReadout = document.getElementById("progressReadout");
const progressStatus = document.getElementById("progressStatus");
const clamped = Math.max(0, Math.min(1, ratio));
const pct = Math.round(clamped * 100);
const secs = isComplete
? Math.max(0.1, elapsedSeconds).toFixed(1)
: String(Math.max(0, Math.floor(elapsedSeconds)));
progressFill.style.setProperty("--progress", clamped.toFixed(4));
progressBar.setAttribute("aria-valuenow", String(pct));
progressReadout.textContent = pct + "% (" + secs + "s)";
if (statusText) {
progressStatus.textContent = statusText;
}
}
async function fetchNewJoke() { async function fetchNewJoke() {
const btn = document.getElementById("refreshBtn"); const btn = document.getElementById("refreshBtn");
const btnText = document.getElementById("btnText"); const btnText = document.getElementById("btnText");
const spinner = document.getElementById("spinner"); const spinner = document.getElementById("spinner");
const jokeEl = document.getElementById("joke"); const jokeEl = document.getElementById("joke");
const sheet = document.getElementById("jokeSheet");
const progressWrap = document.getElementById("proofProgress");
const progressFill = document.getElementById("progressFill");
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
btn.disabled = true; btn.disabled = true;
btn.setAttribute("aria-busy", "true");
spinner.style.display = "inline-block"; spinner.style.display = "inline-block";
btnText.textContent = "Compiling humor..."; btnText.textContent = "Pulling fresh proof\u2026";
sheet.setAttribute("data-loading", "true");
progressWrap.hidden = false;
progressFill.style.transition = "none";
setProgressState(0, 0, "Setting type on the press (30 to 50s typical)\u2026", false);
void progressFill.offsetWidth;
progressFill.style.transition = "";
const startTime = performance.now();
setProgressState(0.02, 0, "Setting type on the press (30 to 50s typical)\u2026", false);
progressTimer = setInterval(() => {
const elapsed = (performance.now() - startTime) / 1000;
const ratio = computeExpectedProgress(elapsed);
const status =
elapsed < 30
? "Setting type on the press (30 to 50s typical)\u2026"
: elapsed <= 50
? "Inking the proof sheet (30 to 50s window)\u2026"
: "Finishing slow impression\u2026";
setProgressState(ratio, elapsed, status, false);
}, 100);
let nextJoke = "";
try { try {
const response = await fetch("/api/joke"); const response = await fetch("/api/joke");
if (!response.ok) { if (!response.ok) {
throw new Error("HTTP error " + response.status); throw new Error("HTTP error " + response.status);
} }
const data = await response.json(); const data = await response.json();
jokeEl.textContent = data.joke || "No joke returned."; nextJoke = data.joke || "No joke returned.";
} catch (err) { } catch (err) {
console.error("Failed to load joke:", err); console.error("Failed to load joke:", err);
jokeEl.textContent = "Oops! Could not fetch a new joke right now. Please try again."; nextJoke = "Could not reach the joke service right now. Check the Ollama host and try again.";
} finally { } finally {
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
const totalElapsed = (performance.now() - startTime) / 1000;
setProgressState(1, totalElapsed, "Proof complete (30 to 50s typical)", true);
jokeEl.textContent = nextJoke;
sheet.removeAttribute("data-loading");
btn.disabled = false; btn.disabled = false;
btn.removeAttribute("aria-busy");
spinner.style.display = "none"; spinner.style.display = "none";
btnText.textContent = "Tell Me Another Joke! 🚀"; btnText.textContent = "Pull another joke";
} }
} }
</script> </script>
@ -375,6 +707,14 @@ def index():
version=version version=version
) )
@app.route("/tokens.css")
def tokens_css():
return send_from_directory(os.path.dirname(os.path.abspath(__file__)), "tokens.css", mimetype="text/css")
@app.route("/base.css")
def base_css():
return send_from_directory(os.path.dirname(os.path.abspath(__file__)), "base.css", mimetype="text/css")
@app.route("/api/joke") @app.route("/api/joke")
def api_joke(): def api_joke():
joke = request_joke() joke = request_joke()

25
base.css Normal file
View File

@ -0,0 +1,25 @@
*, *::before, *::after { box-sizing: border-box; }
html, body { overflow-x: clip; }
html { scrollbar-color: var(--color-muted) var(--color-paper); scroll-padding-top: var(--space-3xl); }
body { margin: 0; background: var(--color-paper); color: var(--color-ink); font-family: var(--font-body); font-size: var(--text-md); line-height: 1.55; -webkit-font-smoothing: antialiased; caret-color: var(--color-accent); }
::selection { background: color-mix(in oklab, var(--color-accent) 22%, transparent); color: var(--color-ink); }
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.16em; }
h1, h2, h3 { margin: 0; font-family: var(--font-display); font-weight: var(--display-weight, 700); font-synthesis: none; letter-spacing: var(--tracking-display); line-height: 1.05; overflow-wrap: break-word; min-width: 0; text-wrap: balance; }
h1 { max-width: 20ch; font-size: var(--text-display); } h2 { max-width: 30ch; font-size: var(--text-section); } h3 { max-width: 40ch; font-size: var(--text-title); line-height: 1.2; }
.lead { font-size: var(--text-lead); line-height: 1.45; max-width: 52ch; }
p { margin: 0; text-wrap: pretty; }
p:not([class]) { max-width: 65ch; }
img, video { max-width: 100%; height: auto; display: block; }
svg { display: block; }
[hidden] { display: none !important; }
button, input, select, textarea { font: inherit; }
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); box-sizing: border-box; }
.num, .figure, td.num { font-variant-numeric: tabular-nums; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
[data-slot] { position: relative; overflow: hidden; background: linear-gradient(160deg, var(--color-paper-2, var(--color-paper)), var(--color-paper-3, var(--color-paper))); }
[data-slot] > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; }
[data-slot]:has(> img) > :not(img):not([data-keep]) { display: none !important; }
@media (max-width: 60rem) { :root { --gutter: 24px; --section-gap-s: 80px; --section-gap: 96px; --section-gap-l: 120px; } }
@media (max-width: 40rem) { :root { --gutter: 16px; --section-gap-s: 56px; --section-gap: 72px; --section-gap-l: 96px; --gap-columns: 16px; } }
@media (prefers-reduced-motion: reduce) { *:not(.keep-motion), *:not(.keep-motion)::before, *:not(.keep-motion)::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }

23
tokens.css Normal file
View File

@ -0,0 +1,23 @@
:root {
color-scheme: light;
--space-3xs: 2px; --space-2xs: 4px; --space-xs: 8px; --space-sm: 12px; --space-md: 16px; --space-lg: 24px; --space-xl: 40px; --space-2xl: 64px; --space-3xl: 96px; --space-4xl: 144px;
--text-xs: 0.8125rem; --text-sm: 0.875rem; --text-md: 1rem; --text-lg: 1.25rem; --text-xl: 1.5625rem; --text-2xl: 1.953rem; --text-3xl: 2.441rem;
--text-display-s: clamp(2.25rem, 4vw + 0.5rem, 3.75rem); --text-display: clamp(2.75rem, 5vw + 1rem, 5.25rem);
--text-section: clamp(2rem, 2.5vw + 1rem, 3.5rem); --text-title: clamp(1.25rem, 0.75vw + 0.875rem, 1.625rem); --text-lead: clamp(1.125rem, 0.35vw + 1rem, 1.375rem);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in: cubic-bezier(0.7, 0, 0.84, 0); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--dur-micro: 120ms; --dur-short: 220ms; --dur-long: 420ms;
--color-error: oklch(52% 0.19 27); --color-success: oklch(50% 0.12 150);
--container: 1440px; --gutter: 64px; --gap-columns: 32px; --section-gap-s: 120px; --section-gap: 144px; --section-gap-l: 176px;
--z-base: 1; --z-raised: 10; --z-dropdown: 100; --z-sticky: 200; --z-sticky-nav: 300; --z-modal: 400; --z-toast: 500; --z-tooltip: 600;
--color-paper: oklch(97.2% 0.006 215); --color-paper-2: oklch(94% 0.009 215); --color-paper-3: oklch(90.5% 0.011 215); --color-paper-hi: oklch(99% 0.004 215);
--color-ink: oklch(16% 0.032 250); --color-ink-2: oklch(34% 0.022 245); --color-muted: oklch(49% 0.018 240);
--color-rule: oklch(84% 0.010 225);
--color-accent: oklch(68% 0.172 50); --color-accent-ink: oklch(16% 0.032 250); --color-focus: oklch(52% 0.15 48);
--color-dot: color-mix(in oklab, var(--color-ink) 20%, transparent); --color-mark: color-mix(in oklab, var(--color-ink) 72%, transparent);
--font-display: "Hedvig Letters Serif", ui-serif, Georgia, serif; --font-body: "Geist", ui-sans-serif, system-ui, sans-serif;
--font-outlier: "Sofia Sans Extra Condensed", "Arial Narrow", ui-sans-serif, sans-serif;
--radius-card: 3px; --radius-pill: 3px; --rule-hair: 1px;
--shadow-card: none;
--shadow-sheet: 1px 2px 0 color-mix(in oklab, var(--color-ink) 7%, transparent), 10px 18px 36px -14px color-mix(in oklab, var(--color-ink) 32%, transparent);
--tracking-display: -0.022em; --display-weight: 400;
}

View File

@ -1,2 +1,2 @@
[metadata] [metadata]
version = 0.1 version = 0.2