Files
website/src/pages/index.astro
T

813 lines
27 KiB
Plaintext
Raw Normal View History

---
const title = 'Blakeslee Research — research that runs';
const description =
'Blakeslee Research engineers infrastructure that keeps AI agents—and the digital intelligence to come—forever online and forever active, including Nucleic, the home for persistent, orchestrated, observable coding agents.';
const canonical = new URL('/', Astro.site);
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Deliberately no viewport-fit=cover. It looks like the fix for the bands behind
Safari's chrome and it is not: measured on iOS 27, screen.height is 874 while
100lvh is 754 and both safe-area insets report 0, cover or not. Safari does not
hand those 120px to the page, so no element and no viewport unit reaches them —
see the note on .ambient for what actually resolves it. -->
<meta name="viewport" content="width=device-width" />
<!-- --paper, and it has to stay --paper: this is one of the surfaces Safari fills
around the page, alongside the canvas, and the field is now brought to --paper
at the viewport edges so all of them agree. A "better matching" value here is
precisely the bug — it reintroduces a colour that differs from the page edge,
which surfaces as a bar whenever Safari switches to it during a scroll. -->
<meta name="theme-color" content="#0a100c" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<link rel="icon" href="/blakeslee-icon.svg" type="image/svg+xml" sizes="any" />
<link
rel="preload"
href="/fonts/archivo.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/jetbrains-mono.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Blakeslee Research" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={new URL('/og.png', Astro.site)} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Blakeslee Research wordmark and wolf-and-pine mark" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL('/og.png', Astro.site)} />
</head>
<body>
<div class="ambient" aria-hidden="true">
<span class="ambient-haze"></span>
<span class="ambient-drift"></span>
<span class="ambient-grain"></span>
</div>
<div class="page-shell">
<header class="masthead" aria-label="Blakeslee Research">
<div class="brand-lockup">
<img
class="mark"
src="/blakeslee-icon.svg"
width="378"
height="392"
alt="Blakeslee Research wolf and pine mark"
/>
<span class="brand-name">Blakeslee<br />Research</span>
</div>
</header>
<main>
<section class="hero" aria-labelledby="hero-heading">
<!-- Headline and card share one wrapper so they sit on a single flex line
that can break. Grid alone cannot: a headline too wide for its columns
keeps its area and simply prints over whatever occupies the next one. -->
<div class="hero-lead">
<h1 id="hero-heading" class="statement" aria-label="Research that runs.">
<span class="line line-research"><span class="word">Research</span></span>
<span class="line line-runs"><span class="word"><span class="that">that</span> <span class="runs">runs.</span></span></span>
</h1>
<a class="product-card" href="https://nucleic.blakeslee.xyz">
<span class="card-index">01 / Flagship system</span>
<span class="product-name">Nucleic <span aria-hidden="true">↗</span></span>
<span class="product-description">
Where agents live — persisted, orchestrated, observed.
</span>
<span class="card-rule" aria-hidden="true"></span>
</a>
</div>
<p class="mission">
We engineer infrastructure that keeps intelligence forever online and forever
active — immortal by design.
</p>
</section>
</main>
<footer class="footer">
<p>© 2026 Blakeslee Research</p>
<div class="footer-links">
<a href="mailto:[email protected]">Contact</a>
<a href="https://version.blakeslee.xyz" target="_blank" rel="noopener noreferrer">Code <span aria-hidden="true">↗</span></a>
</div>
</footer>
</div>
</body>
</html>
<style is:global>
@font-face {
font-family: 'Archivo';
src: url('/fonts/archivo.woff2') format('woff2');
font-style: normal;
font-weight: 100 900;
font-stretch: 62% 125%;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/jetbrains-mono.woff2') format('woff2');
font-style: normal;
font-weight: 400 700;
font-display: swap;
}
:root {
color-scheme: dark;
--ink: #eef3ee;
--secondary: #aebdae;
--muted: #93a494;
--pine: #3fae74;
--sage: #c6d8c6;
--forest: #144b35;
--paper: #0a100c;
--line: rgb(238 243 238 / 16%);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--paper);
}
body {
margin: 0;
min-width: 320px;
min-height: 100svh;
overflow-x: hidden;
color: var(--ink);
/* No background here: html paints the canvas, so the fixed ambient layer below
(negative z-index) is not covered by body's own background box. */
font-family: 'Archivo', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Ambient field: two soft bands of light crossing at different angles, and a fine
grain that dithers away the banding flat gradients show on near-black. */
.ambient {
position: fixed;
z-index: -2;
top: 0;
right: 0;
left: 0;
/* Deliberately not inset: 0. A fixed box pinned to the bottom stops at the small
viewport, leaving the strip between svh and lvh unpainted by this layer — which
matters because the toolbar retracts and hands those pixels back to the page.
Sized to the large viewport, the layer owns them in both states; what it puts
there is the ::after note's business. */
height: 100%;
height: 100lvh;
overflow: hidden;
pointer-events: none;
}
/* The grey bar, properly.
iOS reserves a strip above and below the layout viewport for its own chrome, and
fills it from the document canvas — which takes its paint from the root element
and nowhere else. Measured on iOS 27: screen.height 874, 100lvh 754, both safe-area
insets 0. Those 120px are not addressable by the page. No height, no viewport unit
and no viewport-fit=cover reaches them; earlier attempts only moved the seam.
So the canvas is the single point, and there is exactly one thing to get right:
every surface Safari fills around the page, and the page's own colour where it
meets them, must be the same paint. Those surfaces can only be flat colours, and
the field is a moving light whose edge tone drifts, so matching them by picking a
constant is a value that is wrong again as soon as the bands move — and wrong in a
different way in each scroll state, which is what made the bar come and go.
The field is therefore brought to zero at both edges of the viewport. --paper is
then the only colour in play: canvas, theme-color, overscroll, the chrome strips,
and the page itself where it touches any of them. There is no second value left
for Safari to swap in, so no scroll state, toolbar collapse or fixed-layer
reposition has anything to expose. That is the property to preserve — if this ever
needs editing, keep the edges at --paper rather than tuning a colour to match.
The one thing that has to be right is where the bottom reaches --paper, and it is
not a number to taste. This layer is fixed and 100lvh tall, but the bottom edge of
what the reader can see is only at 100lvh with the toolbar collapsed; expanded, it
is at 100svh, ~100px higher, and Safari slides between the two mid-scroll. Ending
the fade at the element's own bottom therefore left the visible edge sitting part
way up the fade whenever the toolbar was out — lighter than the strip below it, so
the bar came back — and clean again once the toolbar went away. A boundary at a
coordinate that moves is not a fix, it is the same bug relocated.
Nor can that band simply be held flat: those pixels are behind the chrome in one
state and are ordinary page in the other, so a flat patch there is invisible with
the toolbar out and a hard-edged band the moment it retracts. Both failures are
the same failure — an edge existing anywhere the viewport boundary can land.
So there is no edge. The field decays to --paper continuously, and is already
exactly --paper by 100svh, above the highest position the visible bottom can take;
everything from there to 100lvh is the same paint. Every boundary the viewport
could land on is gone, in either state and part way through the transition between
them. The decay is spread over ~14rem because the whole excursion is about five
steps of RGB — under a step per 40px, below anything the eye reads as an edge, and
the grain dithers the remainder. Keep that shape if this is ever edited: reach
--paper at or before 100svh, and get there gradually. Top edge likewise, though it
is simpler — the layout viewport's top does not move, so it only needs to arrive.
Static, so the layer still composites without repainting per frame. */
.ambient::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
var(--paper) 0,
rgb(10 16 12 / 0%) 8rem,
rgb(10 16 12 / 0%) calc(100svh - 14rem),
var(--paper) 100svh,
var(--paper) 100%
);
}
/* Oversized so the bands can travel a long way without an edge ever entering the
frame — what should be visible is the light moving, never the thing carrying it. */
.ambient-haze,
.ambient-drift {
position: absolute;
top: -60%;
left: -60%;
width: 220%;
height: 220%;
will-change: transform;
}
.ambient-haze {
background: linear-gradient(
106deg,
transparent 31%,
rgb(63 174 116 / 6%) 45%,
rgb(96 186 137 / 2.5%) 53%,
transparent 63%
);
transform: translate3d(-9%, 3%, 0);
animation: haze-drift 46s -7s ease-in-out infinite alternate;
}
/* The second band is wider, crosses the first at a shallow angle, and runs on a
period that shares no factor with it. Where the two overlap the field lifts a
little; because 46s and 67s only realign every 51 minutes, that bright seam
wanders instead of pulsing on a beat you can count. */
.ambient-drift {
background: linear-gradient(
-24deg,
transparent 20%,
rgb(120 196 152 / 3%) 43%,
rgb(63 174 116 / 4.5%) 59%,
transparent 79%
);
transform: translate3d(7%, -5%, 0);
animation: drift-across 67s -21s ease-in-out infinite alternate;
}
.ambient-grain {
position: absolute;
inset: -50%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
opacity: 0.022;
}
a {
color: inherit;
}
a:focus-visible {
outline: 2px solid var(--sage);
outline-offset: 5px;
}
.page-shell {
/* One inset for everything. It is deliberately generous: at mid widths a tighter
margin puts the copy and the window edge in each other's way. */
--edge: clamp(1.35rem, 4.8vw, 5rem);
--shell-y: clamp(1.25rem, 2.6vh, 2.5rem);
position: relative;
display: grid;
grid-template-rows: auto 1fr auto;
width: min(100%, 1600px);
min-height: 100svh;
margin: 0 auto;
padding: var(--shell-y) var(--edge);
}
.masthead,
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
.masthead {
padding-bottom: clamp(1rem, 2vw, 1.75rem);
border-bottom: 1px solid var(--line);
}
.brand-lockup {
display: flex;
align-items: center;
gap: 0.75rem;
}
.mark {
display: block;
width: 2.65rem;
height: auto;
flex-shrink: 0;
}
.brand-name {
color: var(--ink);
font-size: 0.9rem;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.05;
}
.card-index,
.footer {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
letter-spacing: 0.12em;
line-height: 1.45;
text-transform: uppercase;
}
.card-index,
.footer p {
color: var(--muted);
}
main {
display: grid;
}
.hero {
--hero-gap: clamp(1rem, 2vw, 2rem);
/* One column of the 12-column grid, and the spans the hero composition uses.
The flex line below is sized off these so it lands on the same lines. */
--hero-col: calc((100% - 11 * var(--hero-gap)) / 12);
--hero-span-8: calc(var(--hero-col) * 8 + 7 * var(--hero-gap));
--hero-span-4: calc(var(--hero-col) * 4 + 3 * var(--hero-gap));
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
column-gap: var(--hero-gap);
row-gap: clamp(1.9rem, 4.4vh, 4rem);
min-height: 0;
align-content: center;
padding: clamp(1.75rem, 4.5vh, 4.5rem) 0 clamp(1.5rem, 3vh, 3rem);
}
/* Row one of the hero, as a flex line rather than two grid areas. The two bases
add up to exactly the grid width, so at normal text size this renders where the
8/4 column split always put it — but when text alone is scaled up (Firefox's
text-only zoom, Safari's minimum font size, a large browser default) the
headline's minimum width outgrows its share, the line breaks, and the card
steps down instead of being printed over. */
.hero-lead {
grid-column: 1 / -1;
grid-row: 1;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
column-gap: var(--hero-gap);
row-gap: clamp(1.5rem, 3vh, 2.5rem);
}
.statement {
/* Optical, not metric: the R carries a left side bearing, so a box that starts on
the grid line prints ~0.078em to the right of it. The mission copy below opens
on a W, whose diagonal reaches the origin with no bearing at all, so the two
only look aligned once the headline is pulled back by that much. Measured off
rendered pixels, not the font metrics, and kept in em so it holds across the
whole font-size clamp. */
--optical-inset: 0.078em;
display: grid;
/* Flex sizes the border box, so the basis has to give back what the negative
margin takes or the card lands inside the eight columns. The half-pixel keeps
sub-pixel rounding in the two calc()s from ever summing past the hero width and
breaking the line at normal size; flex-grow hands it straight back. */
flex: 1 1 calc(var(--hero-span-8) + var(--optical-inset) - 0.5px);
/* Fills the row the way a stretched grid item did, so on the narrow desktop
widths where the card is the taller of the two, the headline's own rows keep
the extra height instead of collapsing to the card's baseline. */
align-self: stretch;
justify-items: start;
margin: 0 0 0 calc(-1 * var(--optical-inset));
font-size: clamp(3.2rem, min(9vw, 18vh), 8.25rem);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 0.98;
}
.line {
display: block;
/* Clips the word's drift exactly as overflow: hidden did, without making the line
a scroll container. A scroll container reports an automatic minimum size of
zero, which told the flex line above that the headline could always be squeezed
into its columns — so it never broke, and the type printed over the card. */
clip-path: inset(0);
padding: 0 0.08em 0.1em 0;
opacity: 0;
animation: rise-in 900ms cubic-bezier(0.2, 0.75, 0.2, 1) forwards;
}
.line-research {
animation-delay: 320ms;
}
.line-runs {
margin-left: 2.2em;
animation-delay: 480ms;
}
.word {
display: block;
padding-right: 0.06em;
color: inherit;
}
/* Rests at zero so the headline's left edge is the one the copy below aligns to.
The drift now arrives at that edge instead of parking 0.045em to the right of
it, which was what pushed "Research" off the mission line. */
.line-research .word {
transform: translate3d(0, 0, 0);
animation: drift-right 12s 1.35s ease-in-out backwards;
}
/* Deliberately wrappable. There is ~5% of slack between "that runs." and the card
at every width, so the pair only ever separates when the type has been scaled
past what the column can hold — at which point two short lines inside the
column beat one long line across the card. */
.line-runs .word {
transform: translate3d(0, -0.035em, 0);
animation: drift-up 11s 1.5s ease-in-out backwards;
}
.line-runs .that {
color: var(--secondary);
}
.line-runs .runs {
color: transparent;
background: linear-gradient(100deg, #cdead9 8%, #2f8f5c 82%);
background-clip: text;
-webkit-background-clip: text;
}
.mission {
grid-column: 1 / span 4;
grid-row: 2;
max-width: 29rem;
margin: 0;
color: var(--secondary);
font-size: clamp(0.95rem, 1.25vw, 1.14rem);
line-height: 1.5;
opacity: 0;
animation: fade-up 700ms 850ms ease-out forwards;
}
.product-card {
position: relative;
display: grid;
/* Never grows, so the half-pixel the headline gives up comes back to the headline
alone and the card starts exactly on the ninth column line. */
flex: 0 1 var(--hero-span-4);
grid-template-columns: 1fr;
gap: 0.55rem;
padding: 1.15rem 0 1.05rem;
border-top: 1px solid var(--line);
text-decoration: none;
opacity: 0;
animation: fade-up 700ms 980ms ease-out forwards;
}
.product-card::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 1px;
background: var(--pine);
content: '';
transform: scaleX(0);
transform-origin: left;
transition: transform 350ms ease;
}
.product-card:hover::after,
.product-card:focus-visible::after {
transform: scaleX(1);
}
.card-index {
color: var(--pine);
}
.product-name {
font-size: clamp(1.75rem, 3vw, 2.8rem);
font-weight: 600;
letter-spacing: -0.04em;
line-height: 1;
}
.product-description {
max-width: 18rem;
color: var(--secondary);
font-size: 0.95rem;
line-height: 1.45;
}
.footer {
padding-top: 1rem;
border-top: 1px solid var(--line);
opacity: 0;
animation: fade-in 700ms 1.1s ease-out forwards;
}
.footer p {
margin: 0;
}
/* Uppercase mono at 0.12em tracking is wide enough that a phone will break both
the copyright and the arrow off its label. Neither is allowed to wrap; the
mobile query below stacks them instead when the row no longer fits. */
.footer p,
.footer a {
white-space: nowrap;
}
.footer-links {
display: flex;
align-items: center;
gap: 1.25rem;
}
.footer a {
color: var(--muted);
text-decoration-thickness: 1px;
text-underline-offset: 0.35rem;
}
@keyframes rise-in {
from {
transform: translate3d(0, 72%, 0) rotate(1.5deg);
opacity: 0;
}
to {
transform: translate3d(0, 0, 0) rotate(0);
opacity: 1;
}
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-up {
from {
transform: translate3d(0, 1rem, 0);
opacity: 0;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@keyframes drift-right {
from { transform: translate3d(-0.045em, 0, 0); }
}
@keyframes drift-up {
from { transform: translate3d(0, 0, 0); }
}
@keyframes haze-drift {
from { transform: translate3d(-9%, 3%, 0) rotate(-0.8deg); }
to { transform: translate3d(10%, -4%, 0) rotate(0.8deg); }
}
/* Travels further and slower than the haze, and scales as it goes, so the two
bands never hold the same relative position for long. */
@keyframes drift-across {
from { transform: translate3d(7%, -5%, 0) scale(1.04); }
to { transform: translate3d(-9%, 7%, 0) scale(1.13); }
}
@media (max-width: 720px) {
/* A phone has no width to spare, so the inset falls back to what it takes to keep
the text off the bezel rather than what the desktop composition wants. */
.page-shell {
--edge: clamp(1.15rem, 5vw, 1.85rem);
/* The footer is parked past the fold: on a phone its rule is a third horizontal
line competing with the masthead's and the card's, and the page reads calmer
without it. The drop has to pay for the footer's own height and the shell's
bottom padding before it buys any clearance, hence a value this large.
The (100lvh - 100svh) term is the toolbar: main is sized to the small
viewport so the hero centres in what you can actually see, but Safari's
bottom bar is translucent and the page shows through it, so clearing svh
only put the footer's rule behind the controls rather than out of sight.
Both terms collapse to the clamp anywhere the two viewports agree.
The clamp is sized to park the footer just out of sight, not to bury it: it
pays for the footer's own height and the shell's bottom padding first, and
what is left over is the clearance below the fold — about 20-27px, enough
that the rule cannot peek at rest, and no more. Every pixel beyond that is
scroll the reader has to spend before the footer starts arriving. */
--footer-drop: calc(100lvh - 100svh + clamp(5.25rem, 10.5vh, 6rem));
min-height: calc(100svh + var(--footer-drop));
}
.hero {
grid-template-columns: 1fr;
row-gap: clamp(1.75rem, 5vh, 3rem);
padding-top: clamp(2.5rem, 8vh, 5rem);
}
.statement {
font-size: clamp(2.9rem, 14vw, 5.5rem);
}
.line-runs {
margin-left: 1.2em;
}
/* One column, so there is nothing for the headline to collide with and no need
for the flex line. Dissolving the wrapper puts all three back on the hero grid,
where explicit rows keep the phone order the design has always had — the DOM
carries the card first because that is where it reads on the desktop layout. */
.hero-lead {
display: contents;
}
.statement,
.mission,
.product-card {
grid-column: 1;
}
.statement {
grid-row: 1;
}
.mission {
grid-row: 2;
max-width: 25rem;
}
/* Stacked, the card is a separate thought from the hero copy rather than the
column beside it, so it wants more air above than the hero's row rhythm gives. */
.product-card {
grid-row: 3;
margin-top: clamp(0.75rem, 3.5vh, 2.25rem);
}
/* One line each: side by side these two need ~360px of tracking-heavy mono, which
is more than a 390px screen has once the edge inset is paid. */
.footer {
flex-direction: column;
align-items: flex-start;
gap: 0.8rem;
margin-top: var(--footer-drop);
}
}
/* The phone footer is below the fold, so a timed entrance would have played out
unseen. Driven off the scroll position instead, it and its rule arrive as they
come up the screen. Scroll-driven animations are still the newest thing here, so
this stays behind @supports: without it the footer keeps the timed fade above,
which lands on load and leaves it plainly visible. */
@supports (animation-timeline: view()) {
@media (max-width: 720px) {
/* Where the reveal runs, the footer is transparent until it is scrolled to, so
opacity is what hides it and the layout no longer has to. That buys back both
halves of the drop the fallback needs: the toolbar term goes, because a footer
behind Safari's bar cannot show through it at zero alpha, and the clearance
shrinks to the little that keeps the rule from being mid-fade at rest. All of
it was scroll the reader had to spend before anything began to happen. */
.page-shell {
--footer-drop: clamp(4.75rem, 9.3vh, 5.1rem);
}
.footer {
opacity: 0;
animation: footer-reveal linear both;
animation-timeline: view();
/* "entry" opens before the footer's edge reaches the fold, not at it: parked,
it already measures 9-11% entered across phone sizes. So the start has to be
past that or the footer sits permanently part-faded at rest — 20% clears it
on every size tested with room to spare, and still begins the reveal within
the first few pixels of scroll. Ending at 45% means it reads as arrived at
roughly a third of the travel, instead of still fading while it already
looks fully on screen. */
animation-range: entry 20% entry 45%;
}
}
}
@keyframes footer-reveal {
from {
transform: translate3d(0, 1.15rem, 0);
opacity: 0;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@media (max-width: 440px) {
.mark {
width: 2.35rem;
}
.statement {
font-size: clamp(2.5rem, 13.5vw, 4.2rem);
letter-spacing: -0.025em;
}
.footer {
font-size: 0.72rem;
}
}
/* Short viewports: compress the vertical rhythm so the composition still lands
on one screen. Declared after the width queries so it wins where they overlap. */
@media (max-height: 780px) and (min-width: 721px) {
.hero {
row-gap: clamp(1.1rem, 2.8vh, 2.25rem);
padding: clamp(0.75rem, 2.2vh, 2rem) 0 clamp(0.5rem, 1.8vh, 1.5rem);
}
.statement {
font-size: clamp(2.25rem, min(9vw, 15vh), 8.25rem);
}
}
/* Short phones can't fit the whole composition at a headline size worth reading,
so tighten the spacing but leave the type alone and let the page scroll. */
@media (max-height: 700px) and (max-width: 720px) {
.hero {
row-gap: clamp(1.1rem, 2.9vh, 2rem);
padding: clamp(0.75rem, 2.2vh, 1.75rem) 0 clamp(0.5rem, 1.4vh, 1.25rem);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation: none !important;
transition: none !important;
}
/* The ambient bands are deliberately absent from this list. Each one's base
transform is an endpoint of its own keyframes, so with the animation off they
hold a frame of it — clearing them would snap both back to the origin and
stack the two washes on top of each other. */
.masthead,
.line,
.mission,
.product-card,
.footer {
opacity: 1;
transform: none;
}
}
</style>