semantique / static /style.css
Ben Blaker
feat(game): add run stats to the win screen (#71)
d6d25e1 unverified
Raw
History Blame Contribute Delete
22.6 kB
/* Semantique — paper & ink. */
.sq-root {
--paper: #faf8f2;
--ink: #1c1b18;
--ink-soft: #5a564c;
--accent: #b3402e;
/* The board is always paper-light; Gradio's ".prose *" rule sets
color: var(--body-text-color), which flips to white in browser dark
mode, so pin the var to ink here. No backticks in this file: Gradio
evals css_template inside a JS template literal. */
--body-text-color: var(--ink);
position: relative;
width: 100%;
height: 100vh;
height: 100dvh; /* dynamic vh: don't get clipped by mobile browser chrome */
overflow: hidden;
background: var(--paper);
color: var(--ink);
font-family: "Patrick Hand", cursive;
user-select: none;
/* swipes drive the game — stop the browser from scrolling/zooming on them */
touch-action: none;
overscroll-behavior: none;
/* identity filter + transition so the glitch flip (below) eases in smoothly
from light instead of snapping — interpolates to invert(1) hue-rotate(180) */
filter: invert(0) hue-rotate(0deg);
transition: filter 0.85s ease-in-out;
}
/* faint ruled-notebook lines, like the sketch */
.sq-root::before {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
to bottom,
transparent 0 31px,
rgba(110, 140, 170, 0.13) 31px 32px
);
pointer-events: none;
}
.sq-stage {
position: absolute;
left: 0;
right: 0;
/* The board centres in the stage, so its on-screen size tracks the stage
height. To keep every board's grid the SAME size, drop the stage by a
constant baseline (~a two-row checklist) regardless of this board's HUD:
otherwise a board with a short checklist (the bonus board's 3 chips fit one
row) keeps a taller stage and frames its grid bigger than the full-checklist
boards, riding up over the sentence. Keep growing past the baseline as the
checklist wraps even taller on narrow windows, so the sentence never lands
on the board. */
top: max(90px, calc((var(--hud-h, 70px) - 70px) * 1.6));
/* Reserve the bottom band the hint line lives in: the board reframes into the
shorter stage, so tiles never render under "arrow keys / WASD to hop". */
bottom: calc(max(18px, env(safe-area-inset-bottom)) + 34px);
}
.sq-stage canvas {
display: block;
}
/* ---- HUD ---- */
.sq-hud {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 22px;
padding: max(18px, env(safe-area-inset-top)) 28px 0;
pointer-events: none;
z-index: 2;
}
.sq-title {
font-family: "Caveat", cursive;
font-size: 28px;
font-weight: 700;
color: var(--ink-soft);
transform: rotate(-2deg);
}
.sq-targets {
font-family: "Caveat", cursive;
font-size: 28px;
font-weight: 700;
color: var(--ink-soft);
}
.sq-target-list {
display: inline-flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
gap: 6px 8px;
margin-left: 6px;
font-size: 32px;
}
.sq-target-item {
position: relative;
display: inline-block;
padding: 0 8px 2px;
border: 2px solid var(--ink);
border-radius: 255px 25px 225px 25px / 25px 225px 25px 255px;
color: var(--ink);
transform: rotate(var(--tilt, -1.5deg));
}
/* checked: fade the word and strike it through */
.sq-target-item.sq-checked {
color: var(--ink-soft);
border-color: var(--ink-soft);
opacity: 0.65;
}
.sq-target-item.sq-checked::after {
content: "";
position: absolute;
left: 7px;
right: 7px;
top: 52%;
height: 2.5px;
background: var(--ink-soft);
transform: rotate(-4deg);
}
/* hops: a "N/budget" counter */
.sq-context {
display: flex;
align-items: center;
gap: 10px;
}
.sq-context-count {
font-family: "Caveat", cursive;
font-size: 30px;
font-weight: 700;
min-width: 52px;
}
.sq-context-count.sq-full {
color: var(--accent);
}
/* ---- audio toggles (music + sfx), grouped with the hop counter ---- */
.sq-hud-right {
display: flex;
align-items: center;
gap: 16px;
}
.sq-audio {
position: relative; /* anchors the mixer popover */
display: flex;
align-items: center;
gap: 6px;
pointer-events: auto; /* the HUD is pointer-events:none — re-enable hits here */
}
.sq-audio-btn {
display: block;
width: 30px;
height: 30px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
line-height: 0;
}
.sq-audio-btn canvas {
display: block;
width: 30px;
height: 30px;
}
/* mixer popover: two hand-drawn volume sliders, one per channel */
.sq-audio-pop {
position: absolute;
top: calc(100% + 8px);
right: 0;
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 14px;
background: #fffdf7;
border: 2.5px solid var(--ink);
border-radius: 18px 7px 20px 7px / 7px 20px 7px 18px;
box-shadow: 3px 4px 0 rgba(28, 27, 24, 0.12);
z-index: 5;
}
.sq-vol-row {
display: flex;
align-items: center;
gap: 9px;
}
.sq-vol-icon {
flex: none;
width: 24px;
height: 24px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
line-height: 0;
}
.sq-vol-icon canvas {
display: block;
width: 24px;
height: 24px;
}
.sq-slider {
width: 120px; /* must equal SLIDER_W in game.js — the drag math assumes it */
height: 26px;
cursor: pointer;
touch-action: none; /* dragging the nib must not scroll the page */
}
.sq-slider canvas {
display: block;
width: 120px;
height: 26px;
}
/* ---- hint bulb (boards 1 & 3): a line-drawn lightbulb + its nudge bubble ---- */
.sq-help {
position: relative; /* anchors the hint bubble */
display: flex;
align-items: center;
pointer-events: auto; /* the HUD is pointer-events:none — re-enable hits here */
}
.sq-help-btn {
display: block;
width: 40px;
height: 40px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
line-height: 0;
}
.sq-help-btn canvas {
display: block;
width: 40px;
height: 40px;
}
/* the hint modal: a confirm step ("view the hint?") that, on yes, reveals one
nudge. Modelled on the welcome card so it reads as the same hand-drawn note. */
.sq-hintbox {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(250, 248, 242, 0.82);
z-index: 4;
}
.sq-hintbox-card {
position: relative;
width: min(420px, 86vw);
padding: 28px 32px 28px;
background: #fffdf7;
border: 3px solid var(--ink);
border-radius: 235px 25px 245px 20px / 25px 235px 20px 245px;
box-shadow: 4px 5px 0 rgba(28, 27, 24, 0.12);
transform: rotate(-1deg);
text-align: center;
}
.sq-hintbox-q {
font-family: "Caveat", cursive;
font-size: 34px;
font-weight: 700;
line-height: 1.3;
text-wrap: balance;
color: var(--ink);
margin-bottom: 22px;
}
.sq-hintbox-text {
font-family: "Patrick Hand", cursive;
font-size: 24px;
line-height: 1.45;
text-wrap: balance;
color: var(--ink);
margin-bottom: 22px;
}
.sq-hintbox-actions {
display: flex;
justify-content: center;
gap: 16px;
}
.sq-hintbox-yes,
.sq-hintbox-no,
.sq-hintbox-close {
font-family: "Patrick Hand", cursive;
font-size: 21px;
color: var(--ink);
background: transparent;
border: 2.5px solid var(--ink);
border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
padding: 5px 24px 7px;
cursor: pointer;
transform: rotate(-0.6deg);
}
.sq-hintbox-no { transform: rotate(0.8deg); }
.sq-hintbox-yes:hover,
.sq-hintbox-no:hover,
.sq-hintbox-close:hover { background: var(--ink); color: var(--paper); }
/* ---- prompt strip ---- */
.sq-prompt {
position: absolute;
left: 0;
right: 0;
/* Sit a fixed gap below the target checklist. The HUD grows taller as the
checklist wraps on narrower windows, so track its measured height (--hud-h,
published by game.js) instead of a fixed offset — the sentence then clears
the targets at every width, not just the widest. */
top: calc(var(--hud-h, 69px) + 36px);
display: flex;
align-items: center;
justify-content: center; /* sentence grows from the centre */
flex-wrap: wrap;
gap: 8px;
padding: 6px 28px 0;
font-size: 22px;
z-index: 2;
}
.sq-chips {
display: inline-flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
gap: 14px;
}
.sq-chip {
display: inline-block;
font-family: "Caveat", cursive;
font-size: 42px;
font-weight: 700;
line-height: 1;
color: var(--ink);
transform: rotate(var(--tilt, 0deg));
}
/* a context-bomb file dumped into the prompt: raw monospace bytes, clamped to
one line so the "flood" reads without smearing across the screen */
.sq-chip-file {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace !important;
font-size: 16px !important;
font-weight: 400 !important;
line-height: 1.3;
color: var(--accent);
max-width: min(70vw, 620px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.sq-chip-text {
display: inline-block;
/* The write-on (game.js) clips this box left→right. We clip with a plain
inset(0) — Safari clamps negative clip-path insets to 0 and won't repaint a
cleared clip-path, so any negative-inset reveal shaves Caveat's slant /
ascenders / descenders until a reflow. Pad the ink box and cancel the pad
in layout, so inset(0) always wraps every glyph with room to spare. */
padding: 0.25em 0.35em 0.2em 0;
margin: -0.25em -0.35em -0.2em 0;
}
/* ---- hint ---- */
.sq-hint {
position: absolute;
bottom: max(18px, env(safe-area-inset-bottom));
left: 0;
right: 0;
text-align: center;
font-size: 19px;
color: var(--ink-soft);
z-index: 2;
pointer-events: none;
}
/* ---- verdict overlay ---- */
.sq-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(250, 248, 242, 0.82);
z-index: 3;
}
.sq-card {
position: relative;
width: min(480px, 86vw);
padding: 30px 34px 26px;
background: #fffdf7;
border: 3px solid var(--ink);
border-radius: 235px 25px 245px 20px / 25px 235px 20px 245px;
box-shadow: 4px 5px 0 rgba(28, 27, 24, 0.12);
transform: rotate(-1deg);
text-align: center;
}
.sq-card-sentence {
font-family: "Caveat", cursive;
font-size: 34px;
font-weight: 600;
margin-bottom: 18px;
min-height: 40px;
}
/* file-bomb death card: a caption + the overrun bytes in monospace.
margin-top drops the caption clear of the "context window exceeded"
stamp, which dips into the top-right corner. */
.sq-card-dumpcap {
font-family: "Caveat", cursive;
font-weight: 600;
font-size: 26px;
margin-top: 20px;
margin-bottom: 8px;
}
.sq-card-dump {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 13px;
line-height: 1.35;
color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 2px dashed var(--ink-soft);
border-radius: 8px;
padding: 6px 8px;
margin-bottom: 4px;
}
/* file-specific aside under the dump — the game's reaction to the file */
.sq-card-tagline {
font-family: "Caveat", cursive;
font-size: 22px;
color: var(--ink-soft);
margin-top: 8px;
}
.sq-bars {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 8px;
}
.sq-bar-row {
display: grid;
grid-template-columns: 76px 1fr 52px;
gap: 10px;
align-items: center;
font-size: 20px;
}
.sq-bar-label { text-align: right; }
.sq-bar-track {
height: 18px;
border: 2px solid var(--ink);
border-radius: 155px 15px 145px 15px / 15px 145px 15px 155px;
overflow: hidden;
background: rgba(255, 255, 255, 0.7);
}
.sq-bar-fill {
height: 100%;
width: 0%;
background: repeating-linear-gradient(
-55deg,
var(--ink-soft) 0 2px,
transparent 2px 6px
);
}
.sq-bar-row.sq-bar-target .sq-bar-fill {
background: repeating-linear-gradient(
-55deg,
var(--ink) 0 2.5px,
transparent 2.5px 5.5px
);
}
.sq-bar-pct { text-align: left; color: var(--ink-soft); }
.sq-stamp {
position: absolute;
top: -22px;
right: -18px;
font-family: "Caveat", cursive;
font-size: 40px;
font-weight: 700;
padding: 0 16px;
border: 4px solid var(--accent);
border-radius: 12px;
color: var(--accent);
background: rgba(255, 253, 247, 0.9);
transform: rotate(8deg);
opacity: 0;
}
.sq-stamp.sq-win { --accent: #2c6e3f; border-color: #2c6e3f; color: #2c6e3f; }
.sq-card-actions { margin-top: 14px; }
.sq-card-actions button {
font-family: "Patrick Hand", cursive;
font-size: 21px;
color: var(--ink);
background: transparent;
border: 2.5px solid var(--ink);
border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
padding: 4px 20px 6px;
cursor: pointer;
transform: rotate(0.5deg);
}
.sq-card-actions button:hover {
background: var(--ink);
color: var(--paper);
}
/* ---- welcome modal ---- */
/* Same paper-and-ink card as the verdict overlay, shown once on game open. */
.sq-welcome {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(250, 248, 242, 0.82);
z-index: 4; /* above the verdict overlay (3), though they never coexist */
}
.sq-welcome-card {
position: relative;
width: min(440px, 86vw);
padding: 30px 34px 30px;
background: #fffdf7;
border: 3px solid var(--ink);
border-radius: 235px 25px 245px 20px / 25px 235px 20px 245px;
box-shadow: 4px 5px 0 rgba(28, 27, 24, 0.12);
transform: rotate(-1deg);
text-align: center;
}
.sq-welcome-title {
font-family: "Caveat", cursive;
font-size: 38px;
font-weight: 700;
line-height: 1.3; /* room for the title to wrap onto two lines */
text-wrap: balance; /* even out the two wrapped lines */
color: var(--ink);
margin-bottom: 16px;
}
/* "targets" boxed like the real HUD target chips (.sq-target-item) — kept
light (thin rim, slight tilt) so it echoes the chips instead of shouting */
.sq-welcome-target {
display: inline-block;
padding: 0 8px 1px;
border: 2px solid var(--ink);
border-radius: 255px 25px 225px 25px / 25px 225px 25px 255px;
transform: rotate(-1deg);
}
.sq-welcome-body {
font-family: "Patrick Hand", cursive;
font-size: 21px;
line-height: 1.5;
color: var(--ink-soft);
margin-bottom: 22px;
}
/* the close button: a small carriage-return keycap drawn like the submit tile —
soft ink, a dashed "press me" rim, and the ⏎ glyph centred. */
.sq-welcome-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 66px;
height: 66px;
padding: 0 0 4px;
font-family: "Patrick Hand", cursive;
font-size: 42px;
line-height: 1;
color: var(--ink-soft);
background: #faf8f2;
border: 3px dashed var(--ink-soft);
border-radius: 18px 7px 20px 7px / 7px 20px 7px 18px;
box-shadow: 3px 4px 0 rgba(28, 27, 24, 0.12);
cursor: pointer;
transform: rotate(-1.5deg);
}
.sq-welcome-close:hover {
color: var(--ink);
border-color: var(--ink);
}
.sq-welcome-close:active {
transform: rotate(-1.5deg) translate(3px, 4px); /* press the keycap down */
box-shadow: 0 0 0 rgba(28, 27, 24, 0.12);
}
/* ---- you-win modal ---- */
/* Shown over the bonus board once build + small + hackathon are all collected.
It inherits the glitch invert (the board is inverted when this fires). */
.sq-victory {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(250, 248, 242, 0.82);
z-index: 6; /* above the verdict (3) and welcome (4) */
}
.sq-victory-card {
position: relative;
width: min(460px, 86vw);
padding: 34px 34px 30px;
background: #fffdf7;
border: 3px solid var(--ink);
border-radius: 245px 18px 235px 22px / 20px 240px 18px 245px;
box-shadow: 5px 6px 0 rgba(28, 27, 24, 0.14);
transform: rotate(-1.2deg);
text-align: center;
}
.sq-victory-title {
font-family: "Caveat", cursive;
font-size: 52px;
font-weight: 700;
line-height: 1.05;
margin-bottom: 14px;
/* an animated rainbow sweeping through the letters (the @keyframes lives in
app.py's un-scoped BLOCKS_CSS). Stays a rainbow under the glitch invert. */
background: linear-gradient(
90deg, #ff004d, #ff8a00, #ffe000, #36d800, #00b3ff, #8b5cf6, #ff004d
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: sq-rainbow 3s linear infinite;
}
.sq-victory-body {
font-family: "Patrick Hand", cursive;
font-size: 21px;
line-height: 1.5;
color: var(--ink-soft);
margin-bottom: 18px;
}
/* The run tally — a tidy ledger of the session, ruled like a notebook line. */
.sq-victory-stats {
width: min(300px, 100%);
margin: 0 auto 22px;
text-align: left;
font-family: "Patrick Hand", cursive;
}
.sq-stat {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 5px 2px;
border-bottom: 2px dotted rgba(28, 27, 24, 0.18);
}
.sq-stat:last-child { border-bottom: 0; }
.sq-stat dt {
margin: 0;
font-size: 19px;
color: var(--ink-soft);
}
.sq-stat dd {
margin: 0;
font-size: 22px;
color: var(--ink);
}
.sq-victory-close {
font-family: "Patrick Hand", cursive;
font-size: 21px;
color: var(--ink);
background: transparent;
border: 2.5px solid var(--ink);
border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
padding: 5px 22px 7px;
cursor: pointer;
transform: rotate(0.5deg);
}
.sq-victory-close:hover { background: var(--ink); color: var(--paper); }
/* The "delete all data" 404. Sits above every other modal; like the victory card
it inherits the glitch invert (it only ever fires on the bonus board), reading
as a dark system-error dialog over the negated screen. */
.sq-errorbox {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(250, 248, 242, 0.82);
z-index: 7; /* top of the stack — above the win modal (6) */
}
.sq-errorbox-card {
position: relative;
width: min(440px, 86vw);
padding: 30px 30px 26px;
background: #fffdf7;
border: 3px solid var(--ink);
border-radius: 235px 20px 245px 18px / 18px 245px 20px 235px;
box-shadow: 5px 6px 0 rgba(28, 27, 24, 0.14);
transform: rotate(-1deg);
text-align: center;
}
.sq-errorbox-title {
font-family: "Caveat", cursive;
font-size: 42px;
font-weight: 700;
line-height: 1.05;
color: var(--ink);
margin-bottom: 12px;
}
.sq-errorbox-body {
font-family: "Patrick Hand", cursive;
font-size: 21px;
line-height: 1.45;
color: var(--ink-soft);
margin-bottom: 22px;
}
.sq-errorbox-ok {
font-family: "Patrick Hand", cursive;
font-size: 21px;
color: var(--ink);
background: transparent;
border: 2.5px solid var(--ink);
border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
padding: 5px 30px 7px;
cursor: pointer;
transform: rotate(0.6deg);
}
.sq-errorbox-ok:hover { background: var(--ink); color: var(--paper); }
.sq-hidden { display: none !important; }
/* ---- glitch / bonus mode ---- */
/* The whole board photo-negates: a single filter on the root flips paper to
black and every inked line to white (the WebGL canvas inverts along with the
DOM), while the hue-rotate keeps the red accent roughly red instead of cyan.
One declaration, and tiles/doodle/cards/HUD all come along for free. The
@keyframes + the page-chrome darkening live in app.py's (un-scoped) BLOCKS_CSS,
because this css_template is injected NESTED under the component id — global
selectors and at-rules don't survive here, but .sq-root-anchored ones do. */
.sq-root.sq-glitch {
filter: invert(1) hue-rotate(180deg); /* eased by the transition on .sq-root */
}
/* faint moving scanlines over the negative, for that decoded-signal hum */
.sq-root.sq-glitch::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0 2px,
rgba(0, 0, 0, 0.05) 2px 3px
);
animation: sq-scanline 7s linear infinite;
}
/* ---- compact / mobile ---- */
/* Phones (and short landscape viewports) only. The tablet/desktop single-row HUD
holds well down to phone widths, so only here does the HUD WRAP: the title and
the hop counter / audio cluster keep the first row (title left, counter right
via space-between), and the checklist drops to its own full-width second row
(order:3, flex-basis:100%) in a smaller font, where it wraps over fewer, wider
lines. The wrapped HUD is a variable height, so game.js publishes it as --hud-h
and the prompt strip + board drop below it (instead of colliding). */
@media (max-width: 560px), (max-height: 540px) {
.sq-hud { padding-left: 12px; padding-right: 12px; gap: 8px 10px; flex-wrap: wrap; }
.sq-title { font-size: 18px; order: 1; }
.sq-targets { order: 3; flex: 1 1 100%; min-width: 0; }
.sq-target-list { display: flex; width: 100%; gap: 5px 6px; margin-left: 0; font-size: 16px; }
.sq-target-item { padding: 0 6px 1px; }
.sq-context { gap: 4px; }
.sq-context-count { font-size: 18px; min-width: 28px; }
.sq-hud-right { order: 2; gap: 10px; }
.sq-audio { gap: 4px; }
.sq-audio-btn, .sq-audio-btn canvas { width: 24px; height: 24px; }
.sq-help-btn, .sq-help-btn canvas { width: 32px; height: 32px; }
/* drop the prompt + board below the variable-height wrapped checklist */
.sq-prompt { top: var(--hud-h, 52px); padding-left: 12px; padding-right: 12px; gap: 6px; }
.sq-stage { top: calc(var(--hud-h, 52px) + 38px); }
.sq-chips { gap: 9px; }
.sq-chip { font-size: 26px; }
.sq-hint { font-size: 16px; }
.sq-card { padding: 24px 22px 22px; }
.sq-card-sentence { font-size: 27px; }
.sq-bar-row { grid-template-columns: 60px 1fr 44px; font-size: 17px; }
.sq-stamp { font-size: 32px; top: -16px; right: -4px; } /* keep the sticker on-screen */
.sq-hintbox-card { width: min(380px, 88vw); padding: 22px 22px 24px; }
.sq-hintbox-q { font-size: 29px; margin-bottom: 18px; }
.sq-hintbox-text { font-size: 21px; margin-bottom: 18px; }
.sq-welcome-card { width: min(400px, 88vw); padding: 24px 22px 26px; }
.sq-welcome-title { font-size: 32px; }
.sq-welcome-body { font-size: 18px; margin-bottom: 20px; }
.sq-welcome-close { width: 56px; height: 56px; font-size: 34px; }
.sq-errorbox-card { width: min(380px, 88vw); padding: 24px 22px 22px; }
.sq-errorbox-title { font-size: 36px; margin-bottom: 10px; }
.sq-errorbox-body { font-size: 18px; margin-bottom: 18px; }
.sq-victory-stats { width: min(280px, 100%); margin-bottom: 18px; }
.sq-stat dt { font-size: 17px; }
.sq-stat dd { font-size: 20px; }
}