/* 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; } }