commit 3601bc88e79f625d4036803a9a061af286421b0a Author: G360Pmk Date: Tue Oct 6 18:38:18 2026 +0000 Initial commit: site + GA4 + Docker for Coolify diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9146a39 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a82f600 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store +.vercel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6cb290c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY . . +RUN node build.mjs + +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1879ed5 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# dylanmazzei.com + +Sitio de Dylan Mazzei, Founder & Operator. HTML estático generado con un script de Node sin dependencias. + +## Estructura + +``` +src/layout.mjs Layout compartido: , nav, footer, iconos y helpers +src/pages/*.mjs Una página por archivo (texto literal del documento de contenido) +src/service.mjs Bloques compartidos de las páginas de servicios de Consulting +static/ Se copia tal cual a dist/ (CSS, JS, imágenes, favicon) +build.mjs Genera dist/ + sitemap.xml + robots.txt + 404.html +``` + +| Ruta | Archivo | +|------|---------| +| `/` | `src/pages/home.mjs` | +| `/about/` | `src/pages/about.mjs` | +| `/portfolio/` | `src/pages/portfolio.mjs` | +| `/consulting/` | `src/pages/consulting.mjs` | +| `/consulting/marketing/` | `src/pages/marketing.mjs` | +| `/consulting/business-setup/` | `src/pages/business-setup.mjs` | +| `/consulting/business-optimization/` | `src/pages/business-optimization.mjs` | +| `/consulting/supply-chain/` | `src/pages/supply-chain.mjs` | +| `/consulting/cybersecurity/` | `src/pages/cybersecurity.mjs` | +| `/consulting/education/` | `src/pages/education.mjs` | +| `/consulting/fractional/` | `src/pages/fractional.mjs` | +| `/contact/` | `src/pages/contact.mjs` | +| `/press/` | `src/pages/press.mjs` | + +## Desarrollo + +```bash +npm run build # genera dist/ +npm run dev # build + servidor local +``` + +## Deploy en Vercel + +Importa el repo en Vercel. `vercel.json` ya define el build (`npm run build`), la salida (`dist`) y las URLs limpias con slash final. No hace falta configurar nada más. + +## Pendientes de contenido + +- **Press:** falta el link del artículo de Gulf Insider y confirmar su titular exacto (`FEATURE` en `src/pages/press.mjs`). +- **Korrith:** falta el cargo de Dylan (el tercer campo, vacío, en `src/pages/portfolio.mjs`). +- **Páginas fuera del alcance actual:** Playbook y Poetry no forman parte del sitio. Sus secciones se muestran sin botón; si se crean esas páginas, se añade el botón con su texto original del documento. +- **Calendario:** poner el link real de agenda en `SITE.calendar`. +- **Formulario de contacto:** hoy abre el cliente de correo con el mensaje prellenado. Para recibirlo directo hay que conectar un servicio (Formspree, Resend, etc.). diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..815054a --- /dev/null +++ b/build.mjs @@ -0,0 +1,45 @@ +// Builds the static site into ./dist. No dependencies: `node build.mjs`. +import { mkdir, rm, writeFile, cp, readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { page, eyebrow, btn } from './src/layout.mjs'; + +const root = dirname(fileURLToPath(import.meta.url)); +const out = join(root, 'dist'); +const pages = ['home', 'about', 'portfolio', 'consulting', 'marketing', 'business-setup', 'business-optimization', 'supply-chain', 'cybersecurity', 'education', 'fractional', 'contact', 'press']; + +await rm(out, { recursive: true, force: true }); +await mkdir(out, { recursive: true }); +await cp(join(root, 'static'), out, { recursive: true }); + +// Fingerprint CSS/JS so a new deploy never reuses a stale cached copy. +const hashOf = async (f) => createHash('sha256').update(await readFile(join(root, 'static', f))).digest('hex').slice(0, 10); +const assetVersions = Object.fromEntries(await Promise.all(['styles.css', 'site.js', 'home.css', 'home.js', 'fonts.css'].map(async (f) => ['/assets/' + f, await hashOf('assets/' + f)]))); +const fingerprint = (html) => Object.entries(assetVersions).reduce((h, [u, v]) => h.replaceAll(`"${u}"`, `"${u}?v=${v}"`), html); + +const urls = []; +for (const name of pages) { + const def = (await import(`./src/pages/${name}.mjs`)).default; + const file = join(out, def.path, 'index.html'); + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, fingerprint(page(def))); + urls.push(def.path); + console.log('built', def.path); +} + +await writeFile(join(out, '404.html'), fingerprint(page({ + path: '/404/', + title: 'Page Not Found | Dylan Mazzei', + description: 'This page could not be found.', + body: `
${eyebrow('404')}

Page Not Found

${btn('BACK TO HOME', '/')}
`, +}))); + +const today = new Date().toISOString().slice(0, 10); +await writeFile(join(out, 'sitemap.xml'), ` + +${urls.map((u) => ` https://dylanmazzei.com${u}${today}`).join('\n')} + +`); +await writeFile(join(out, 'robots.txt'), 'User-agent: *\nAllow: /\nSitemap: https://dylanmazzei.com/sitemap.xml\n'); +console.log('done →', out); diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..b4f3dc8 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + gzip on; + gzip_types text/css application/javascript image/svg+xml application/xml; + + location /assets/ { + add_header Cache-Control "public, max-age=604800, stale-while-revalidate=86400"; + } + + location / { + try_files $uri $uri/index.html =404; + } + + error_page 404 /404.html; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8c8940 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "dylanmazzei-website", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "dylanmazzei.com — static site built with a zero-dependency Node script.", + "scripts": { + "build": "node build.mjs", + "dev": "node build.mjs && npx --yes serve dist" + }, + "engines": { "node": ">=18" } +} diff --git a/src/layout.mjs b/src/layout.mjs new file mode 100644 index 0000000..f9ff69e --- /dev/null +++ b/src/layout.mjs @@ -0,0 +1,258 @@ +// Shared layout, icons and small HTML helpers for every page. + +export const SITE = { + url: 'https://dylanmazzei.com', + gaId: 'G-P9H24L6WM5', + email: 'dylan@dylanmazzei.com', + pressEmail: 'press@dylanmazzei.com', + linkedin: 'https://www.linkedin.com/in/dylanmazzei/', + instagram: 'https://www.instagram.com/thedylanmazzei', + // Replace with the real scheduling link (Calendly, Cal.com, etc.). + calendar: 'mailto:dylan@dylanmazzei.com?subject=Scheduling%20a%20conversation', +}; + +const ICONS = { + arrow: '', + 'arrow-ur': '', + check: '', + plus: '', + chev: '', + menu: '', + close: '', + brace: '', + search: '', + chip: '', + bottle: '', + edu: '', + shield: '', + bag: '', + umbrella: '', + megaphone: '', + heart: '', + shirt: '', + building: '', + gauge: '', + truck: '', + compass: '', + target: '', + users: '', + chart: '', + layers: '', + mic: '', + rocket: '', + route: '', + mail: '', + calendar: '', + pin: '', + book: '', + pen: '', + moon: '', + sun: '', + spark: '', +}; + +const SMALL = new Set(['arrow', 'arrow-ur', 'check', 'plus', 'chev', 'brace']); + +export function icon(name, cls = '') { + const vb = SMALL.has(name) ? '0 0 16 16' : name === 'menu' || name === 'close' ? '0 0 18 18' : '0 0 24 24'; + const sw = SMALL.has(name) || name === 'menu' || name === 'close' ? '' : ' stroke-width="1.75"'; + return ``; +} + +// Dylan Mazzei monogram: italic "D" (inherits text color) and the sky-blue blade. +export const mark = (cls = 'logo-mark') => ``; + +export const logo = () => `${mark()}Dylan Mazzei`; + +export const eyebrow = (t) => `${t}`; + +export function btn(text, href, variant = 'primary', extra = '') { + const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener"' : ''; + return `${text}${icon('arrow')}`; +} + +export function textLink(text, href) { + const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener"' : ''; + return `${text} ${icon('arrow')}`; +} + +export const paras = (arr, cls = '') => arr.map((t) => `${t}

`).join(''); + +export function faq(items) { + return `
${items + .map( + ([q, a], i) => `${q}${icon('plus')}
${paras(a)}
` + ) + .join('')}
`; +} + +const NAV = [ + { label: 'Portfolio', href: '/portfolio/' }, + { label: 'About', href: '/about/' }, + { + label: 'Consulting', + href: '/consulting/', + children: [ + { label: 'Consulting', desc: 'Find the Real Problem. Then Fix the Right One.', href: '/consulting/', icon: 'compass' }, + { label: 'Marketing & AI Search', desc: 'Get Found Where Attention Is Moving.', href: '/consulting/marketing/', icon: 'search' }, + { label: 'Business Setup', desc: 'Start With Fewer Expensive Mistakes.', href: '/consulting/business-setup/', icon: 'building' }, + { label: 'Business Optimization', desc: 'Find What Growth Made Messy.', href: '/consulting/business-optimization/', icon: 'gauge' }, + { label: 'Supply Chain', desc: 'Know What Happens Before It Reaches the Customer.', href: '/consulting/supply-chain/', icon: 'truck' }, + { label: 'Cybersecurity', desc: 'Find the Exposure Before Someone Else Does.', href: '/consulting/cybersecurity/', icon: 'shield' }, + { label: 'Education', desc: 'Make the Strategy Survive the Classroom.', href: '/consulting/education/', icon: 'edu' }, + { label: 'Fractional Leadership', desc: 'When the Business Needs an Owner, Not Another Recommendation.', href: '/consulting/fractional/', icon: 'users' }, + ], + }, + { label: 'Press', href: '/press/' }, +]; + +function nav(path) { + const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href)); + const desktop = NAV.map((item) => { + if (!item.children) { + return `
  • ${item.label}
  • `; + } + return `
  • + + +
  • `; + }).join(''); + const mobile = NAV.map((item) => { + const top = `
  • ${item.label} ${icon('arrow', 'check')}
  • `; + if (!item.children) return top; + return top + item.children.slice(1).map((c) => `
  • ${c.label}
  • `).join(''); + }).join(''); + + return ` +`; +} + +function footer() { + return ``; +} + +export function page({ path, title, description, body, schema = [], head = '', scripts = '' }) { + const canonical = SITE.url + path; + const person = { + '@context': 'https://schema.org', + '@type': 'Person', + '@id': SITE.url + '/#dylan', + name: 'Dylan Mazzei', + url: SITE.url, + email: SITE.email, + image: SITE.url + '/assets/dylan-portrait.jpg', + jobTitle: 'Founder & Operator', + sameAs: [SITE.linkedin, SITE.instagram], + }; + const ld = [person, ...schema].map((s) => ``).join('\n'); + return ` + + + + + + +${title} + + + + + + + + + + + + + + + + + + + + + +${head} + +${ld} + + + + +${nav(path)} +
    +${body} +
    +${footer()} + +${scripts} + + +`; +} diff --git a/src/pages/about.mjs b/src/pages/about.mjs new file mode 100644 index 0000000..bca2e6c --- /dev/null +++ b/src/pages/about.mjs @@ -0,0 +1,238 @@ +import { icon, eyebrow, btn, paras } from '../layout.mjs'; + +const lines = (arr) => `
    ${arr.map((t, i) => `${t}

    `).join('')}
    `; + +const firsts = [ + ['The First Yes', ['Before the team, the systems, and the growth plan, someone has to believe in what you’re selling.', 'That first customer teaches you something no projection can.']], + ['The First Wrong Call', ['Eventually, you make one.', 'Mine taught me something useful: being wrong is expensive only if your ego keeps you there.']], + ['The First Real Win', ['It changes your confidence.', 'Not because you suddenly know everything.', 'Because now you know you can figure something out.']], +]; + +const noStraight = [ + ['compass', 'New Industry. Blank Page.', ['Walking into a business you don’t completely understand is uncomfortable.', 'Good.', 'It makes you listen before speaking, find people smarter than you, and question things an insider might have stopped noticing.']], + ['layers', 'Different Business. Familiar Problem.', ['The terminology changes.', 'The fundamentals have a strange habit of showing up again: attention, trust, people, economics, execution, and whether the customer actually cares.']], + ['spark', 'Beginner Is a Useful Position.', ['I never want experience to make me too sophisticated to ask an obvious question.', 'Obvious questions have opened some very interesting doors.']], +]; + +const people = [ + ['mic', 'Speak Your Mind', ['I want people who bring their own judgment to the table. If there\'s a better approach, a concern I\'ve overlooked, or a decision worth questioning, I want to hear it.', 'A different perspective can change everything.']], + ['users', 'Experience Isn\'t Everything', ['Good ideas don\'t always come from the most senior person in the room.', 'I pay attention to people who understand the work firsthand. Sometimes the clearest insight comes from someone who has just joined the team.']], + ['target', 'Decide and Deliver', ['I believe in open discussions and honest disagreements. Every perspective deserves consideration, but eventually, a decision has to be made.', 'Once we agree on a direction, the focus shifts to execution. That\'s where a strong team proves itself.']], +]; + +const exitsLater = [ + ['Winning Doesn’t Make You Right Forever', ['Something worked.', 'Great.', 'The next company does not owe you the same answer.']], + ['Pattern Recognition Has a Catch', ['Experience helps you see what you’ve seen before.', 'It can also convince you that something new is familiar.', 'I try to know the difference.']], + ['I Still Want More', ['Not more for the sake of accumulation.', 'More problems worth thinking about. More people who raise my standard. More ideas that make ignoring them difficult.', 'My definition of ambition has become more selective, not smaller.']], +]; + +const knowNow = [ + ['I Know What I’m Good At.', ['And I’ve become much better at recognizing who is better than me at everything else.']], + ['I Know When I Don’t Know.', ['That sentence gets easier with experience.', 'It also gets more valuable.']], + ['I Know I’m Not Finished.', ['Not because I have a number of companies left to start or some finish line in mind.', 'I just haven’t stopped being interested yet.']], +]; + +export default { + path: '/about/', + title: 'About Dylan Mazzei | Founder, Operator & Entrepreneur', + description: 'Meet Dylan Mazzei, a founder and operator shaped by eight years of companies, four exits, hard decisions, changing industries, and what comes next.', + body: ` +
    +
    +
    + ${eyebrow('ABOUT DYLAN')} +

    Founder in Title. Operator in Practice.

    +

    Anyone can have an opinion about a business.
    I prefer being responsible for one.

    +
    +

    For more than eight years, I’ve put ideas into the world, brought people around them, watched customers respond, made decisions with imperfect information, and learned what happens after the strategy meeting ends.

    +

    Four companies have been sold. Others are still growing, changing, and giving me new things to figure out.

    +

    That’s the part I like.

    +
    +
    +
    + Dylan Mazzei + +
    +
    +
    + +
    +
    +
    +
    ${eyebrow('THE BEGINNING')}

    Business Made More Sense Once It Was Mine to Figure Out.

    +

    I grew up in South Florida and studied business. Then I got close enough to the real thing to understand how much the classroom could never simulate.

    +
    +
    + ${firsts.map(([h, p], i) => `
    ${i + 1}

    ${h}

    ${lines(p)}
    `).join('')} +
    +
    +
    + +
    +
    +
    +
    ${eyebrow('NO STRAIGHT LINES')}

    My Career Would Be Easier to Explain If I Had Stayed in One Industry.

    +

    I didn’t.
    And I wouldn’t rewrite it.

    +
    +
    + ${noStraight.map(([ic, h, p]) => `
    ${icon(ic)}

    ${h}

    ${lines(p)}
    `).join('')} +
    +
    ${btn('EXPLORE MY JOURNEY', '/portfolio/')}
    +
    +
    + +
    +
    +
    +
    ${eyebrow('MY OPERATING SYSTEM')}

    I Don’t Have a Formula. I Have a Few Rules I Keep Coming Back To.

    +

    Eight years has been long enough to develop instincts.

    And long enough to know when not to trust them.

    +
    +
    +
    RULE_01

    Get Closer

    +

    When something doesn’t make sense, I move toward it.

    +

    Talk to the customer. Sit with the team. Follow the process. Look at the numbers.

    +

    Distance makes problems look cleaner than they are.

    +
    RULE_02

    Ask the Uncomfortable Question

    +

    Why are we doing this?

    What if we’re wrong?

    Would I buy this?

    What are we pretending isn’t a problem?

    +

    The question people least want to answer is occasionally the one worth asking first.

    +
    RULE_03

    Make the Call

    +

    Eventually, research becomes procrastination.

    +

    Get enough information. Understand the risk. Decide.

    +

    Then stay around for the consequences.

    +
    RULE_04

    Let Reality Win

    +

    I can love an idea.

    +

    The customer is still allowed to hate it.

    +

    Reality wins every argument eventually. I prefer finding out early.

    +
    +
    +
    + +
    +
    +
    +
    ${eyebrow('THE PEOPLE BEHIND THE BUSINESS')}

    The Right People Make the Business Better.

    +
    +

    I've learned that the people around me influence far more than the work getting done. They shape the decisions we make, the problems we notice, and how far an idea can go.

    +

    I value people who think independently, take ownership, and care enough about the outcome to challenge my perspective.

    +
    +
    +
    + ${people.map(([ic, h, p]) => `
    ${icon(ic)}

    ${h}

    ${paras(p)}
    `).join('')} +
    +
    +
    + +
    +
    +
    +
    + ${eyebrow('FOUR EXITS LATER')} +

    Success Made Me More Confident. Experience Made Me Less Certain.

    +

    Four exits look neat when reduced to a number.

    The years behind them were anything but.

    +
    +
    + ${exitsLater.map(([h, p]) => `

    ${h}

    ${lines(p)}
    `).join('')} +
    +
    +
    +
    + +
    +
    +
    + ${eyebrow('THINGS I NOTICE')} +

    The Business Usually Whispers Before It Shouts.

    +
    +
    +
    +
    +

    A customer asks the same question for the tenth time.

    +

    Someone creates a spreadsheet to work around the software.

    +

    A five-minute task somehow takes an afternoon.

    +
    +

    Nobody thinks any of it is urgent.

    I notice those things.

    +
    +
    +
    +
    “We’ve Always Done It This Way.”Interesting.
    +
    “Nobody Complains About It.”Even more interesting.
    +
    “It’s Not a Big Problem.”Maybe.
    +
    +

    Or maybe everyone has simply become very good at living with it.

    Small friction has introduced me to some very big questions.

    +
    +
    +
    +
    + +
    +
    +
    +
    ${eyebrow('THE PART THAT DOESN’T FIT ON LINKEDIN')}

    Not Everything I Make Is Meant to Become a Company.

    +
    +

    This might be one of the more important things I’ve learned.

    +

    Not every interest needs monetizing.
    Not every thought needs optimizing.
    Not everything meaningful needs a metric attached to it.

    +
    +
    +
    +
    + ${eyebrow('A DIFFERENT OUTLET')} +

    Some Thoughts Need Space.

    +

    That’s poetry for me.No pitch deck. No growth plan. No conversion rate.

    +

    Just something I wanted to say and the patience to find the right words for it.

    +
    +
    +
    +
    + +
    +
    +
    +
    ${eyebrow('WHAT I KNOW NOW')}

    Eight Years In, I Have Fewer Absolutes.

    +
    +
    + ${knowNow.map(([h, p]) => `

    ${h}

    ${paras(p)}
    `).join('')} +
    +
    +
    + +
    +
    +
    +
    +
    01

    Diagnose First

    The obvious problem and the real problem are not always the same.

    Get close enough to know the difference.

    +
    02

    Make It Work in the Real Business

    A strategy can look great on paper and fall apart on Monday morning.

    I care about what can actually be executed, measured, and improved.

    That is the same standard I use inside my own companies.

    +
    +
    + ${eyebrow('WHEN FOUNDERS CALL')} +

    I Work With Founders the Way I Run My Own Companies.

    +
    +

    When I step into another founder’s business, I want to understand what is actually happening before deciding what needs to change.

    +

    No assumptions. No solution looking for a problem.

    +
    +
    ${btn('WORK WITH DYLAN', '/consulting/')}
    +
    +
    +
    +
    + +
    +
    +
    +
    + ${eyebrow('STILL CURIOUS')} +

    I’m More Interested in What I Haven’t Seen Yet.

    +
    +

    Eight years ago, I couldn’t have mapped the route that brought me here. I’m glad I didn’t.

    +

    There are still people I haven’t met, problems I haven’t seen, and ideas that will probably ruin a perfectly good night’s sleep.

    +

    If the next eight years surprise me too, I’ll take it.

    +
    +
    ${btn('START A CONVERSATION', '/contact/')}
    +
    +
    +
    +
    +`, +}; diff --git a/src/pages/business-optimization.mjs b/src/pages/business-optimization.mjs new file mode 100644 index 0000000..e877cd6 --- /dev/null +++ b/src/pages/business-optimization.mjs @@ -0,0 +1,77 @@ +import { icon } from '../layout.mjs'; +import { heroBlock, coversBlock, whoBlock, deliveryBlock, operatorBlock, faqBlock, closingBlock, faqSchema } from '../service.mjs'; + +const faqs = [ + ['How is business optimisation different from hiring a fractional COO?', ['Business optimisation is a defined diagnostic and improvement engagement.', 'A fractional COO takes ongoing ownership of an operating function. If the diagnostic shows that the business needs continuing operational leadership rather than a focused optimisation sprint, we can discuss whether a fractional role makes more sense.']], + ['Will AI automation replace my staff?', ['That is not the assumption.', 'We look for work that is repetitive, manual, slow, or unnecessarily dependent on people before deciding whether AI or another form of automation belongs there.', 'Sometimes technology is the answer. Sometimes the process itself needs to change first.']], + ['My business is small. Is optimisation premature?', ['Not necessarily.', 'Optimisation becomes useful when operational friction is affecting margin, time, customer experience, decision-making, or the founder’s ability to focus.', 'Company size alone does not determine that.']], + ['What do you need from me to start?', ['Enough access to understand how the business actually operates.', 'Depending on the diagnostic, that can include financial information, software and vendor costs, team structure, existing processes, reporting, and conversations with the people closest to the work.']], +]; + +const visual = `
    ${icon('brace')}operations.log
    +
    $ find the friction→ time→ margin→ visibility→ unnecessary founder attention$ prioritise the fixes✓ what deserves attention first✓ what can wait$ make it lighter
    `; + +export default { + path: '/consulting/business-optimization/', + title: 'Business Optimisation Consulting | Dylan Mazzei', + description: 'Business optimisation consulting for founders focused on margins, processes, team structure, AI automation, reporting, and operational efficiency.', + schema: [faqSchema(faqs)], + body: + heroBlock({ + id: 'bo-hero', + crumb: 'Business Optimization', + kicker: 'BUSINESS OPTIMIZATION CONSULTING', + h1: 'Better Operations Create Better Businesses.', + lead: 'Revenue can grow while margins tighten, processes multiply, teams get harder to manage, and the founder somehow ends up with more on their plate.', + paras: [ + '*That doesn’t always mean the business needs more sales.', + 'Sometimes it needs a closer look at where the money, time, people, and decisions are going.', + 'My teams and I find the friction, determine what is actually creating it, and make the business easier to operate as it grows.', + ], + visual, + }).replace('