heartwood every commit a ring

Refactor: Migrate styles from styled-components to CSS Modules

576bfc16 by Isaac Bythewood · 7 months ago

Refactor: Migrate styles from styled-components to CSS Modules

- Converted code.js to use CSS Modules for styling, creating code.module.css.
- Removed styled-components and replaced with className references.
- Updated contact.js to utilize CSS Modules, creating contact.module.css.
- Removed styled-components and replaced with className references.
- Refactored index.js to use CSS Modules, creating index.module.css.
- Removed styled-components and replaced with className references.
- Created globals.css for global styles and design tokens.
- Ensured consistent styling across components with CSS Modules.
added components/canvas.module.css
@@ -0,0 +1,4 @@.canvas {  width: 100%;  height: 100%;}
modified components/constellations.js
@@ -1,5 +1,5 @@import React, { useEffect, useRef } from "react";import styled from "styled-components";import styles from "./canvas.module.css";import PropTypes from "prop-types";const Constellations = ({ options }) => {
@@ -121,7 +121,7 @@ const Constellations = ({ options }) => {    };  }, [isActive]);  return <Canvas ref={canvas} />;  return <canvas ref={canvas} className={styles.canvas} />;};Constellations.propTypes = {
@@ -130,7 +130,4 @@ Constellations.propTypes = {export default Constellations;const Canvas = styled.canvas`  width: 100%;  height: 100%;`;// migrated to CSS Modules
modified components/grid.js
@@ -1,19 +1,25 @@import React from "react";import styled from "styled-components";import styles from "./grid.module.css";import PropTypes from "prop-types";const Grid = (props) => {  return (    <>      <GridLines>      <div className={styles.gridLines}>        {Array(6)          .fill()          .map((_, i) => {            const column = i + 1;            return <GridLine key={column} column={column} />;            return (              <div                key={column}                className={styles.gridLine}                style={{ gridColumn: column }}              />            );          })}      </GridLines>      <GridArea>{props.children}</GridArea>      </div>      <div className={styles.gridArea}>{props.children}</div>    </>  );};
@@ -27,31 +33,4 @@ Grid.propTypes = {export default Grid;const GridArea = styled.div`  display: grid;  grid-template-columns: 60px 15% 1fr 1fr 15% 60px;  grid-template-rows: 1fr auto 1fr;  grid-template-areas:    ". . .    .    . ."    ". . main main . ."    ". . .    .    . .";  min-height: 100vh;  width: 100%;  @media (${(props) => props.theme.breakpoints.tablet}) {    grid-template-columns: 10px 5% 1fr 1fr 5% 10px;  }`;const GridLines = styled(GridArea)`  position: fixed;  pointer-events: none;  @media (${(props) => props.theme.breakpoints.tablet}) {    display: none;  }`;const GridLine = styled.div`  border-right: 1px solid rgba(125, 125, 125, 0.2);  grid-row: 1 / -1;  grid-column: ${(props) => props.column};`;// migrated to CSS Modules
added components/grid.module.css
@@ -0,0 +1,42 @@.gridArea {  display: grid;  grid-template-columns: 60px 15% 1fr 1fr 15% 60px;  grid-template-rows: 1fr auto 1fr;  grid-template-areas:    ". . .    .    . ."    ". . main main . ."    ". . .    .    . .";  min-height: 100vh;  width: 100%;}@media (max-width: 1023.98px) {  .gridArea {    grid-template-columns: 10px 5% 1fr 1fr 5% 10px;  }}.gridLines {  position: fixed;  pointer-events: none;  display: grid;  grid-template-columns: 60px 15% 1fr 1fr 15% 60px;  grid-template-rows: 1fr auto 1fr;  grid-template-areas:    ". . .    .    . ."    ". . main main . ."    ". . .    .    . .";  min-height: 100vh;  width: 100%;}@media (max-width: 1023.98px) {  .gridLines {    display: none;  }}.gridLine {  border-right: 1px solid rgba(125, 125, 125, 0.2);  grid-row: 1 / -1;}
modified components/loader.js
@@ -1,66 +1,32 @@import React from "react";import styled, { keyframes } from "styled-components";import styles from "./loader.module.css";const Loader = () => {  return (    <>      <Grid>      <div className={styles.grid}>        {Array(6)          .fill()          .map((_, i) => {            const column = i + 1;            return <GridColumn key={column} column={column} />;            return (              <div                key={column}                className={styles.gridColumn}                style={{                  gridColumn: column,                  // match animation-delay: column * 100ms for both pseudo-elements                  // We'll set CSS variable that both ::before and ::after can read                  ["--delay"]: `${column * 100}ms`,                }}              />            );          })}      </Grid>      </div>    </>  );};export default Loader;const FoldUp = keyframes`  0% {    height: 100vh;  }  100% {    height: 0;  }`;const Grid = styled.div`  display: grid;  grid-template-columns: 60px 15% 1fr 1fr 15% 60px;  grid-template-rows: 1fr;  min-height: 100vh;  width: 100%;  position: fixed;  z-index: 9999;  pointer-events: none;`;const GridColumn = styled.div`  grid-column: ${(props) => props.column};  &::before {    content: "";    display: block;    width: 100%;    height: 100vh;    background-color: ${(props) => props.theme.colors.primary};    border-right: 1px solid ${(props) => props.theme.colors.primary};    animation: ${FoldUp} 1000ms normal forwards;    animation-delay: ${(props) => props.column * 100}ms;  }  &::after {    content: "";    display: block;    width: 100%;    height: 100vh;    background-color: white;    border-right: 1px solid white;    animation: ${FoldUp} 1000ms normal forwards;    animation-delay: ${(props) => props.column * 100}ms;  }`;// migrated to CSS Modules
added components/loader.module.css
@@ -0,0 +1,39 @@@keyframes foldUp {  0% {    height: 100vh;  }  100% {    height: 0;  }}.grid {  display: grid;  grid-template-columns: 60px 15% 1fr 1fr 15% 60px;  grid-template-rows: 1fr;  min-height: 100vh;  width: 100%;  position: fixed;  z-index: 9999;  pointer-events: none;}.gridColumn::before,.gridColumn::after {  content: "";  display: block;  width: 100%;  height: 100vh;  animation: foldUp 1000ms normal forwards;  animation-delay: var(--delay, 0ms);}.gridColumn::before {  background-color: var(--color-primary);  border-right: 1px solid var(--color-primary);}.gridColumn::after {  background-color: white;  border-right: 1px solid white;}
modified components/menu.js
@@ -3,7 +3,7 @@ import React, { useState } from "react";import { CSSTransition } from "react-transition-group";import Image from "next/image";import Link from "next/link";import styled from "styled-components";import styles from "./menu.module.css";const Menu = () => {  const [open, setOpen] = useState(false);
@@ -27,314 +27,103 @@ const Menu = () => {  return (    <>      <Hamburger onClick={toggleMenu} aria-label="Hamburger">        <Patty style={{ width: "15px" }} />        <Patty />        <Patty style={{ width: "20px" }} />      </Hamburger>      <CSSTransition in={open} timeout={500} classNames="transition">        <Overlay>          <OverlayGrid>            <CSSTransition              in={open}              timeout={500}              classNames="transition"              appear            >              <OverlayGridLeft>                <TopBar>      <button        className={styles.hamburger}        onClick={toggleMenu}        aria-label="Hamburger"      >        <div className={styles.patty} style={{ width: "15px" }} />        <div className={styles.patty} />        <div className={styles.patty} style={{ width: "20px" }} />      </button>      <CSSTransition        in={open}        timeout={500}        classNames="menu"        appear        onEnter={() => {          if (typeof document !== "undefined") {            document.body.style.overflowY = "hidden";          }        }}        onExited={() => {          if (typeof document !== "undefined") {            document.body.style.overflowY = "scroll";          }        }}      >        <div className={styles.overlay}>          <div className={styles.overlayGrid}>            <CSSTransition in={open} timeout={500} classNames="menu" appear>              <div className={styles.overlayGridLeft}>                <div className={styles.topBar}>                  <Link href="https://blog.bythewood.me/" passHref>                    <TopLink target="_blank">Blog</TopLink>                    <a className={styles.topLink} target="_blank">                      Blog                    </a>                  </Link>                  <Link                    href="https://status.bythewood.me/properties/87097ef2-5643-4999-917e-72b172dd9e19/"                    passHref                  >                    <TopLink target="_blank">Status</TopLink>                    <a className={styles.topLink} target="_blank">                      Status                    </a>                  </Link>                  <Link                    href="https://analytics.bythewood.me/properties/30e69c06-9beb-4283-8919-8c7a686ab013/"                    passHref                  >                    <TopLink target="_blank">Analytics</TopLink>                    <a className={styles.topLink} target="_blank">                      Analytics                    </a>                  </Link>                  <Link href="https://github.com/overshard" passHref>                    <TopLink target="_blank">GitHub</TopLink>                    <a className={styles.topLink} target="_blank">                      GitHub                    </a>                  </Link>                  <Bar />                </TopBar>                  <div className={styles.bar} />                </div>                {pages.map((page) => {                  return (                    <Link key={page.href} href={page.href} passHref>                      <OverlayLink data-text={page.title} onClick={toggleMenu}>                      <a                        className={styles.overlayLink}                        data-text={page.title}                        onClick={toggleMenu}                      >                        {page.title}                      </OverlayLink>                      </a>                    </Link>                  );                })}                <BottomBar>                <div className={styles.bottomBar}>                  <Image                    src="/static/images/avatar.webp"                    alt="Isaac Bythewood"                    width={50}                    height={50}                  />                  <Bar />                </BottomBar>              </OverlayGridLeft>                  <div className={styles.bar} />                </div>              </div>            </CSSTransition>            <CSSTransition              in={open}              timeout={500}              classNames="transition"              appear            >              <OverlayGridRight>            <CSSTransition in={open} timeout={500} classNames="menu" appear>              <div className={styles.overlayGridRight}>                <Image                  src="/static/images/art/acrylic-pours/006.webp"                  alt="#006 Molten Copper"                  loading="lazy"                  layout="fill"                />              </OverlayGridRight>              </div>            </CSSTransition>          </OverlayGrid>        </Overlay>          </div>        </div>      </CSSTransition>    </>  );};export default Menu;const Hamburger = styled.button`  position: fixed;  top: 30px;  right: 0;  padding: 20px 15px;  background: white;  z-index: 5001;  border: none;  &:hover * {    width: 30px !important;  }  @media (${(props) => props.theme.breakpoints.tablet}) {    top: 0;    padding-bottom: 21px;  }`;const Patty = styled.div`  background: black;  width: 30px;  height: 3px;  margin-bottom: 5px;  margin-left: auto;  transition-duration: 250ms;  transition-property: width;  &:last-child {    margin-bottom: 0;  }`;const Overlay = styled.div`  position: fixed;  top: 0;  right: 0;  bottom: 0;  left: 0;  z-index: -5000;  &.transition-enter {    z-index: 5000;  }  &.transition-enter-active {    z-index: 5000;  }  &.transition-enter-done {    z-index: 5000;  }  &.transition-exit {    z-index: 5000;  }  &.transition-exit-active {    z-index: 5000;  }  &.transition-exit-done {    z-index: -5000;  }`;const OverlayGrid = styled.div`  display: grid;  grid-template-columns: 60% 40%;  @media (${(props) => props.theme.breakpoints.mobile}) {    grid-template-columns: 100%;  }`;const OverlayGridLeft = styled.div`  grid-area: 1/1;  display: flex;  flex-direction: column;  justify-content: center;  align-items: center;  background: white;  transition-duration: 500ms;  transition-property: transform;  transform: translateX(-100%);  &.transition-enter {    transform: translateX(-100%);  }  &.transition-enter-active {    transform: translateX(0);  }  &.transition-enter-done {    transform: translateX(0);  }  &.transition-exit {    transform: translateX(0);  }  &.transition-exit-active {    transform: translateX(-100%);  }  &.transition-exit-done {    transform: translateX(-100%);  }`;const OverlayGridRight = styled.div`  grid-area: 1/2;  height: 100vh;  overflow: hidden;  transition-duration: 500ms;  transition-property: transform;  transform: translateX(100%);  &.transition-enter {    transform: translateX(100%);  }  &.transition-enter-active {    transform: translateX(0);  }  &.transition-enter-done {    transform: translateX(0);  }  &.transition-exit {    transform: translateX(0);  }  &.transition-exit-active {    transform: translateX(100%);  }  &.transition-exit-done {    transform: translateX(100%);  }  img {    object-fit: cover;    height: 100vh;  }`;const OverlayLink = styled.a`  font-weight: 900;  text-decoration: none;  margin-bottom: 10px;  font-size: 3em;  color: black;  position: relative;  &:last-child {    margin-bottom: 0;  }  &::before {    content: attr(data-text);    color: white;    position: absolute;    width: 0;    left: 0;    bottom: 3px;    height: 27px;    background-color: black;    transition-property: width;    transition-duration: 200ms;    line-height: 0;    overflow: hidden;  }  &:hover {    &::before {      width: 100%;    }  }`;const TopBar = styled.div`  display: flex;  width: 100%;  position: absolute;  left: 20px;  top: 20px;  align-items: center;  justify-content: start;`;const TopLink = styled.a`  text-decoration: none;  color: white;  background-color: rgba(0, 0, 0, 1);  padding: 0.25rem 0.5rem;  margin-left: 1rem;  transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;  font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",    "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",    "Fira Mono", "Droid Sans Mono", "Courier New", monospace;  &:hover {    background-color: rgba(0, 0, 0, 0.1);    color: black;  }  @media (${(props) => props.theme.breakpoints.mobile}) {    font-size: 0.8em;    margin-left: 0.5rem;  }`;const BottomBar = styled.div`  display: flex;  width: 100%;  position: absolute;  left: 20px;  bottom: 20px;  align-items: center;`;const Bar = styled.div`  width: 100%;  height: 1px;  background: rgba(0, 0, 0, 0.3);  margin-left: 40px;  margin-right: 60px;`;
added components/menu.module.css
@@ -0,0 +1,196 @@.hamburger {  position: fixed;  top: 30px;  right: 0;  padding: 20px 15px;  background: white;  z-index: 5001;  border: none;}.hamburger:hover * {  width: 30px !important;}@media (max-width: 1023.98px) {  .hamburger {    top: 0;    padding-bottom: 21px;  }}.patty {  background: black;  width: 30px;  height: 3px;  margin-bottom: 5px;  margin-left: auto;  transition: width 250ms;}.patty:last-child {  margin-bottom: 0;}.overlay {  position: fixed;  top: 0;  right: 0;  bottom: 0;  left: 0;  z-index: -5000;  background: transparent;}.overlay:global(.menu-enter),.overlay:global(.menu-enter-active),.overlay:global(.menu-enter-done),.overlay:global(.menu-exit),.overlay:global(.menu-exit-active) {  z-index: 5000;}.overlay:global(.menu-exit-done) {  z-index: -5000;}.overlayGrid {  display: grid;  grid-template-columns: 60% 40%;}@media (max-width: 767.98px) {  .overlayGrid {    grid-template-columns: 100%;  }}.overlayGridLeft {  grid-area: 1/1;  display: flex;  flex-direction: column;  justify-content: center;  align-items: center;  background: white;  transition: transform 500ms;  transform: translateX(-100%);}.overlayGridLeft:global(.menu-enter),.overlayGridLeft:global(.menu-enter-active),.overlayGridLeft:global(.menu-enter-done) {  transform: translateX(0);}.overlayGridLeft:global(.menu-exit),.overlayGridLeft:global(.menu-exit-active),.overlayGridLeft:global(.menu-exit-done) {  transform: translateX(-100%);}.overlayGridRight {  grid-area: 1/2;  height: 100vh;  overflow: hidden;  transition: transform 500ms;  transform: translateX(100%);}.overlayGridRight:global(.menu-enter),.overlayGridRight:global(.menu-enter-active),.overlayGridRight:global(.menu-enter-done) {  transform: translateX(0);}.overlayGridRight:global(.menu-exit),.overlayGridRight:global(.menu-exit-active),.overlayGridRight:global(.menu-exit-done) {  transform: translateX(100%);}.overlayGridRight img {  object-fit: cover;  height: 100vh;}.overlayLink {  font-weight: 900;  text-decoration: none;  margin-bottom: 10px;  font-size: 3em;  color: black;  position: relative;}.overlayLink:last-child {  margin-bottom: 0;}.overlayLink::before {  content: attr(data-text);  color: white;  position: absolute;  width: 0;  left: 0;  bottom: 3px;  height: 27px;  background-color: black;  transition: width 200ms;  line-height: 0;  overflow: hidden;}.overlayLink:hover::before {  width: 100%;}.topBar {  display: flex;  width: 100%;  position: absolute;  left: 20px;  top: 20px;  align-items: center;  justify-content: flex-start;}.topLink {  text-decoration: none;  color: white;  background-color: rgba(0, 0, 0, 1);  padding: 0.25rem 0.5rem;  margin-left: 1rem;  transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;  font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",    "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",    "Fira Mono", "Droid Sans Mono", "Courier New", monospace;}.topLink:hover {  background-color: rgba(0, 0, 0, 0.1);  color: black;}@media (max-width: 767.98px) {  .topLink {    font-size: 0.8em;    margin-left: 0.5rem;  }}.bottomBar {  display: flex;  width: 100%;  position: absolute;  left: 20px;  bottom: 20px;  align-items: center;}.bar {  width: 100%;  height: 1px;  background: rgba(0, 0, 0, 0.3);  margin-left: 40px;  margin-right: 60px;}
modified components/mouse.js
@@ -1,5 +1,5 @@import React, { useEffect, useRef } from "react";import styled from "styled-components";import styles from "./mouse.module.css";const Mouse = () => {  const cursorRef = useRef(null);
@@ -19,9 +19,12 @@ const Mouse = () => {      isInteractiveRef.current =        target.tagName === "BUTTON" ||        target.tagName === "A" ||        target.classList?.contains("mouse-activate");        (target.classList && target.classList.contains("mouse-activate"));      if (circleRef.current) {        circleRef.current.classList.toggle("activated", isInteractiveRef.current);        circleRef.current.classList.toggle(          "activated",          isInteractiveRef.current        );      }    };
@@ -33,8 +36,10 @@ const Mouse = () => {    const animate = () => {      const { x, y } = mousePos.current;      if (cursorRef.current) cursorRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;      if (circleRef.current) circleRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;      if (cursorRef.current)        cursorRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;      if (circleRef.current)        circleRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;      animationFrameId = requestAnimationFrame(animate);    };
@@ -50,61 +55,23 @@ const Mouse = () => {  return (    <>      <PageCursor ref={cursorRef}>        <svg width="16" height="16" viewBox="0 0 16 16" fill="#ff2d2d" fillOpacity="0.9" xmlns="http://www.w3.org/2000/svg">      <div ref={cursorRef} className={styles.pageCursor}>        <svg          width="16"          height="16"          viewBox="0 0 16 16"          fill="#ff2d2d"          fillOpacity="0.9"          xmlns="http://www.w3.org/2000/svg"        >          <circle cx="8" cy="8" r="8" />        </svg>      </PageCursor>      <PageCursorCircle ref={circleRef} />      </div>      <div ref={circleRef} className={styles.pageCursorCircle} />    </>  );};export default Mouse;const PageCursor = styled.div`  position: fixed;  z-index: 100000;  top: 0;  left: 0;  width: 16px;  height: 16px;  margin: -8px 0 0 -8px;  pointer-events: none;  will-change: transform;  backface-visibility: hidden;  @media (${(props) => props.theme.breakpoints.tablet}) {    display: none;  }`;const PageCursorCircle = styled.div`  position: fixed;  z-index: 100000;  top: 0;  left: 0;  pointer-events: none;  margin: -25px 0 0 -26px;  width: 50px;  height: 50px;  border: 1px solid #ff2d2d;  opacity: 0.7;  border-radius: 50%;  transform-origin: center center;  transition: width 0.4s ease, height 0.4s ease, transform 0.4s ease, opacity 0.4s ease;  will-change: transform, width, height, opacity;  backface-visibility: hidden;  &.activated {    margin: -35px 0 0 -36px;    width: 70px;    height: 70px;    background-color: #ff2d2d;    opacity: 0.3;  }  @media (${(props) => props.theme.breakpoints.tablet}) {    display: none;  }`;// migrated to CSS Modules
added components/mouse.module.css
@@ -0,0 +1,52 @@.pageCursor {  position: fixed;  z-index: 100000;  top: 0;  left: 0;  width: 16px;  height: 16px;  margin: -8px 0 0 -8px;  pointer-events: none;  will-change: transform;  backface-visibility: hidden;}@media (max-width: 1023.98px) {  .pageCursor {    display: none;  }}.pageCursorCircle {  position: fixed;  z-index: 100000;  top: 0;  left: 0;  pointer-events: none;  margin: -25px 0 0 -26px;  width: 50px;  height: 50px;  border: 1px solid #ff2d2d;  opacity: 0.7;  border-radius: 50%;  transform-origin: center center;  transition: width 0.4s ease, height 0.4s ease, transform 0.4s ease, opacity 0.4s ease;  will-change: transform, width, height, opacity;  backface-visibility: hidden;}.pageCursorCircle.activated,:global(.activated).pageCursorCircle,.pageCursorCircle:global(.activated) {  margin: -35px 0 0 -36px;  width: 70px;  height: 70px;  background-color: #ff2d2d;  opacity: 0.3;}@media (max-width: 1023.98px) {  .pageCursorCircle {    display: none;  }}
modified components/page.js
@@ -1,7 +1,7 @@import React from "react";import Head from "next/head";import PropTypes from "prop-types";import styled from "styled-components";import styles from "./page.module.css";import { strings } from "../site.config";
@@ -20,9 +20,12 @@ const Page = (props) => {          content={props.description ? props.description : defaultDescription}        />      </Head>      <Main gridArea={props.gridArea ? props.gridArea : "main"}>      <main        style={{ gridArea: props.gridArea ? props.gridArea : "main" }}        className={styles.main}      >        {props.children}      </Main>      </main>    </>  );};
@@ -39,6 +42,4 @@ Page.propTypes = {export default Page;const Main = styled.main`  ${(props) => props.gridArea && `grid-area:  ${props.gridArea};`}`;// migrated to CSS Module
added components/page.module.css
@@ -0,0 +1,10 @@.main {  /* dynamic grid-area added inline via style prop */  display: flex;  flex-direction: column;  align-items: flex-start;  justify-content: center;  min-height: 100vh;  width: 100%;  text-align: left;}
modified components/particleflow.js
@@ -1,5 +1,5 @@import React, { useEffect, useRef } from "react";import styled from "styled-components";import styles from "./canvas.module.css";import PropTypes from "prop-types";const ParticleFlow = ({ options }) => {
@@ -181,7 +181,7 @@ const ParticleFlow = ({ options }) => {    };  }, [isActive]);  return <Canvas ref={canvas} />;  return <canvas ref={canvas} className={styles.canvas} />;};ParticleFlow.propTypes = {
@@ -190,7 +190,4 @@ ParticleFlow.propTypes = {export default ParticleFlow;const Canvas = styled.canvas`  width: 100%;  height: 100%;`;// migrated to CSS Modules
modified components/retrostars.js
@@ -1,5 +1,5 @@import React, { useEffect, useRef, useState } from "react";import styled from "styled-components";import styles from "./canvas.module.css";import PropTypes from "prop-types";const RetroStars = ({ options }) => {
@@ -244,7 +244,7 @@ const RetroStars = ({ options }) => {    };  }, [isActive]);  return <Canvas ref={canvas} />;  return <canvas ref={canvas} className={styles.canvas} />;};RetroStars.propTypes = {
@@ -253,7 +253,4 @@ RetroStars.propTypes = {export default RetroStars;const Canvas = styled.canvas`  width: 100%;  height: 100%;`;// migrated to CSS Modules
modified components/sidebar.js
@@ -1,6 +1,6 @@import React, { useState, useEffect } from "react";import PropTypes from "prop-types";import styled from "styled-components";import styles from "./sidebar.module.css";import Link from "next/link";import { withRouter } from "next/router";
@@ -35,15 +35,15 @@ const Sidebar = ({ router }) => {  }, []);  return (    <Nav>    <nav className={styles.nav}>      <Link href="/" passHref>        <NavLogo aria-label="Back to home" />        <a className={styles.navLogo} aria-label="Back to home" />      </Link>      <Link href="/contact" passHref>        <NavContact>Get in touch</NavContact>        <a className={styles.navContact}>Get in touch</a>      </Link>      <NavCurrent>{current}</NavCurrent>    </Nav>      <div className={styles.navCurrent}>{current}</div>    </nav>  );};
@@ -52,107 +52,3 @@ Sidebar.propTypes = {};export default withRouter(Sidebar);const Nav = styled.nav`  background-color: white;  color: black;  display: flex;  flex-direction: column;  justify-content: space-between;  width: 60px;  top: 0;  left: 0;  bottom: 0;  position: fixed;  z-index: 1;  border-right: 1px solid rgba(125, 125, 125, 0.2);  z-index: 100;  @media (${(props) => props.theme.breakpoints.tablet}) {    bottom: auto;    left: 0;    flex-direction: row;    width: 100%;    height: 60px;    border-right: none;  }`;const NavLogo = styled.a`  content: "";  display: block;  background-image: linear-gradient(    to right,    ${(props) => props.theme.colors.blue} 0,    ${(props) => props.theme.colors.purple} 100%  );  width: 40px;  height: 40px;  z-index: 3;  margin-left: 10px;  margin-top: 10px;  transition: transform 0.2s ease-in-out;  &:hover {    transform: rotate(15deg) scale(1.1);  }  @media (${(props) => props.theme.breakpoints.mobile}) {    left: 35px;  }`;const NavContact = styled.a`  font-weight: 700;  writing-mode: vertical-rl;  transform: rotate(180deg);  text-decoration: none;  font-size: 1.2em;  line-height: 1.5em;  margin: 15px;  color: black;  position: relative;  overflow: hidden;  &::before {    content: "";    display: block;    position: absolute;    z-index: -1;    right: 100%;    width: 100%;    height: 100%;    background: rgba(14, 64, 244, 0.3);    transition: right 250ms;  }  &:hover {    &::before {      right: 60%;    }  }  @media (${(props) => props.theme.breakpoints.tablet}) {    writing-mode: horizontal-tb;    transform: none;  }  @media (${(props) => props.theme.breakpoints.mobile}) {    writing-mode: horizontal-tb;    transform: none;    display: none;  }`;const NavCurrent = styled.div`  font-family: monospace;  background-color: black;  text-align: center;  padding: 5px;  color: white;  @media (${(props) => props.theme.breakpoints.tablet}) {    writing-mode: vertical-rl;    transform: rotate(180deg);  }`;
added components/sidebar.module.css
@@ -0,0 +1,109 @@.nav {  background-color: white;  color: black;  display: flex;  flex-direction: column;  justify-content: space-between;  width: 60px;  top: 0;  left: 0;  bottom: 0;  position: fixed;  z-index: 100;  border-right: 1px solid rgba(125, 125, 125, 0.2);}@media (max-width: 1023.98px) {  .nav {    bottom: auto;    left: 0;    flex-direction: row;    width: 100%;    height: 60px;    border-right: none;  }}.navLogo {  display: block;  background-image: linear-gradient(    to right,    var(--color-blue) 0,    var(--color-purple) 100%  );  width: 40px;  height: 40px;  z-index: 3;  margin-left: 10px;  margin-top: 10px;  transition: transform 0.2s ease-in-out;}.navLogo:hover {  transform: rotate(15deg) scale(1.1);}@media (max-width: 767.98px) {  .navLogo {    left: 35px;  }}.navContact {  font-weight: 700;  writing-mode: vertical-rl;  transform: rotate(180deg);  text-decoration: none;  font-size: 1.2em;  line-height: 1.5em;  margin: 15px;  color: black;  position: relative;  overflow: hidden;}.navContact::before {  content: "";  display: block;  position: absolute;  z-index: -1;  right: 100%;  width: 100%;  height: 100%;  background: rgba(14, 64, 244, 0.3);  transition: right 250ms;}.navContact:hover::before {  right: 60%;}@media (max-width: 1023.98px) {  .navContact {    writing-mode: horizontal-tb;    transform: none;  }}@media (max-width: 767.98px) {  .navContact {    writing-mode: horizontal-tb;    transform: none;    display: none;  }}.navCurrent {  font-family: monospace;  background-color: black;  text-align: center;  padding: 5px;  color: white;}@media (max-width: 1023.98px) {  .navCurrent {    writing-mode: vertical-rl;    transform: rotate(180deg);  }}
modified components/synthwave.js
@@ -1,5 +1,5 @@import React, { useEffect, useRef } from "react";import styled from "styled-components";import styles from "./canvas.module.css";const Synthwave = () => {  const canvas = useRef(null);
@@ -146,12 +146,9 @@ const Synthwave = () => {    };  }, []);  return <Canvas ref={canvas} />;  return <canvas ref={canvas} className={styles.canvas} />;};export default Synthwave;const Canvas = styled.canvas`  width: 100%;  height: 100%;`;// migrated to CSS Modules
modified package.json
@@ -15,8 +15,7 @@    "react-transition-group": "^4.4.1",    "sequelize": "^6.21.1",    "sharp": "^0.30.7",    "sqlite3": "^5.0.8",    "styled-components": "^5.2.3"    "sqlite3": "^5.0.8"  },  "devDependencies": {    "@next/eslint-plugin-next": "^12.1.6",
modified pages/_app.js
@@ -1,9 +1,8 @@import React from "react";import App from "next/app";import styled, { ThemeProvider, createGlobalStyle } from "styled-components";import "../styles/globals.css";import { TransitionGroup, CSSTransition } from "react-transition-group";import { theme } from "../site.config";import Grid from "../components/grid";import Sidebar from "../components/sidebar";import Menu from "../components/menu";
@@ -15,83 +14,29 @@ class MyApp extends App {    const { Component, pageProps } = this.props;    return (      <ThemeProvider theme={theme}>        <>          <GlobalStyle />          <Mouse />          <Loader />          <Sidebar />          <Menu />          <TransitionGroup component={null}>            <CSSTransition              key={this.props.router.route}              appear              timeout={250}              classNames="transition"            >              <Transition>                <Grid>                  <Component {...pageProps} />                </Grid>              </Transition>            </CSSTransition>          </TransitionGroup>        </>      </ThemeProvider>      <>        <Mouse />        <Loader />        <Sidebar />        <Menu />        <TransitionGroup component={null}>          <CSSTransition            key={this.props.router.route}            appear            timeout={250}            classNames="transition"          >            <div className="transition">              <Grid>                <Component {...pageProps} />              </Grid>            </div>          </CSSTransition>        </TransitionGroup>      </>    );  }}export default MyApp;const GlobalStyle = createGlobalStyle`  body {    font-family:      -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,      sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";    color: white;    background-color: ${(props) => props.theme.colors.primary};    min-height: 100vh;    width: 100%;    padding: 0;    margin: 0;    overflow-x: hidden;    text-shadow: rgba(0, 0, 0, .01) 0 0 1px;    text-rendering: optimizeLegibility;    user-select: none;    cursor: none;  }  a {    cursor: none;  }  button {    cursor: none;  }`;const Transition = styled.div`  position: absolute;  top: 0;  left: 0;  right: 0;  transition-duration: 250ms;  transition-property: opacity;  &.transition-enter {    opacity: 0;  }  &.transition-enter-active {    opacity: 1;  }  &.transition-enter-done {    opacity: 1;  }  &.transition-exit {    opacity: 1;  }  &.transition-exit-active {    opacity: 0;  }`;// Transition classes now provided by globals.css
modified pages/_document.js
@@ -1,37 +1,9 @@import React from "react";import Document, { Html, Head, Main, NextScript } from "next/document";import { ServerStyleSheet } from "styled-components";import { theme } from "../site.config";class MyDocument extends Document {  static async getInitialProps(ctx) {    // Setup styled-components for rendering server side    const sheet = new ServerStyleSheet();    const originalRenderPage = ctx.renderPage;    try {      ctx.renderPage = () =>        originalRenderPage({          enhanceApp: (App) => (props) =>            sheet.collectStyles(<App {...props} />),        });      const initialProps = await Document.getInitialProps(ctx);      return {        ...initialProps,        styles: (          <>            {initialProps.styles}            {sheet.getStyleElement()}          </>        ),      };    } finally {      sheet.seal();    }  }  render() {    return (      <Html lang="en">
modified pages/about.js
@@ -1,8 +1,8 @@import React, { useState, useEffect, useRef } from "react";import styled, { keyframes } from "styled-components";import { TransitionGroup, CSSTransition } from "react-transition-group";import Page from "../components/page";import styles from "./about.module.css";const About = () => {  const [words, setWords] = useState([
@@ -53,8 +53,8 @@ const About = () => {  return (    <Page title="About" description="A brief professional history of myself.">      <Background />      <Words>      <div className={styles.background} />      <div className={styles.words}>        <TransitionGroup component={null}>          {words.map((word) => {            return (
@@ -62,26 +62,39 @@ const About = () => {                key={word}                timeout={1000}                appear                classNames="transition"                classNames={{                  appearActive: styles.wordAppearActive,                  appearDone: styles.wordAppearDone,                  enterActive: styles.wordEnterActive,                  enterDone: styles.wordEnterDone,                  exitActive: styles.wordExitActive,                }}              >                <Word>                  <WordText>{word}</WordText>                </Word>                <h2 className={styles.word}>                  <span className={styles.wordText}>{word}</span>                </h2>              </CSSTransition>            );          })}        </TransitionGroup>      </Words>      <Paragraph>        I am a <Strong>senior solutions architect</Strong>. I enjoy working on        everything from linux kernel modules to website front-ends. My first job        was <Strong>modifying kernel modules</Strong> for Digium Telephony Cards        to work on CentOS. I am currently doing a bit of everything — chiefly        creating <Strong>custom software</Strong> on a variety of platforms,        mostly the web. I have done consulting for many companies on SEO, Online        Advertising, Social Media, Cloud Services, Security, HIPAA &amp; PCI        Compliance, and myriad other topics.        <Resume href="/static/pdfs/resume-isaac-bythewood.pdf" target="_blank">      </div>      <p className={styles.paragraph}>        I am a <span className={styles.strong}>senior solutions architect</span>        . I enjoy working on everything from linux kernel modules to website        front-ends. My first job was{" "}        <span className={styles.strong}>modifying kernel modules</span> for        Digium Telephony Cards to work on CentOS. I am currently doing a bit of        everything — chiefly creating{" "}        <span className={styles.strong}>custom software</span> on a variety of        platforms, mostly the web. I have done consulting for many companies on        SEO, Online Advertising, Social Media, Cloud Services, Security, HIPAA        &amp; PCI Compliance, and myriad other topics.        <a          className={styles.resume}          href="/static/pdfs/resume-isaac-bythewood.pdf"          target="_blank"          rel="noopener noreferrer"        >          <svg            xmlns="http://www.w3.org/2000/svg"            width="20"
@@ -95,218 +108,10 @@ const About = () => {            />          </svg>          My Resume        </Resume>      </Paragraph>        </a>      </p>    </Page>  );};export default About;const SlideLeft = keyframes`  0% {    left: -100%;  }  40% {    left: 0;  }  60% {    left: 0;  }  100% {    left: 100%;  }`;const QuickFadeIn = keyframes`  0% {    color: rgba(0, 0, 0, 0);  }  49% {    color: rgba(0, 0, 0, 0);  }  50% {    color: rgba(0, 0, 0, 1);  }  100% {    color: rgba(0, 0, 0, 1);  }`;const FadeIn = keyframes`  0% {    color: rgba(0, 0, 0, 0);  }  100% {    color: rgba(0, 0, 0, 1);  }`;const Background = styled.div`  position: absolute;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;`;const Words = styled.div`  position: absolute;  z-index: -1;  top: 0;  bottom: 0;  left: 60px;  opacity: 0.05;  font-size: 5vh;  line-height: 11vh;  color: black;  text-transform: uppercase;  height: 100vh;  overflow: hidden;  @media (${(props) => props.theme.breakpoints.mobile}) {    top: 50px;    font-size: 4vh;    line-height: 10vh;    left: calc(10px + 5%);  }`;const Word = styled.h2`  margin: 0;  &.transition-appear-active,  &.transition-enter-active {    & > span {      animation-name: ${QuickFadeIn};      animation-duration: 1000ms;      &::before {        animation-name: ${SlideLeft};        animation-duration: 1000ms;      }    }  }  &.transition-exit-active {    & > span {      animation-name: ${QuickFadeIn};      animation-duration: 1000ms;      animation-direction: reverse;      &::before {        animation-name: ${SlideLeft};        animation-duration: 1000ms;        animation-direction: reverse;      }    }  }  &.transition-appear-done,  &.transition-enter-done {    & > span {      color: rgba(0, 0, 0, 1);    }  }`;const WordText = styled.span`  position: relative;  white-space: nowrap;  overflow: hidden;  color: rgba(0, 0, 0, 0);  display: inline-block;  vertical-align: top;  &::before {    content: "";    position: absolute;    width: 100%;    height: 100%;    background-color: black;    left: -100%;    z-index: 1;  }`;const Paragraph = styled.p`  font-size: 1.8em;  font-weight: 300;  color: black;  position: relative;  color: rgba(0, 0, 0, 0);  animation-name: ${FadeIn};  animation-delay: 1500ms;  animation-duration: 1000ms;  animation-fill-mode: forwards;  &::before {    content: "";    display: block;    width: 75px;    height: 5px;    margin-bottom: 20px;    background-color: ${(props) => props.theme.colors.blue};  }  @media (${(props) => props.theme.breakpoints.mobile}) {    font-size: 1.2em;  }`;const Strong = styled.strong`  font-weight: 700;  position: relative;  white-space: nowrap;  overflow: hidden;  animation: ${QuickFadeIn} 1250ms normal forwards;  color: rgba(0, 0, 0, 0);  display: inline-block;  vertical-align: top;  &::before {    content: "";    position: absolute;    width: 100%;    height: 100%;    background-color: black;    left: -100%;    z-index: 1;    animation: ${SlideLeft} 1250ms normal forwards;  }`;const Resume = styled.a`  display: block;  text-decoration: none;  margin-top: 20px;  font-weight: 700;  color: black;  position: relative;  overflow: hidden;  &::before {    content: "";    display: block;    position: absolute;    z-index: -1;    top: 100%;    width: 100%;    height: 100%;    background: rgba(14, 64, 244, 0.3);    transition: top 250ms;  }  &:hover {    &::before {      top: 60%;    }  }  svg {    margin-right: 10px;  }`;
added pages/about.module.css
@@ -0,0 +1,206 @@/* About page styles migrated from styled-components */@keyframes slideLeft {  0% {    left: -100%;  }  40% {    left: 0;  }  60% {    left: 0;  }  100% {    left: 100%;  }}@keyframes quickFadeIn {  0% {    color: rgba(0, 0, 0, 0);  }  49% {    color: rgba(0, 0, 0, 0);  }  50% {    color: rgba(0, 0, 0, 1);  }  100% {    color: rgba(0, 0, 0, 1);  }}@keyframes fadeIn {  0% {    color: rgba(0, 0, 0, 0);  }  100% {    color: rgba(0, 0, 0, 1);  }}.background {  position: absolute;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;}.words {  position: absolute;  z-index: -1;  top: 0;  bottom: 0;  left: 60px;  opacity: 0.05;  font-size: 5vh;  line-height: 11vh;  color: black;  text-transform: uppercase;  height: 100vh;  overflow: hidden;}@media (max-width: 767.98px) {  .words {    top: 50px;    font-size: 4vh;    line-height: 10vh;    left: calc(10px + 5%);  }}.word {  margin: 0;}.wordText {  position: relative;  white-space: nowrap;  overflow: hidden;  color: rgba(0, 0, 0, 0);  display: inline-block;  vertical-align: top;}.wordText::before {  content: "";  position: absolute;  width: 100%;  height: 100%;  background-color: black;  left: -100%;  z-index: 1;}/* Transition mappings for word animations */.wordAppearActive > .wordText,.wordEnterActive > .wordText {  animation-name: quickFadeIn;  animation-duration: 1000ms;}.wordAppearActive > .wordText::before,.wordEnterActive > .wordText::before {  animation-name: slideLeft;  animation-duration: 1000ms;}.wordExitActive > .wordText {  animation-name: quickFadeIn;  animation-duration: 1000ms;  animation-direction: reverse;}.wordExitActive > .wordText::before {  animation-name: slideLeft;  animation-duration: 1000ms;  animation-direction: reverse;}.wordAppearDone > .wordText,.wordEnterDone > .wordText {  color: rgba(0, 0, 0, 1);}.paragraph {  font-size: 1.8em;  font-weight: 300;  color: black;  position: relative;  color: rgba(0, 0, 0, 0);  animation-name: fadeIn;  animation-delay: 1500ms;  animation-duration: 1000ms;  animation-fill-mode: forwards;}.paragraph::before {  content: "";  display: block;  width: 75px;  height: 5px;  margin-bottom: 20px;  background-color: var(--color-blue);}@media (max-width: 767.98px) {  .paragraph {    font-size: 1.2em;  }}.strong {  font-weight: 700;  position: relative;  white-space: nowrap;  overflow: hidden;  animation: quickFadeIn 1250ms normal forwards;  color: rgba(0, 0, 0, 0);  display: inline-block;  vertical-align: top;}.strong::before {  content: "";  position: absolute;  width: 100%;  height: 100%;  background-color: black;  left: -100%;  z-index: 1;  animation: slideLeft 1250ms normal forwards;}.resume {  display: block;  text-decoration: none;  margin-top: 20px;  font-weight: 700;  color: black;  position: relative;  overflow: hidden;}.resume::before {  content: "";  display: block;  position: absolute;  z-index: -1;  top: 100%;  width: 100%;  height: 100%;  background: rgba(14, 64, 244, 0.3);  transition: top 250ms;}.resume:hover::before {  top: 60%;}.resume svg {  margin-right: 10px;}
modified pages/art.js
@@ -1,5 +1,5 @@import React, { useState } from "react";import styled from "styled-components";import styles from "./art.module.css";import Image from "next/image";import Page from "../components/page";
@@ -30,19 +30,20 @@ const Art = () => {  return (    <Page title="Art" description="Some of my art... what even is art...">      <Background />      <Heading>Acrylic Pours</Heading>      <Paragraph>      <div className={styles.background} />      <h1 className={styles.heading}>Acrylic Pours</h1>      <p className={styles.paragraph}>        A bit more traditional than my usual art, acrylics mixed with water,        glue, and silicone on canvas and hit with a heat gun.      </Paragraph>      <Cards>        <Card      </p>      <div className={styles.artGrid}>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/006.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/006.webp"              alt="Molten Copper"
@@ -51,17 +52,18 @@ const Art = () => {              className="mouse-activate"              priority={true}            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Molten Copper <span>006</span>          </CardHeading>        </Card>        <Card          </h2>        </div>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/005.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/005.webp"              alt="Nebulas in Triangulum"
@@ -70,17 +72,18 @@ const Art = () => {              className="mouse-activate"              priority={true}            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Nebulas in Triangulum <span>005</span>          </CardHeading>        </Card>        <Card          </h2>        </div>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/004.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/004.webp"              alt="Metal on Mars"
@@ -89,17 +92,18 @@ const Art = () => {              className="mouse-activate"              priority={true}            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Metal on Mars <span>004</span>          </CardHeading>        </Card>        <Card          </h2>        </div>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/003.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/003.webp"              alt="Water on Jupiter"
@@ -108,17 +112,18 @@ const Art = () => {              className="mouse-activate"              priority={true}            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Water on Jupiter <span>003</span>          </CardHeading>        </Card>        <Card          </h2>        </div>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/002.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/002.webp"              alt="Cracks in Clay"
@@ -126,17 +131,18 @@ const Art = () => {              height={360}              className="mouse-activate"            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Cracks in Clay <span>002</span>          </CardHeading>        </Card>        <Card          </h2>        </div>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/001.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/001.webp"              alt="Reef Drop-off"
@@ -144,17 +150,18 @@ const Art = () => {              height={360}              className="mouse-activate"            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Reef Drop-off <span>001</span>          </CardHeading>        </Card>        <Card          </h2>        </div>        <div          className={styles.artItem}          onClick={() =>            openLightbox("/static/images/art/acrylic-pours/000.webp")          }        >          <CardImage>          <span className={styles.cardImage}>            <Image              src="/static/images/art/acrylic-pours/000.webp"              alt="Blood in Waves"
@@ -162,337 +169,126 @@ const Art = () => {              height={360}              className="mouse-activate"            />          </CardImage>          <CardHeading>          </span>          <h2 className={styles.artItemHeader}>            Blood in Waves <span>000</span>          </CardHeading>        </Card>      </Cards>      <Heading>Emergent Generative Art</Heading>      <Paragraph>          </h2>        </div>      </div>      <h1 className={styles.heading}>Emergent Generative Art</h1>      <p className={styles.paragraph}>        Autonomously generated entities that are observed to have qualities in a        group that they do not have on their own.      </Paragraph>      <Subheading>      </p>      <h2 className={styles.subheading}>        <span>000</span> Constellations      </Subheading>      <Paragraph>      </h2>      <p className={styles.paragraph}>        Moving stars, circles, on a canvas that attach to nearby stars, with a        line, to generate constellations. Line opacity is based on star        distance.      </Paragraph>      <ArtContainer>      </p>      <div className={styles.artContainer}>        <Constellations          options={{ numStars: 50, isActive: activeArt === "constellations" }}        />        <PlayButton        <button          className={styles.playButton}          onClick={() => handleArtToggle("constellations")}          active={activeArt === "constellations"}          data-active={activeArt === "constellations"}        >          {activeArt === "constellations" ? "⏸" : "▶"}        </PlayButton>      </ArtContainer>      <Link        </button>      </div>      <a        className={styles.artItemButton}        href="https://github.com/overshard/isaacbythewood.com/blob/master/components/constellations.js"        rel="noopener noreferrer"        target="_blank"      >        See the Code      </Link>      <Subheading>      </a>      <h2 className={styles.subheading}>        <span>001</span> Retro Stars      </Subheading>      <Paragraph>      </h2>      <p className={styles.paragraph}>        Multiple parallax planes of stars that shift based on cursor position.        Inspired by the retro art style of Celeste.      </Paragraph>      <ArtContainer>      </p>      <div className={styles.artContainer}>        <RetroStars          options={{ numStars: 50, isActive: activeArt === "retrostars" }}        />        <PlayButton        <button          className={styles.playButton}          onClick={() => handleArtToggle("retrostars")}          active={activeArt === "retrostars"}          data-active={activeArt === "retrostars"}        >          {activeArt === "retrostars" ? "⏸" : "▶"}        </PlayButton>      </ArtContainer>      <Link        </button>      </div>      <a        className={styles.artItemButton}        href="https://github.com/overshard/isaacbythewood.com/blob/master/components/retrostars.js"        rel="noopener noreferrer"        target="_blank"      >        See the Code      </Link>      <Subheading>      </a>      <h2 className={styles.subheading}>        <span>002</span> Particle Flow      </Subheading>      <Paragraph>      </h2>      <p className={styles.paragraph}>        Colorful particles flowing through an invisible force field, creating        organic, flowing patterns with trailing effects. Each particle follows        the field while leaving a colorful trail that slowly fades.      </Paragraph>      <ArtContainer>      </p>      <div className={styles.artContainer}>        <ParticleFlow          options={{ numParticles: 80, isActive: activeArt === "particleflow" }}        />        <PlayButton        <button          className={styles.playButton}          onClick={() => handleArtToggle("particleflow")}          active={activeArt === "particleflow"}          data-active={activeArt === "particleflow"}        >          {activeArt === "particleflow" ? "⏸" : "▶"}        </PlayButton>      </ArtContainer>      <Link        </button>      </div>      <a        className={styles.artItemButton}        href="https://github.com/overshard/isaacbythewood.com/blob/master/components/particleflow.js"        rel="noopener noreferrer"        target="_blank"      >        See the Code      </Link>      </a>      {lightboxImage !== null && (        <Lightbox onClick={() => closeLightbox()}>          <LightboxLoading className={lightboxLoaded && "hide"}>        <div className={styles.lightboxOverlay} onClick={() => closeLightbox()}>          <div            className={              styles.lightboxLoading + (lightboxLoaded ? " " + styles.hide : "")            }          >            Loading...          </LightboxLoading>          <LightboxImage            src={lightboxImage}            className={lightboxLoaded && "show"}            onLoad={() => setLightboxLoaded(true)}          />        </Lightbox>          </div>          <span className={styles.lightboxImageWrapper}>            <Image              className={                styles.lightboxImage + (lightboxLoaded ? " " + styles.show : "")              }              src={lightboxImage}              alt="Lightbox"              layout="fill"              sizes="90vw"              objectFit="contain"              onLoad={() => setLightboxLoaded(true)}            />          </span>        </div>      )}    </Page>  );};export default Art;const Background = styled.div`  position: absolute;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;`;const Heading = styled.h1`  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2.5em;  color: black;  &::before {    content: "";    display: block;    width: 50px;    height: 5px;    margin-bottom: 20px;    background-color: ${(props) => props.theme.colors.blue};  }`;const Subheading = styled.h2`  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2em;  color: black;  position: relative;  & > span {    font-family: monospace;    font-size: 0.8em;    padding: 3px 9px;    background-color: black;    color: white;    position: absolute;    right: 100%;    margin-right: 10px;    top: 20%;    @media (${(props) => props.theme.breakpoints.tablet}) {      display: none;    }  }`;const Paragraph = styled.p`  font-size: 1.5em;  margin-top: 0;  margin-bottom: 20px;  font-weight: 300;  color: black;`;const ArtContainer = styled.div`  width: 100%;  height: 500px;  max-height: 100vh;  background: black;  margin-bottom: 20px;  position: relative;  overflow: hidden;`;const Link = styled.a`  padding: 10px 15px;  font-size: 1.2em;  text-decoration: none;  background-color: black;  margin-bottom: 20px;  font-weight: 700;  text-transform: uppercase;  display: inline-block;  letter-spacing: 2px;  color: white;  background-image: linear-gradient(    to right,    ${(props) => props.theme.colors.blue} 0,    ${(props) => props.theme.colors.purple} 100%  );  transform: scale(1);  transition-duration: 250ms;  transition-property: transform;  &:hover {    transform: scale(1.2);  }`;const Cards = styled.div`  display: flex;  width: 100%;  flex-wrap: wrap;  gap: 20px;`;const Card = styled.div`  background-color: black;  color: white;  box-shadow: 0 5px 0 rgba(0, 0, 0, 0);  transition-duration: 200ms;  transition-property: box-shadow;  transition-timing-function: ease-in;  width: calc(50% - 20px);  display: flex;  flex-direction: column;  &:hover {    box-shadow: 0 5px 25px rgba(0, 0, 0, 0.4);  }  @media (${(props) => props.theme.breakpoints.mobile}) {    width: 100%;  }`;const CardHeading = styled.h2`  margin: 0;  margin-top: -5px;  padding: 10px;  display: flex;  justify-content: space-between;  align-items: center;  font-weight: 900;  & > span {    color: black;    font-family: monospace;    font-weight: 100;    font-size: 0.9em;    background-color: white;    padding: 3px 7px;  }`;const CardImage = styled.span`  & img {    object-fit: cover;    object-position: center;    width: 100%;    height: 360px;  }`;const Lightbox = styled.div`  width: 100vw;  height: 100vh;  position: fixed;  top: 0;  bottom: 0;  left: 0;  right: 0;  z-index: 10;  background-color: rgba(0, 0, 0, 0.7);  display: flex;  justify-content: center;  align-items: center;`;const LightboxImage = styled.img`  position: absolute;  max-width: 100%;  max-height: 100%;  padding: 5vw;  box-sizing: border-box;  visibility: hidden;  opacity: 0;  transition-property: opacity;  transition-duration: 250ms;  &.show {    opacity: 1;    visibility: visible;  }`;const LightboxLoading = styled.div`  position: absolute;  max-width: 100%;  max-height: 100%;  box-sizing: border-box;  color: white;  font-family: monospace;  font-size: 3em;  visibility: visible;  opacity: 1;  transition-property: opacity;  transition-duration: 250ms;  &.hide {    opacity: 0;    visibility: hidden;  }`;const PlayButton = styled.button`  position: absolute;  top: 20px;  right: 20px;  width: 60px;  height: 60px;  border: none;  background: transparent;  color: white;  font-size: 32px;  cursor: pointer;  display: flex;  align-items: center;  justify-content: center;  transition: all 0.3s ease;  z-index: 5;  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);  &:hover {    transform: scale(1.1);    text-shadow: 0 4px 8px rgba(0, 0, 0, 0.9);  }  &:active {    transform: scale(0.95);  }`;
added pages/art.module.css
@@ -0,0 +1,318 @@/* Art page styles migrated from styled-components */@keyframes transformRight {  from {    transform: scaleX(0);  }  to {    transform: scaleX(1);  }}@keyframes transformLeft {  from {    transform: scaleX(1);  }  to {    transform: scaleX(0);  }}.background {  position: absolute;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;}.heading {  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2.5em;  color: black;}.heading::before {  content: "";  display: block;  width: 50px;  height: 5px;  margin-bottom: 20px;  background-color: var(--color-blue);}.paragraph {  font-size: 1.5em;  margin-top: 0;  margin-bottom: 20px;  font-weight: 300;  color: black;}.subheading {  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2em;  color: black;  position: relative;}.subheading > span {  font-family: monospace;  font-size: 0.8em;  padding: 3px 9px;  background-color: black;  color: white;  position: absolute;  right: 100%;  margin-right: 10px;  top: 20%;}@media (max-width: 1023.98px) {  .subheading > span {    display: none;  }}.paragraph a {  color: black;  text-decoration: none;  position: relative;  white-space: nowrap;}.paragraph a::before {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: 0;  height: 2px;  background: rgba(0, 0, 0, 1);  transform-origin: left;  animation: transformLeft 300ms normal forwards;}.paragraph a::after {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: 0;  height: 2px;  background: rgba(0, 0, 0, 0.2);}.paragraph a:hover::before {  animation: transformRight 300ms normal forwards;}.artGrid {  display: grid;  grid-template-columns: repeat(2, 1fr);  gap: 20px;}@media (max-width: 1023.98px) {  .artGrid {    grid-template-columns: repeat(2, 1fr);  }}@media (max-width: 767.98px) {  .artGrid {    grid-template-columns: 1fr;  }}.artItem {  background-color: black;  color: white;  box-shadow: 0 5px 0 rgba(0, 0, 0, 0);  transition-duration: 200ms;  transition-property: box-shadow;  transition-timing-function: ease-in;}.artItem:hover {  box-shadow: 0 5px 25px rgba(0, 0, 0, 0.4);}.artItemHeader {  margin: 0;  margin-top: -5px;  padding: 10px;  display: flex;  justify-content: space-between;  align-items: center;  font-weight: 900;}.artItemHeader > span {  color: black;  font-family: monospace;  font-weight: 100;  font-size: 0.9em;  background-color: white;  padding: 3px 7px;}.artItemTitle {  font-weight: 700;}.artItemButton {  padding: 10px 15px;  font-size: 1.2em;  text-decoration: none;  background-color: black;  margin-bottom: 20px;  font-weight: 700;  text-transform: uppercase;  display: inline-block;  letter-spacing: 2px;  color: white;  background-image: linear-gradient(    to right,    var(--color-blue) 0,    var(--color-purple) 100%  );  transform: scale(1);  transition-duration: var(--transition-250);  transition-property: transform;}.artItemButton:hover {  transform: scale(1.2);}.artItemImageWrapper {  position: relative;  width: 100%;  padding-top: 66.6667%;}.artItemImage {  position: absolute;  top: 0;  left: 0;  width: 100%;  height: 100%;  object-fit: cover;}/* Card image wrapper used in JSX as styles.cardImage */.cardImage img {  object-fit: cover;  object-position: center;  width: 100%;  height: 360px;}.artContainer {  width: 100%;  height: 500px;  max-height: 100vh;  background: black;  margin-bottom: 20px;  position: relative;  overflow: hidden;}.playButton {  position: absolute;  top: 20px;  right: 20px;  width: 60px;  height: 60px;  border: none;  background: transparent;  color: white;  font-size: 32px;  cursor: pointer;  display: flex;  align-items: center;  justify-content: center;  transition: all 0.3s ease;  z-index: 5;  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);}.playButton:hover {  transform: scale(1.1);  text-shadow: 0 4px 8px rgba(0, 0, 0, 0.9);}.playButton:active {  transform: scale(0.95);}.lightboxOverlay {  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  background: rgba(0, 0, 0, 0.85);  display: flex;  align-items: center;  justify-content: center;  z-index: 1000;  opacity: 1;  pointer-events: auto;}.lightboxImage {  visibility: hidden;  opacity: 0;  transition-property: opacity;  transition-duration: var(--transition-250);}.lightboxLoading {  position: absolute;  max-width: 100%;  max-height: 100%;  box-sizing: border-box;  color: white;  font-family: monospace;  font-size: 3em;  visibility: visible;  opacity: 1;  transition-property: opacity;  transition-duration: var(--transition-250);}.show {  opacity: 1;  visibility: visible;}.hide {  opacity: 0;  visibility: hidden;}/* Lightbox image wrapper ensures centering and sizing for Next/Image with layout=fill */.lightboxImageWrapper {  position: relative;  width: 90vw;  height: 90vh;}.closeButton {  position: absolute;  top: 20px;  right: 20px;  background: transparent;  border: none;  color: white;  font-size: 2rem;  cursor: pointer;}
modified pages/code.js
@@ -1,6 +1,6 @@import React from "react";import styled, { keyframes } from "styled-components";import PropTypes from "prop-types";import styles from "./code.module.css";import Page from "../components/page";
@@ -21,9 +21,9 @@ const GitHubIcon = () => {const Code = ({ commits }) => {  return (    <Page title="Code" description="Some of my most recent coding projects.">      <Background />      <Heading>Code</Heading>      <Paragraph>      <div className={styles.background} />      <h1 className={styles.heading}>Code</h1>      <p className={styles.paragraph}>        A probably not entirely up-to-date list of my current side projects...        There is plenty more to see on{" "}        <a
@@ -34,187 +34,197 @@ const Code = ({ commits }) => {          my GitHub account        </a>{" "}        and generally around the internet if you are interested.      </Paragraph>      <Projects>        <Project>          <ProjectHeading>Analytics</ProjectHeading>          <ProjectParagraph>      </p>      <div className={styles.projects}>        <div className={styles.project}>          <h1 className={styles.projectHeading}>Analytics</h1>          <p className={styles.projectParagraph}>            A self-hostable analytics service with a straightforward API to            track events from any source.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.analytics.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/analytics"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>Status</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>Status</h1>          <p className={styles.projectParagraph}>            A self-hosted status monitoring service.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.status.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/status"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>Blog</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>Blog</h1>          <p className={styles.projectParagraph}>            A self-hostable blog built on Wagtail targeted towards developers            with code blocks, syntax highlighting, live search, great SEO, and a            clean customizable UI.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.blog.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/blog"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>Timelite</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>Timelite</h1>          <p className={styles.projectParagraph}>            A simple time tracking progressive web app. Uses local storage and            service workers to remain accessible offline. Sometimes you just            need the essentials when you are busy.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.timelite.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/timelite"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>Timestrap</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>Timestrap</h1>          <p className={styles.projectParagraph}>            A full feature time tracking web app. Supports multiple users and            exporting reports in multiple formats. Makes use of websockets to            maintain state across clients.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.timestrap.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/timestrap"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>isaacbythewood.com</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>isaacbythewood.com</h1>          <p className={styles.projectParagraph}>            The personal website of Isaac Bythewood. So this site...          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.isaacbythewood.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/isaacbythewood.com"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>New Tab</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>New Tab</h1>          <p className={styles.projectParagraph}>            A clean new tab page extension for Chrome.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.newtab.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/newtab"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>dockerfiles</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>dockerfiles</h1>          <p className={styles.projectParagraph}>            All the Dockerfiles I use for various purposes. More detailed usage            instructions are at the top of each Dockerfile.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.dockerfiles.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/dockerfiles"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>alpinefiles</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>alpinefiles</h1>          <p className={styles.projectParagraph}>            Some of the files that I use on my Alpine Linux servers.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.alpinefiles.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/alpinefiles"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>        <Project>          <ProjectHeading>dotfiles</ProjectHeading>          <ProjectParagraph>          </a>        </div>        <div className={styles.project}>          <h1 className={styles.projectHeading}>dotfiles</h1>          <p className={styles.projectParagraph}>            A variety of config files for setting up new systems.          </ProjectParagraph>          <ProjectCommit>          </p>          <pre className={styles.projectCommit}>            {JSON.stringify(commits.dotfiles.data, null, 2)}          </ProjectCommit>          <ProjectButton          </pre>          <a            className={styles.projectButton}            href="https://www.github.com/overshard/dotfiles"            rel="noopener noreferrer"            target="_blank"          >            <GitHubIcon />            GitHub          </ProjectButton>        </Project>      </Projects>          </a>        </div>      </div>    </Page>  );};
@@ -355,170 +365,3 @@ Code.propTypes = {};export default Code;const TransformRight = keyframes`  from {    transform: scaleX(0);  }  to {    transform: scaleX(1);  }`;const TransformLeft = keyframes`  from {    transform: scaleX(1);  }  to {    transform: scaleX(0);  }`;const Background = styled.div`  position: absolute;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;`;const Heading = styled.h1`  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2.5em;  color: black;  &::before {    content: "";    display: block;    width: 50px;    height: 5px;    margin-bottom: 20px;    background-color: ${(props) => props.theme.colors.blue};  }`;const Paragraph = styled.p`  font-size: 1.5em;  margin-top: 0;  margin-bottom: 20px;  font-weight: 300;  color: black;  a {    color: black;    text-decoration: none;    position: relative;    white-space: nowrap;    &::before {      content: "";      position: absolute;      left: 0;      right: 0;      bottom: 0;      height: 2px;      background: rgba(0, 0, 0, 1);      transform-origin: left;      animation: ${TransformLeft} 300ms normal forwards;    }    &::after {      content: "";      position: absolute;      left: 0;      right: 0;      bottom: 0;      height: 2px;      background: rgba(0, 0, 0, 0.2);    }    &:hover {      &::before {        animation: ${TransformRight} 300ms normal forwards;      }    }  }`;const Projects = styled.div`  display: flex;  width: 100%;  flex-wrap: wrap;  gap: 20px;`;const Project = styled.div`  width: calc(50% - 20px);  display: flex;  flex-direction: column;  @media (${(props) => props.theme.breakpoints.mobile}) {    width: 100%;  }`;const ProjectHeading = styled.h1`  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2em;  color: black;`;const ProjectParagraph = styled.p`  font-size: 1.5em;  margin-top: 0;  margin-bottom: 20px;  font-weight: 300;  color: black;  flex-grow: 1;`;const ProjectButton = styled.a`  padding: 10px 15px;  font-size: 1.2em;  text-decoration: none;  background-color: black;  margin-bottom: 20px;  font-weight: 700;  text-transform: uppercase;  display: inline-block;  letter-spacing: 2px;  color: white;  background-image: linear-gradient(    to right,    ${(props) => props.theme.colors.blue} 0,    ${(props) => props.theme.colors.purple} 100%  );  transform: scale(1);  transition-duration: 250ms;  transition-property: transform;  width: 125px;  text-align: center;  display: flex;  align-items: center;  & svg {    margin-right: 10px;  }  &:hover {    transform: scale(1.2);  }`;const ProjectCommit = styled.pre`  font-family: monospace;  background: black;  color: #00ff00;  padding: 20px;  overflow-x: hidden;  max-width: 100%;  text-overflow: ellipsis;`;
added pages/code.module.css
@@ -0,0 +1,166 @@/* Code page styles migrated from styled-components */@keyframes transformRight {  from {    transform: scaleX(0);  }  to {    transform: scaleX(1);  }}@keyframes transformLeft {  from {    transform: scaleX(1);  }  to {    transform: scaleX(0);  }}.background {  position: absolute;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;}.heading {  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2.5em;  color: black;}.heading::before {  content: "";  display: block;  width: 50px;  height: 5px;  margin-bottom: 20px;  background-color: var(--color-blue);}.paragraph {  font-size: 1.5em;  margin-top: 0;  margin-bottom: 20px;  font-weight: 300;  color: black;}.paragraph a {  color: black;  text-decoration: none;  position: relative;  white-space: nowrap;}.paragraph a::before {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: 0;  height: 2px;  background: rgba(0, 0, 0, 1);  transform-origin: left;  animation: transformLeft 300ms normal forwards;}.paragraph a::after {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: 0;  height: 2px;  background: rgba(0, 0, 0, 0.2);}.paragraph a:hover::before {  animation: transformRight 300ms normal forwards;}.projects {  display: flex;  width: 100%;  flex-wrap: wrap;  gap: 20px;}.project {  width: calc(50% - 20px);  display: flex;  flex-direction: column;}@media (max-width: 767.98px) {  .project {    width: 100%;  }}.projectHeading {  font-weight: 700;  margin-top: 60px;  margin-bottom: 20px;  font-size: 2em;  color: black;}.projectParagraph {  font-size: 1.5em;  margin-top: 0;  margin-bottom: 20px;  font-weight: 300;  color: black;  flex-grow: 1;}.projectButton {  padding: 10px 15px;  font-size: 1.2em;  text-decoration: none;  background-color: black;  margin-bottom: 20px;  font-weight: 700;  text-transform: uppercase;  display: inline-flex;  align-items: center;  justify-content: center;  letter-spacing: 2px;  color: white;  background-image: linear-gradient(    to right,    var(--color-blue) 0,    var(--color-purple) 100%  );  transform: scale(1);  transition-duration: 250ms;  transition-property: transform;  width: 125px;  text-align: center;}.projectButton svg {  margin-right: 10px;}.projectButton:hover {  transform: scale(1.2);}.projectCommit {  font-family: monospace;  background: black;  color: #00ff00;  padding: 20px;  overflow-x: hidden;  max-width: 100%;  text-overflow: ellipsis;}
modified pages/contact.js
@@ -1,6 +1,7 @@import React from "react";import styled, { keyframes } from "styled-components";import { TransitionGroup, CSSTransition } from "react-transition-group";import styles from "./contact.module.css";import Image from "next/image";import Page from "../components/page";
@@ -19,55 +20,61 @@ const Contact = () => {      description="How to get in contact with me."      gridArea="1 / 1 / 4 / 7"    >      <Background />      <Grid>        <GridLeft>          <ContactWrapper>            <Heading>Contact Me</Heading>            <ShoutOut>I love chatting</ShoutOut>            <ShoutOut>with other developers!</ShoutOut>            <ContactList>              <ContactKey>Email</ContactKey>              <ContactValue>                <ContactLink href="mailto:isaac@bythewood.me">      <div className={styles.background} />      <div className={styles.grid}>        <div className={styles.gridLeft}>          <div className={styles.contactWrapper}>            <h1 className={styles.heading}>Contact Me</h1>            <h2 className={styles.shoutOut}>I love chatting</h2>            <h2 className={styles.shoutOut}>with other developers!</h2>            <dl className={styles.contactList}>              <dt className={styles.contactKey}>Email</dt>              <dd className={styles.contactValue}>                <a                  className={styles.contactLink}                  href="mailto:isaac@bythewood.me"                >                  isaac@bythewood.com                </ContactLink>              </ContactValue>              <ContactKey>LinkedIn</ContactKey>              <ContactValue>                <ContactLink                </a>              </dd>              <dt className={styles.contactKey}>LinkedIn</dt>              <dd className={styles.contactValue}>                <a                  className={styles.contactLink}                  href="https://www.linkedin.com/in/isaac-bythewood/"                  rel="noopener noreferrer"                  target="_blank"                >                  Isaac Bythewood                </ContactLink>              </ContactValue>              <ContactKey>GitHub</ContactKey>              <ContactValue>                <ContactLink                </a>              </dd>              <dt className={styles.contactKey}>GitHub</dt>              <dd className={styles.contactValue}>                <a                  className={styles.contactLink}                  href="https://github.com/overshard"                  rel="noopener noreferrer"                  target="_blank"                >                  /overshard                </ContactLink>              </ContactValue>              <ContactKey>Discord</ContactKey>              <ContactValue>                <ContactLink                </a>              </dd>              <dt className={styles.contactKey}>Discord</dt>              <dd className={styles.contactValue}>                <a                  className={styles.contactLink}                  href="https://discordapp.com/"                  rel="noopener noreferrer"                  target="_blank"                >                  Overshard#4907                </ContactLink>              </ContactValue>            </ContactList>          </ContactWrapper>        </GridLeft>        <GridRight>          <Chat>                </a>              </dd>            </dl>          </div>        </div>        <div className={styles.gridRight}>          <div className={styles.chat}>            <TransitionGroup component={null}>              {chatMessages.map((message, index) => {                const transitionTimeout = (index + 2) * 2000;
@@ -79,277 +86,32 @@ const Contact = () => {                    timeout={{ appear: transitionTimeout }}                    classNames="fade"                  >                    <ChatLine                    <div                      className={styles.chatLine}                      style={{ transitionDelay: `${transitionDelay}ms` }}                    >                      <ChatAvatar                        src="/static/images/avatar.webp"                        alt="Chat Avatar"                      />                      <ChatMessage>                      <span className={styles.chatAvatar}>                        <Image                          src="/static/images/avatar.webp"                          alt="Chat Avatar"                          width={50}                          height={50}                        />                      </span>                      <div className={styles.chatBubble}>                        {message}                        <span>Isaac</span>                      </ChatMessage>                    </ChatLine>                      </div>                    </div>                  </CSSTransition>                );              })}            </TransitionGroup>          </Chat>        </GridRight>      </Grid>          </div>        </div>      </div>    </Page>  );};export default Contact;const SlideUp = keyframes`  from {    opacity: 0;    transform: translateX(-100px);  }  to {    opacity: 1;    transform: translateY(0);  }`;const TransformRight = keyframes`  from {    transform: scaleX(0);  }  to {    transform: scaleX(1);  }`;const TransformLeft = keyframes`  from {    transform: scaleX(1);  }  to {    transform: scaleX(0);  }`;const Background = styled.div`  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;`;const Grid = styled.div`  display: grid;  grid-template-columns: 50vw 1fr;  grid-template-rows: auto;  grid-template-areas: "left right";  margin-left: 60px;  transform: translateX(-100vw);  animation: ${SlideUp} 750ms 500ms forwards;  @media (${(props) => props.theme.breakpoints.tablet}) {    margin-left: 0;    grid-template-columns: 1fr;    grid-template-rows: auto auto;    margin-top: 60px;  }`;const GridColumn = styled.div`  min-height: 100vh;  display: flex;  align-items: center;  @media (${(props) => props.theme.breakpoints.tablet}) {    min-height: auto;  }`;const GridLeft = styled(GridColumn)`  grid-area: left;  background-color: rgba(0, 0, 0, 0.9);  @media (${(props) => props.theme.breakpoints.tablet}) {    grid-column: 1;    grid-row: 1;  }`;const GridRight = styled(GridColumn)`  grid-area: right;  @media (${(props) => props.theme.breakpoints.tablet}) {    grid-column: 1;    grid-row: 2;  }`;const ContactWrapper = styled.div`  padding-left: 60px;  @media (${(props) => props.theme.breakpoints.tablet}) {    min-height: auto;    padding-top: 30px;    padding-bottom: 30px;  }`;const Heading = styled.h1`  font-size: 3em;  margin-top: 10px;  margin-bottom: 20px;  &::before {    content: "";    display: block;    width: 75px;    height: 5px;    margin-bottom: 10px;    background-color: ${(props) => props.theme.colors.blue};  }  @media (${(props) => props.theme.breakpoints.mobile}) {    margin-bottom: 10px;  }`;const ShoutOut = styled.h2`  text-transform: uppercase;  padding: 5px;  margin: 0 0 2px 0;  float: left;  clear: left;  font-size: 1.4em;  background-image: linear-gradient(    to right,    ${(props) => props.theme.colors.blue} 0,    ${(props) => props.theme.colors.purple} 100%  );  @media (${(props) => props.theme.breakpoints.mobile}) {    font-size: 1.1em;  }`;const ContactList = styled.dl`  padding-top: 30px;  clear: both;`;const ContactKey = styled.dt`  text-transform: uppercase;  font-size: 0.8em;  font-weight: 700;  opacity: 0.7;`;const ContactValue = styled.dd`  font-size: 1.5em;  margin-left: 0;  margin-bottom: 30px;`;const ContactLink = styled.a`  color: white;  text-decoration: none;  position: relative;  &::before {    content: "";    position: absolute;    left: 0;    right: 0;    bottom: 0;    height: 2px;    background: rgba(255, 255, 255, 1);    transform-origin: left;    animation: ${TransformLeft} 300ms normal forwards;  }  &::after {    content: "";    position: absolute;    left: 0;    right: 0;    bottom: 0;    height: 2px;    background: rgba(255, 255, 255, 0.2);  }  &:hover {    &::before {      animation: ${TransformRight} 300ms normal forwards;    }  }`;const Chat = styled.div`  margin: 30px 0;  display: flex;  flex-direction: column;`;const ChatLine = styled.div`  display: flex;  margin-bottom: 20px;  justify-content: center;  opacity: 0;  &.fade-appear {    opacity: 0;    transform: translateX(-100px);  }  &.fade-appear-active {    opacity: 1;    transform: translateX(0);    transition-duration: 250ms;    transition-property: opacity, transform;  }  &.fade-appear-done {    opacity: 1;  }`;const ChatMessage = styled.div`  font-size: 1.2em;  padding: 10px 20px;  background-color: rgba(0, 0, 0, 0.7);  width: 65%;  @media (${(props) => props.theme.breakpoints.mobile}) {    font-size: 1em;    width: 70%;  }  & span {    display: block;    font-size: 0.5em;    text-transform: uppercase;    margin-top: 5px;    color: rgba(255, 255, 255, 0.7);    letter-spacing: 1px;    font-weight: 800;  }`;const ChatAvatar = styled.img`  margin-right: 20px;  width: 50px;  height: 50px;  @media (${(props) => props.theme.breakpoints.mobile}) {    margin-right: 10px;    width: 40px;    height: 40px;  }`;
added pages/contact.module.css
@@ -0,0 +1,258 @@/* Contact page styles migrated from styled-components */@keyframes transformRight {  from {    transform: scaleX(0);  }  to {    transform: scaleX(1);  }}@keyframes transformLeft {  from {    transform: scaleX(1);  }  to {    transform: scaleX(0);  }}@keyframes slideUp {  from {    opacity: 0;    transform: translateX(-100vw);  }  to {    opacity: 1;    transform: translateX(0);  }}.background {  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  background-color: white;  z-index: -2;}.grid {  display: grid;  grid-template-columns: 50vw 1fr;  grid-template-rows: auto;  grid-template-areas: "left right";  margin-right: 60px;  transform: translateX(-100vw);  animation: slideUp 750ms 500ms forwards;}@media (max-width: 1023.98px) {  .grid {    margin-right: 0;    grid-template-columns: 1fr;    grid-template-rows: auto auto;    margin-top: 60px;  }}/* Former GridColumn shared styles applied directly to left/right columns */.gridLeft {  grid-area: left;  background-color: rgba(0, 0, 0, 0.9);  min-height: 100vh;  display: flex;  align-items: center;  padding-left: 60px;}.gridRight {  grid-area: right;  min-height: 100vh;  display: flex;  align-items: center;}@media (max-width: 1023.98px) {  .gridLeft,  .gridRight {    min-height: auto;  }}.contactWrapper {  padding-left: 60px; /* match legacy inner offset so content doesn't hug the sidebar */}@media (max-width: 1023.98px) {  .contactWrapper {    min-height: auto;    padding-top: 30px;    padding-bottom: 30px;  }}.heading {  font-size: 3em;  margin-top: 10px;  margin-bottom: 20px;}.heading::before {  content: "";  display: block;  width: 75px;  height: 5px;  margin-bottom: 10px;  background-color: var(--color-blue);}@media (max-width: 767.98px) {  .heading {    margin-bottom: 10px;  }}.shoutOut {  text-transform: uppercase;  padding: 5px;  margin: 0 0 2px 0;  float: left;  clear: left;  font-size: 1.4em;  background-image: linear-gradient(    to right,    var(--color-blue) 0,    var(--color-purple) 100%  );}@media (max-width: 767.98px) {  .shoutOut {    font-size: 1.1em;  }}.contactList {  padding-top: 30px;  clear: both;}.contactKey {  text-transform: uppercase;  font-size: 0.8em;  font-weight: 700;  opacity: 0.7;}.contactValue {  font-size: 1.5em;  margin-left: 0;  margin-bottom: 30px;}.contactLink {  color: white;  text-decoration: none;  position: relative;}.contactLink::before {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: 0;  height: 2px;  background: rgba(255, 255, 255, 1);  transform-origin: left;  animation: transformLeft 300ms normal forwards;}.contactLink::after {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: 0;  height: 2px;  background: rgba(255, 255, 255, 0.2);}.contactLink:hover::before {  animation: transformRight 300ms normal forwards;}.chat {  margin: 30px 0;  display: flex;  flex-direction: column;}.chatLine {  display: flex;  margin-bottom: 20px;  justify-content: center;  opacity: 0;}/* Fade-in transitions for chat lines (react-transition-group classNames="fade") */.chatLine:global(.fade-appear) {  opacity: 0;  transform: translateX(-100px);}.chatLine:global(.fade-appear-active) {  opacity: 1;  transform: translateX(0);  transition-property: opacity, transform;  transition-duration: 250ms;}.chatLine:global(.fade-appear-done) {  opacity: 1;}.chatBubble {  font-size: 1.2em;  padding: 10px 20px;  background-color: rgba(0, 0, 0, 0.7);  color: white;  width: 65%;}@media (max-width: 767.98px) {  .chatBubble {    font-size: 1em;    width: 70%;  }}.chatBubble span {  display: block;  font-size: 0.5em;  text-transform: uppercase;  margin-top: 5px;  color: rgba(255, 255, 255, 0.7);  letter-spacing: 1px;  font-weight: 800;}/* removed conflicting duplicate chat styles *//* Restore avatar spacing/sizing per legacy */.chatAvatar {  margin-right: 20px;  width: 50px;  height: 50px;  display: inline-flex;}@media (max-width: 767.98px) {  .chatAvatar {    margin-right: 10px;    width: 40px;    height: 40px;  }}
modified pages/index.js
@@ -1,9 +1,9 @@import React, { useEffect, useState, useRef } from "react";import styled, { keyframes } from "styled-components";import { TransitionGroup, CSSTransition } from "react-transition-group";import Image from "next/image";import Page from "../components/page";import styles from "./index.module.css";const Index = () => {  const words = ["Developer", "SysAdmin", "DevOps", "Consultant"];
@@ -26,153 +26,49 @@ const Index = () => {  return (    <Page title="Senior Solutions Architect located in Elkin, NC">      <ImageWrapper>      <div className={styles.imageWrapper}>        <Image          src="/static/images/art/acrylic-pours/005.webp"          alt="#005 Nebulas in Triangulum"          loading="eager"          layout="fill"          priority={true}          objectFit="cover"          objectPosition="center"        />      </ImageWrapper>      <TransitionGroup component={Words}>      </div>      <TransitionGroup component="div" className={styles.words}>        {currentWords.map((word) => {          return (            <CSSTransition key={word} timeout={1000} classNames="transition">              <Word>{word}</Word>            <CSSTransition              key={word}              timeout={1000}              classNames={{                appear: styles.wordAppear,                appearActive: styles.wordAppearActive,                appearDone: styles.wordAppearDone,                enter: styles.wordEnter,                enterActive: styles.wordEnterActive,                enterDone: styles.wordEnterDone,                exit: styles.wordExit,                exitActive: styles.wordExitActive,              }}              appear            >              <h3 className={styles.word}>{word}</h3>            </CSSTransition>          );        })}      </TransitionGroup>      <Description>Senior Solutions Architect located in Elkin, NC</Description>      <Name>Isaac</Name>      <Name style={{ animationDelay: "100ms" }}>Bythewood</Name>      <h1 className={styles.description}>        Senior Solutions Architect located in Elkin, NC      </h1>      <h2 className={styles.name}>Isaac</h2>      <h2 className={styles.name} style={{ animationDelay: "100ms" }}>        Bythewood      </h2>    </Page>  );};export default Index;const FadeStart = keyframes`  from {    opacity: 0;  }  to {    opacity: 1;  }`;const SlideStart = keyframes`  from {    transform: translateX(-100vw);  }  to {    transform: translateX(0);  }`;const ImageWrapper = styled.div`  img {    object-fit: cover;    object-position: center;  }`;const Words = styled.div`  position: absolute;  top: 0;  bottom: 0;  left: 0;  right: 0;  width: 100%;  height: 100vh;  display: flex;  align-items: center;  justify-content: center;  z-index: 10;  mix-blend-mode: darken;  background: rgba(0, 0, 0, 0.8);`;const Word = styled.h3`  opacity: 0.2;  font-size: 14vw;  text-transform: uppercase;  margin: 0;  position: absolute;  &.transition-appear,  &.transition-enter {    opacity: 0;    transform: translateY(-200px);  }  &.transition-appear-active,  &.transition-enter-active {    opacity: 0.2;    transform: translateY(0);    transition-duration: 1000ms;    transition-property: opacity, transform;  }  &.transition-appear-done,  &.transition-enter-done {    opacity: 0.2;  }  &.transition-exit {    transform: translateY(0);    opacity: 0.2;  }  &.transition-exit-active {    opacity: 0;    transform: translateY(200px);    transition-duration: 1000ms;    transition-property: opacity, transform;  }`;const Description = styled.h1`  font-size: 3.5em;  font-weight: bolder;  opacity: 0;  animation-name: ${FadeStart};  animation-duration: 1500ms;  animation-fill-mode: forwards;  position: relative;  z-index: 20;  &::before {    content: "";    display: block;    width: 75px;    height: 5px;    margin-bottom: 20px;    background-color: ${(props) => props.theme.colors.blue};  }  @media (${(props) => props.theme.breakpoints.mobile}) {    font-size: 2em;  }`;const Name = styled.h2`  font-size: 2.5em;  text-transform: uppercase;  padding: 5px;  margin: 0 0 2px 0;  float: left;  clear: left;  background-image: linear-gradient(    to right,    ${(props) => props.theme.colors.blue} 0,    ${(props) => props.theme.colors.purple} 100%  );  transform: translateX(-100vw);  animation-name: ${SlideStart};  animation-duration: 750ms;  animation-fill-mode: forwards;  position: relative;  z-index: 20;  @media (${(props) => props.theme.breakpoints.mobile}) {    font-size: 1.5em;  }`;
added pages/index.module.css
@@ -0,0 +1,139 @@/* Landing page styles migrated from styled-components */@keyframes fadeStart {  from {    opacity: 0;  }  to {    opacity: 1;  }}@keyframes slideStart {  from {    transform: translateX(-100vw);  }  to {    transform: translateX(0);  }}.imageWrapper {  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  width: 100%;  height: 100vh;  z-index: 0;  pointer-events: none;}.words {  position: fixed;  top: 0;  bottom: 0;  left: 0;  right: 0;  width: 100%;  height: 100vh;  display: flex;  align-items: center;  justify-content: center;  z-index: 10;  mix-blend-mode: darken;  background: rgba(0, 0, 0, 0.8);}.word {  opacity: 0.2;  font-size: 14vw;  text-transform: uppercase;  margin: 0;  position: absolute;}/* CSSTransition mappings for the word animation */.wordAppear,.wordEnter {  opacity: 0;  transform: translateY(-200px);}.wordAppearActive,.wordEnterActive {  opacity: 0.2;  transform: translateY(0);  transition-duration: 1000ms;  transition-property: opacity, transform;}.wordAppearDone,.wordEnterDone {  opacity: 0.2;}.wordExit {  transform: translateY(0);  opacity: 0.2;}.wordExitActive {  opacity: 0;  transform: translateY(200px);  transition-duration: 1000ms;  transition-property: opacity, transform;}.description {  font-size: 3.5em;  font-weight: bolder;  opacity: 0;  animation-name: fadeStart;  animation-duration: 1500ms;  animation-fill-mode: forwards;  position: relative;  z-index: 20;}.description::before {  content: "";  display: block;  width: 75px;  height: 5px;  margin: 0 0 20px 0;  background-color: var(--color-blue);}@media (max-width: 767.98px) {  .description {    font-size: 2em;  }}.name {  font-size: 2.5em;  text-transform: uppercase;  padding: 5px;  margin: 0 0 2px 0;  /* centered via parent flex container */  align-self: flex-start;  background-image: linear-gradient(    to right,    var(--color-blue) 0,    var(--color-purple) 100%  );  transform: translateX(-100vw);  animation-name: slideStart;  animation-duration: 750ms;  animation-fill-mode: forwards;  position: relative;  z-index: 20;}@media (max-width: 767.98px) {  .name {    font-size: 1.5em;  }}
added styles/globals.css
@@ -0,0 +1,54 @@:root {  /* Design tokens (used by CSS Modules as var(--token)) */  --color-primary: #20232e;  --color-blue: #0e3ff4;  --color-purple: #842bff;  --radius-2: 3px;  --transition-250: 250ms;  --animation-1500: 1500ms;}/* Base global styles (migrated from styled-components GlobalStyle) */body {  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";  color: white;  background-color: var(--color-primary);  min-height: 100vh;  width: 100%;  padding: 0;  margin: 0;  overflow-x: hidden;  text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;  text-rendering: optimizeLegibility;  user-select: none;  cursor: none;}a,button {  cursor: none;}/* React Transition Group classes (global) */.transition {  position: absolute;  top: 0;  left: 0;  right: 0;  transition: opacity var(--transition-250);}.transition-enter {  opacity: 0;}.transition-enter-active,.transition-enter-done {  opacity: 1;}.transition-exit {  opacity: 1;}.transition-exit-active {  opacity: 0;}