diff --git a/blocks/roster_grid.ninjatpl b/blocks/roster_grid.ninjatpl index c2f279d..b0dd3b9 100644 --- a/blocks/roster_grid.ninjatpl +++ b/blocks/roster_grid.ninjatpl @@ -7,7 +7,7 @@
{{ member.handle }}
{% endif %} diff --git a/manifest.yaml b/manifest.yaml index 4fa5471..98289e3 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -293,15 +293,17 @@ css: pointer-events: none; z-index: 1; } + /* Inset 5px so the L-brackets clear any rounded / overflow-hidden panel + corner (e.g. the code_neon figure) instead of being sliced by it. */ .hud-frame::before { - top: 0; - left: 0; + top: 5px; + left: 5px; border-top: 2px solid hsl(var(--primary)); border-left: 2px solid hsl(var(--primary)); } .hud-frame::after { - bottom: 0; - right: 0; + bottom: 5px; + right: 5px; border-bottom: 2px solid hsl(var(--accent)); border-right: 2px solid hsl(var(--accent)); } diff --git a/seed/seed.json b/seed/seed.json index 426b51b..5676afd 100644 --- a/seed/seed.json +++ b/seed/seed.json @@ -114,10 +114,10 @@ "template_key": "default", "blocks": [ { "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The studio", "level": 1 } }, - { "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "Neon Forge started as a build tool for a game that shipped late. The tool outlived the game. We kept building it, then built more games to prove it out.
Now we run both. The engine and live service on one side, the games that stress test them on the other. Everything we make, we run in production first.
" } }, + { "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "Neon Forge started as a build tool for a game that shipped two years late. The game came and went. The tool outlived it. We kept sharpening it in the dark between projects, then built more games to prove it could take the punishment.
Now we run both halves at once. The engine and the live service on one side, the games that stress-test them on the other. Nothing ships to a player before it has shipped to us first, on the same pipeline, under the same load.
We are a small crew by design. Fourteen people who would rather own the whole stack than manage a hundred who own slices of it. Everyone here writes code that runs in production, and everyone here has been paged at 3am by something they wrote. That keeps us honest.
The look of this place is not an accident either. Neon on black, mono readouts, a little glitch in the type. It is how our tools have always looked on the wall of screens in the build room, and we never saw a reason to dress it up for the website.
" } }, { "block_key": "cyberpunk:code_neon", "title": "Quickstart", "slot": "main", "sort_order": 3, "content": { "language": "bash", "code": "npm install @neon-forge/sdk\nnpx neon-forge init my-game\nnpx neon-forge ship --stage prod", "copy": "on" } }, { "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 4, "content": { "variant": "line", "weight": "medium" } }, - { "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "content": { "style": "pull", "quote": "Build the tool. Ship the game. Repeat.", "attribution": "The studio" } } + { "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "content": { "style": "pull", "quote": "Build the tool. Ship the game. Run both in production before you trust either.", "attribution": "Nova Reyes", "role": "Engine lead, Neon Forge" } } ] }, { @@ -137,7 +137,7 @@ "parent_slug": "/blog", "blocks": [ { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Making it rain", "level": 1 } }, - { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "The hero on our home page runs a digital-rain canvas. No libraries, no images, just a small script that draws falling glyphs.
The trick is to fade the last frame instead of clearing it. That leaves a trail. Cap the frame rate and it stays cheap, even on a laptop.
" } } + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "The hero on our home page runs a digital-rain canvas. No libraries, no sprite sheets, no video. Just a few dozen lines that draw falling glyphs onto a single <canvas> and never touch the network again.
We wanted the front door of the studio to feel like the wall of screens in our build room: green-on-black readouts, columns of characters sliding down, the faint sense that something is compiling behind the glass. A GIF would have looked dead. A video would have cost a megabyte and stuttered on the first scroll. So we drew it live.
The whole effect is one idea repeated: divide the width into columns, give each column a drop that falls one row per tick, and paint a random glyph wherever that drop currently sits. When a drop runs off the bottom, roll a die and occasionally send it back to the top. That randomness is what stops the rain from marching in lockstep.
The trick that makes it read as rain instead of confetti is the fade. Every frame we do not clear the canvas. We paint a translucent rectangle of the background colour over the whole thing, just barely opaque, then draw the new glyphs on top. Yesterday's characters do not vanish; they dim, one step per frame, leaving a comet tail behind each drop. Clear the canvas outright and you get sharp dots with no soul. Fade it and you get rain.
Colour comes from the theme, not from a hex code baked into the script. On boot we read the CSS custom properties off the document root, the primary and accent channels, and pick one of them at random per glyph. Retheme the site and the rain retints itself for free. That single decision is why the effect survives every palette we throw at it.
Performance was the part we actually sweated. A canvas that repaints as fast as the browser allows will happily cook a laptop fan for a background nobody is looking at. So we cap the loop at roughly eighteen frames a second. Rain does not need sixty. The requestAnimationFrame callback fires as often as the display wants, but we bail early unless enough milliseconds have passed since the last real paint. Half the frames do nothing but check a clock.
We also refused to let it run when it should not. The canvas only wakes up once it scrolls into view, wired through an IntersectionObserver, so a page that never shows the hero never spends a cycle on it. If the visitor has asked for reduced motion, we skip the animation entirely and let a static neon gradient stand in. The rain is a flourish, never a requirement.
Sizing is handled the boring, correct way. A ResizeObserver watches the hero and rebuilds the column list whenever the box changes, multiplying the backing store by the device pixel ratio so the glyphs stay crisp on retina panels without rendering four times the pixels on a phone. Resize the window and the rain reflows mid-fall.
None of this is clever, and that is the point. The failure mode of a signature animation is a dependency you have to keep patched and a bundle that grows every time someone adds a feature. This one has no dependencies, ships as inline script inside the theme, and has not changed since the day it worked. It will still run when the framework it was born next to is three major versions gone.
If you are building your own, start with the fade and the frame cap. Everything else is decoration. Get a single column falling with a trailing tail at a sane frame rate, and you already have the effect. The rest is just more columns.
" } } ] }, { @@ -147,7 +147,7 @@ "parent_slug": "/blog", "blocks": [ { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Caching shaders across branches", "level": 1 } }, - { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "Every branch used to recompile the whole shader set. That is minutes per build, times every push.
We keyed the cache on the shader source, not the branch. Two branches that share a shader now share the compile. Builds got short again.
" } } + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "Every branch used to recompile the whole shader set from scratch. That is minutes per build, times every push, times every engineer. On a busy day the build farm spent more time on shaders nobody had touched than on the code that changed.
The insight was embarrassingly simple once we saw it. A shader does not know what branch it lives on. If the source bytes are identical, the compiled output is identical. So we keyed the cache on a hash of the shader source and its compile flags, not on the branch name.
Now two branches that share a shader share the compile. The first branch to build a given shader pays for it; everyone after pulls the artifact from the cache in milliseconds. A feature branch that only touches gameplay code inherits every shader the mainline already compiled.
Green builds went from tens of minutes back down to single digits. The farm stopped melting under merge storms. Nobody has thought about shader compile times since, which is the highest compliment we can pay a piece of infrastructure.
" } } ] }, { @@ -157,7 +157,7 @@ "parent_slug": "/blog", "blocks": [ { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Netcode on bad links", "level": 1 } }, - { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "Players on slow connections kept desyncing. Rewriting the simulation was off the table.
We added client prediction with a short rollback window. The client guesses, the server corrects, and the gap closes before anyone notices.
" } } + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "Players on slow connections kept desyncing, and the complaints all rhymed: the world would freeze for a beat, then snap somewhere they did not expect. Rewriting the simulation to be network-native was a six-month job we did not have. We needed a fix that lived on top of the sim we already trusted.
So we added client prediction with a short rollback window. The client stops waiting for the server to bless every input and starts guessing. Press dash, and you dash now, locally, immediately. The input also goes to the server, which is still the only authority that counts.
When the server's version of events comes back, the client compares it against what it predicted. Most of the time they agree and nothing happens. When they disagree, the client quietly rewinds the last few frames, replays them with the corrected state, and rolls forward again, all inside a window short enough that the eye never catches the seam.
The result is a game that feels instant on a bad link and stays authoritative on the server. Desync on two-hundred-millisecond connections dropped to something players stopped reporting. The simulation never changed. We just stopped making it wait.
" } } ] }, { diff --git a/templates/overrides/cyberpunk/announcement-bar.ninjatpl b/templates/overrides/cyberpunk/announcement-bar.ninjatpl index 3b4111e..25e673b 100644 --- a/templates/overrides/cyberpunk/announcement-bar.ninjatpl +++ b/templates/overrides/cyberpunk/announcement-bar.ninjatpl @@ -4,7 +4,7 @@{{ message }}{% if linkText and linkUrl %} {{ linkText }}{% endif %}
-{% if dismissable %}{% endif %} +{% if dismissable %}{% endif %}{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}
{{ text }}
{% endif %}{{ event.venue }}
{% endif %} {% if event.description %}{{ event.description }}
{% endif %} - {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %} + {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %}{{ item.text }}
{% endif %} - {% if item.linkUrl and item.linkLabel %}{{ item.linkLabel }}{% endif %} + {% if item.linkUrl and item.linkLabel %}{{ item.linkLabel }}{% endif %}{{ emptyMessage|default:"That route returned nothing. The page moved, shipped, or never existed." }}
{% if context.path %}requested: {{ context.path }}
{% endif %} diff --git a/templates/overrides/cyberpunk/pricing.ninjatpl b/templates/overrides/cyberpunk/pricing.ninjatpl index c722830..96f2466 100644 --- a/templates/overrides/cyberpunk/pricing.ninjatpl +++ b/templates/overrides/cyberpunk/pricing.ninjatpl @@ -15,7 +15,7 @@ {% if tier.description %}{{ tier.description }}
{% endif %} {% if tier.features %}{{ eyebrow }}
{% endif %} {% if heading %}{{ member.role }}
{% endif %} {% if member.bio %}{{ member.bio }}
{% endif %} {% if member.socials %}{% endif %}{% if q.quote %}“{{ q.quote }}”{% endif %}
{{ step.label }}
{% endif %}