[{"data":1,"prerenderedAt":882},["ShallowReactive",2],{"work-index":3},[4,144,219,310,385,455,523,593,675,751],{"id":5,"title":6,"accent":7,"body":8,"completedAt":122,"cover":123,"coverAlt":124,"description":14,"extension":125,"external":126,"liveUrl":127,"meta":128,"navigation":129,"order":130,"path":131,"role":132,"seo":133,"stack":134,"status":139,"stem":140,"summary":141,"year":142,"__hash__":143},"work\u002Fwork\u002Fwayta-peru.md","Wayta Perú","#9a3412",{"type":9,"value":10,"toc":113},"minimark",[11,15,20,23,42,46,58,64,67,73,77,84,98,102,105],[12,13,14],"p",{},"Wayta Perú runs guided trips through the Sacred Valley. They needed a site that reads as a\nserious operator in two languages, and they needed to change the copy themselves without opening\na pull request or waiting on me for a typo.",[16,17,19],"h2",{"id":18},"the-problem-underneath-the-brief","The problem underneath the brief",[12,21,22],{},"Small operators usually end up with two half solutions. Either the site is a static build that\nlooks right and cannot be edited, or it is a page builder that can be edited and never quite\nlooks right. Both fail in the same place: the second language. Translation gets bolted on as a\nduplicate page tree, the two copies drift apart within a season, and the English side quietly\nbecomes the stale one.",[24,25,26,32,37],"metric-row",{},[27,28],"metric",{"label":29,"note":30,"value":31},"Locales","Spanish is the default, English is a first class peer rather than a translation layer.","2",[27,33],{"label":34,"note":35,"value":36},"Deployment","Marketing pages, blog, and the editing studio all ship together.","1",[27,38],{"label":39,"note":40,"value":41},"Duplicated page trees","One route definition, two locales resolved from it.","0",[16,43,45],{"id":44},"how-it-is-built","How it is built",[12,47,48,49,53,54,57],{},"Next.js 15 App Router, with ",[50,51,52],"code",{},"next-intl"," owning the locale segment. The routes are declared once\nand both locales resolve through the same tree, so a section added in Spanish exists in English\nthe moment its copy lands. Sanity holds every editable string and the blog, and the studio is\nembedded at ",[50,55,56],{},"\u002Fstudio"," in the same deployment, which means the client has one URL to remember and\none login to lose.",[59,60],"shot",{"alt":61,"caption":62,"src":63},"The Wayta Perú home page at desktop width, showing the itinerary section.","The home page is one anchored document rather than a set of thin sub pages, which suits how people actually read an operator site.","\u002Fwork\u002Fwayta-peru\u002Fshot-home.png",[12,65,66],{},"The interesting constraint was the language switch. Moving between locales has to keep the reader\nwhere they were. Someone halfway down the itineraries section who taps English and lands back at\nthe top of the home page has been punished for wanting to read it in their own language.",[68,69],"demo",{"component":70,"note":71,"origin":6,"title":72},"LocaleDemo","The switch is labelled in the language it will take you to, not the one you are already reading. That single rule removes most of the confusion around language toggles.","Locale switch that keeps your place",[16,74,76],{"id":75},"the-rest-of-the-site","The rest of the site",[78,79],"reel",{"alt":80,"caption":81,"poster":82,"src":83},"A scroll through the Wayta Perú home page from the hero down to the contact section.","A scroll capture rather than an embedded frame, so this keeps working exactly as it is even after the live site changes.","\u002Fwork\u002Fwayta-peru\u002Freel-poster.png","\u002Fwork\u002Fwayta-peru\u002Freel.mp4",[85,86,88,93],"gallery",{"columns":31,"label":87},"Screens from Wayta Perú",[59,89],{"alt":90,"caption":91,"src":92},"A blog entry rendered from Sanity Portable Text, with the language switch in the header.","Blog entries come out of Sanity as Portable Text.","\u002Fwork\u002Fwayta-peru\u002Fshot-blog.png",[59,94],{"alt":95,"caption":96,"src":97},"The embedded Sanity studio at the studio route, showing the itinerary document type.","The studio ships inside the same deployment.","\u002Fwork\u002Fwayta-peru\u002Fshot-studio.png",[16,99,101],{"id":100},"what-i-would-keep","What I would keep",[12,103,104],{},"The one deployment decision paid for itself immediately. The client edits in the same place they\nvisit, there is no separate CMS host to renew, and preview builds show real content because there\nis only ever one source of it.",[106,107,110],"callout",{"title":108,"tone":109},"Carried into later work","accent",[12,111,112],{},"Every multilingual project since starts from the routing model rather than the design. If the\nlocale strategy is decided after the layout, the layout has already made assumptions that the\nsecond language will break.",{"title":114,"searchDepth":115,"depth":116,"links":117},"",2,3,[118,119,120,121],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":75,"depth":115,"text":76},{"id":100,"depth":115,"text":101},"2026-03","\u002Fwork\u002Fwayta-peru\u002Fcover.png","The Wayta Perú home page, a full width valley photograph under a bilingual navigation bar.","md",null,"https:\u002F\u002Fwaytaperu.com",{},true,1,"\u002Fwork\u002Fwayta-peru","Design and build, end to end",{"title":6,"description":14},[135,136,137,52,138],"Next.js 15","React 19","Sanity","Tailwind v4","completed","work\u002Fwayta-peru","A bilingual travel operator site where the marketing page, the blog, and the studio the client edits in are one deployment.",2026,"iEZyI3UTpDqgOBEOIIp4l6o27UGEkPZ_sTH8HsBAGaM",{"id":145,"title":146,"accent":147,"body":148,"completedAt":203,"cover":204,"coverAlt":205,"description":152,"extension":125,"external":126,"liveUrl":126,"meta":206,"navigation":129,"order":115,"path":207,"role":208,"seo":209,"stack":210,"status":139,"stem":216,"summary":217,"year":142,"__hash__":218},"work\u002Fwork\u002Fpersonae.md","Personae","#0f766e",{"type":9,"value":149,"toc":198},[150,153,155,158,174,176,179,184,187,193,195],[12,151,152],{},"Personae is a browsable library of customer and employee personas. The version that ships\npublicly describes an invented work management product, because the interesting part was never\nthe personas themselves. It was what happens to a persona six months after the research ends.",[16,154,19],{"id":18},[12,156,157],{},"Personas rot. They are published with confidence, printed onto a wall, and then the market moves\nand nobody has the standing to say that the persona is now wrong. The document outlives the\nresearch, and because it looks finished, it keeps being cited.",[24,159,160,165,170],{},[27,161],{"label":162,"note":163,"value":164},"Personas in the sample set","Every one labelled as sample material in its opening line.","6",[27,166],{"label":167,"note":168,"value":169},"Linked notebooks","Each profile points back at the research it came from.","4",[27,171],{"label":172,"note":173,"value":36},"Flag action per claim","Anyone reading can mark a statement as no longer true.",[16,175,45],{"id":44},[12,177,178],{},"IBM Carbon supplies the design system, which was the right call for a document heavy internal\ntool and the wrong call for anything with a marketing tone. Carbon has opinions, and taking them\nwholesale meant the interface argued about layout far less than a bespoke system would have. The\ncontent is MDX so a persona can embed a chart or a regional breakdown inline, and a D3 geo map\nrenders the country notes without shipping a mapping library to every page.",[59,180],{"alt":181,"caption":182,"src":183},"A persona profile showing goals, frustrations, and a link to the research notebook it came from.","Every profile links to the notebook behind it. A claim with no visible source is a claim nobody can check.","\u002Fwork\u002Fpersonae\u002Fshot-profile.png",[12,185,186],{},"The flag flow is the part I would build again anywhere. Any reader can mark a specific claim as\npossibly incorrect, with a short reason. The flag is logged and mailed to an admin through\nResend. There is no approval workflow and no moderation queue, because adding either one raises\nthe cost of flagging above the cost of quietly disbelieving the document.",[106,188,190],{"title":189,"tone":109},"The design decision that mattered",[12,191,192],{},"Making the flag cheap is what keeps the library honest. A correction process that requires a\nmeeting produces a persona set nobody corrects and everybody privately distrusts.",[16,194,101],{"id":100},[12,196,197],{},"Linking every profile to its source notebook, and treating a persona as a dated claim rather than\na finished artefact. Both are one line of frontmatter and they change how the document is read.",{"title":114,"searchDepth":115,"depth":116,"links":199},[200,201,202],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":100,"depth":115,"text":101},"2026-05","\u002Fwork\u002Fpersonae\u002Fcover.png","A persona profile page in IBM Carbon styling, with a profile summary beside a regional map.",{},"\u002Fwork\u002Fpersonae","Design system integration and build",{"title":146,"description":152},[135,211,212,213,214,215],"IBM Carbon","MDX","NextAuth","Resend","D3 geo","work\u002Fpersonae","A persona library built on IBM Carbon, where every profile links out to its research and every reader can flag a claim that no longer holds.","44iMPcUWDWL19R0leJczQxH2XeNcTGwpmL49vtuvl9s",{"id":220,"title":221,"accent":222,"body":223,"completedAt":293,"cover":294,"coverAlt":295,"description":227,"extension":125,"external":126,"liveUrl":296,"meta":297,"navigation":129,"order":116,"path":298,"role":299,"seo":300,"stack":301,"status":139,"stem":307,"summary":308,"year":142,"__hash__":309},"work\u002Fwork\u002Farsonistai.md","ArsonistAI","#c2410c",{"type":9,"value":224,"toc":288},[225,228,230,233,248,250,261,266,277,283,285],[12,226,227],{},"ArsonistAI runs seven services, from content generation to operations consulting to training\nteams on Claude Code and Cursor, and sells three self serve products beside them: a KPIs plugin,\na portfolio tool, and a Figma business toolkit. That is two different businesses sharing one\nhomepage, and the failure mode is obvious. An enterprise buyer evaluating a consultancy and a\ndesigner buying a plugin do not want to read the same page.",[16,229,19],{"id":18},[12,231,232],{},"Most sites solve this by picking one audience and hiding the other behind a nav item. That treats\nthe second audience as an afterthought, which shows. The actual fix is structural: let the\nservices read as a consultancy's capability list, let the products read as a small, confident\nshelf of things you can just buy, and make sure neither section has to apologize for the other's\ntone.",[24,234,235,240,245],{},[27,236],{"label":237,"note":238,"value":239},"Services listed","Content, concept visualization, exploration, experience optimization, product design, ops consulting, training.","7",[27,241],{"label":242,"note":243,"value":244},"Self serve products","A KPIs plugin, a portfolio tool, and a Figma business toolkit, sold without a sales call.","3",[27,246],{"label":29,"note":247,"value":31},"English and Spanish, resolved through the same path segment pattern as the multilingual travel sites.",[16,249,45],{"id":44},[12,251,252,253,256,257,260],{},"Next.js, built with Turbopack rather than the default bundler, on a design token system laid over\nTailwind rather than raw utility classes. Tokens are named in the Material 3 convention,\n",[50,254,255],{},"text-label-lg",", ",[50,258,259],{},"rounded-shape-full",", but the palette underneath is the brand's own lava red\nrather than Material's defaults. That distinction matters more than it sounds: it gets the\ndiscipline of a token system, consistent spacing and type scale across seven service pages,\nwithout inheriting a design language that has nothing to do with fire.",[59,262],{"alt":263,"caption":264,"src":265},"The ArsonistAI home page hero in dark mode, with the lava accented logo and a sticky blurred navigation bar.","Dark mode is the default rendering, not a toggle a visitor discovers later. The lava palette was built to live there permanently.","\u002Fwork\u002Farsonistai\u002Fshot-home.png",[12,267,268,269,272,273,276],{},"The contact form runs Cloudflare Turnstile instead of a visible CAPTCHA, which keeps the one\naction an enterprise lead actually needs to take, send a message, from competing with a puzzle.\nLocale routing follows the same path segment pattern this portfolio uses elsewhere: ",[50,270,271],{},"\u002Fen"," and\n",[50,274,275],{},"\u002Fes"," resolve through one route tree, the same trick applied to a fire themed design consultancy\ninstead of a travel operator.",[106,278,280],{"title":279,"tone":109},"The decision that held up",[12,281,282],{},"Naming the design tokens after a system nobody on the team was actually using kept the palette\ndisciplined without importing that system's opinions. A token layer earns its keep by being\nconsistent, not by being borrowed wholesale.",[16,284,101],{"id":100},[12,286,287],{},"Letting the services and the products sit in genuinely different registers on the same page,\nrather than forcing a single tone to cover a consulting engagement and a plugin purchase. Buyers\nin each category never have to translate the other section into something that applies to them.",{"title":114,"searchDepth":115,"depth":116,"links":289},[290,291,292],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":100,"depth":115,"text":101},"2026-07","\u002Fwork\u002Farsonistai\u002Fcover.png","The ArsonistAI home page in dark mode, showing the lava accented services grid above a row of client logos.","https:\u002F\u002Fwww.arsonistai.com",{},"\u002Fwork\u002Farsonistai","Front end build and design system",{"title":221,"description":227},[302,303,304,305,306],"Next.js","React","Turbopack","Tailwind CSS","Cloudflare Turnstile","work\u002Farsonistai","An AI design consultancy with two audiences on one homepage, enterprises hiring for AI augmented product work, and individual designers buying tools off the shelf.","adx_EYHFqAji85SKl-Ldukrz7wHlt60NevLTzN4h9UY",{"id":311,"title":312,"accent":313,"body":314,"completedAt":372,"cover":373,"coverAlt":374,"description":318,"extension":125,"external":126,"liveUrl":126,"meta":375,"navigation":129,"order":376,"path":377,"role":378,"seo":379,"stack":380,"status":139,"stem":381,"summary":382,"year":383,"__hash__":384},"work\u002Fwork\u002Fhcit.md","HCIT","#7c2d12",{"type":9,"value":315,"toc":367},[316,319,321,324,339,341,348,353,356,362,364],[12,317,318],{},"Hello CIT sells infrastructure, AI agents, telecom, custom software, and cybersecurity, and named\nevery one of them starting with the same word. That naming decision is either the thing that\nmakes five unrelated service lines read as one company, or the thing that makes a visitor scroll\npast all five without remembering any of them. The site had to make it the first one.",[16,320,19],{"id":18},[12,322,323],{},"An integrator selling to enterprise buyers has one job before anything else: prove it belongs in\nthe room with Nvidia and HPE, not just that it resells their hardware. That proof cannot live in\na paragraph of copy, because every integrator's paragraph says the same thing. It has to live in\nthe structure of the page, in how fast a buyer can see the certifications, the client roster, and\nthe specific vertical they came looking for.",[24,325,326,331,335],{},[27,327],{"label":328,"note":329,"value":330},"Service verticals","Hello Infrastructure, Hello Agents, Hello Phone, Hello Code, Hello Cybersecurity.","5",[27,332],{"label":333,"note":334,"value":244},"Featured solutions","AI Factory, AI Agents, and Digital Power sit above the vertical list, not inside it.",[27,336],{"label":337,"note":338,"value":239},"Enterprise client logos","Nvidia, HPE, Huawei, Lenovo, Fortinet, Check Point, and Commvault, all above the fold.",[16,340,45],{"id":44},[12,342,343,344,347],{},"Next.js on the front end, with ",[50,345,346],{},"next\u002Fimage"," doing the heavy lifting across the client logo wall\nand the hero, where a dozen vendor marks and a rotating solutions banner would otherwise be the\nslowest thing on the page. The page order was the real design decision: solutions overview first,\nthen the client logos and testimonials, then the FAQ, then team bios, with the blog and contact\nforms doing the closing work at the bottom.",[59,349],{"alt":350,"caption":351,"src":352},"The five Hello service verticals rendered as a grid, each linking to its own detail page.","Same prefix, five different buyers. The grid has to let a visitor searching for cybersecurity ignore the four verticals that aren't theirs.","\u002Fwork\u002Fhcit\u002Fshot-services.png",[12,354,355],{},"Putting the certifications, ISO 9001, ISO 37001, NVIDIA HPC, PMI, ITIL v4, and Huawei HCSA, in\nthe same visual register as the client logos rather than buried in a footer was the other lever.\nA certification list reads as compliance theater on its own page. Beside the Nvidia and Huawei\nmarks, it reads as evidence for the claim the logos are already making.",[106,357,359],{"title":358,"tone":109},"The naming risk that paid off",[12,360,361],{},"Five sub brands sharing one word is a bet that consistency beats specificity. It only works if\nthe page lets someone skip straight to their vertical in one glance, which is a layout problem\nbefore it is a naming one.",[16,363,101],{"id":100},[12,365,366],{},"Ordering the page around proof before pitch. An integrator's buyer already knows what an\nintegrator does; what they are checking is whether this one has done it for a company their size,\nwhich is exactly what the logo wall and the certification list are there to answer.",{"title":114,"searchDepth":115,"depth":116,"links":368},[369,370,371],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":100,"depth":115,"text":101},"2025-11","\u002Fwork\u002Fhcit\u002Fcover.png","The Hello CIT home page, showing the five Hello service verticals above a row of enterprise client logos.",{},4,"\u002Fwork\u002Fhcit","Front end build",{"title":312,"description":318},[302,303],"work\u002Fhcit","A five vertical AI and infrastructure integrator whose site had to make enterprise grade credibility legible without turning into an alphabet soup of sub brands.",2025,"cTT5liJ1ThIIrb5lFBuALGhykExdVjlTf343A27rA9k",{"id":386,"title":387,"accent":388,"body":389,"completedAt":444,"cover":445,"coverAlt":446,"description":393,"extension":125,"external":126,"liveUrl":126,"meta":447,"navigation":129,"order":448,"path":449,"role":132,"seo":450,"stack":451,"status":139,"stem":452,"summary":453,"year":142,"__hash__":454},"work\u002Fwork\u002Flynne.md","Lynne","#b5696b",{"type":9,"value":390,"toc":439},[391,394,396,399,415,417,420,425,428,434,436],[12,392,393],{},"The practice's name, location, and provider are changed below. The client is one person running a\ntelehealth counseling business, not a company that expects a public case study, so what follows\ndescribes the shape of the problem rather than the specifics of the practice.",[16,395,19],{"id":18},[12,397,398],{},"A solo practitioner's site has one job: turn a visitor into a booked consultation, without a\nreceptionist standing behind it to answer the questions a form did not. The failure mode in this\ncategory is over-building. A multi page marketing site with an About page, a blog, and a services\noverview buries the one action that matters under three others that do not, and a visitor who\ncame to book a session has to find it first.",[24,400,401,405,410],{},[27,402],{"label":403,"note":404,"value":244},"Steps to booking","Schedule, connect, grow, the whole funnel on one page.",[27,406],{"label":407,"note":408,"value":409},"Specialties listed","Grief, anxiety, infertility, and relationship counseling among them, each answering a different search.","8",[27,411],{"label":412,"note":413,"value":414},"Minute free consultation","The single offer every section on the page points back to.","20",[16,416,45],{"id":44},[12,418,419],{},"One page, not several. A warm portrait hero establishes trust before any copy does, a specialties\ngrid lets a visitor confirm their situation is one this practice actually treats, and an FAQ\nanswers insurance, session length, and telehealth efficacy before those questions turn into a\nreason not to book. Every section repeats the same call to action rather than introducing new\nones, so there is only ever one link to decide about.",[59,421],{"alt":422,"caption":423,"src":424},"The three step booking process, schedule, connect, grow, above a free consultation call to action.","Three steps, described in the words a visitor already thinks in rather than the ones a scheduling platform uses.","\u002Fwork\u002Flynne\u002Fshot-booking.png",[12,426,427],{},"There is no CMS behind this. Content is edited directly because a solo practice updates its bio\nand its specialties a few times a year, not weekly, and standing up an editing workflow for that\ncadence costs more than it saves.",[106,429,431],{"title":430,"tone":109},"Sizing the tooling to the update cadence",[12,432,433],{},"A CMS is a cost, not a default. The right question is not whether a client could someday need\none, it is whether the frequency of their actual edits ever gets close to justifying it.",[16,435,101],{"id":100},[12,437,438],{},"Keeping the entire site pointed at one action. A visitor never has to decide which link is the\nimportant one, because there is only ever the one.",{"title":114,"searchDepth":115,"depth":116,"links":440},[441,442,443],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":100,"depth":115,"text":101},"2026-02","\u002Fwork\u002Flynne\u002Fcover.png","The counseling practice home page, showing a warm portrait hero above a three step booking process.",{},5,"\u002Fwork\u002Flynne",{"title":387,"description":393},[305],"work\u002Flynne","A lead capture first booking site for a solo telehealth counseling practice, sized to get one action in front of a visitor rather than a full site's worth of pages.","rTFDIZArq7b60SaaOnnIX3gSUsvrQXtxt2JO0q1ReQM",{"id":456,"title":457,"accent":458,"body":459,"completedAt":126,"cover":512,"coverAlt":513,"description":463,"extension":125,"external":126,"liveUrl":126,"meta":514,"navigation":129,"order":515,"path":516,"role":132,"seo":517,"stack":518,"status":519,"stem":520,"summary":521,"year":142,"__hash__":522},"work\u002Fwork\u002Fn-hub.md","N Brand Portal","#1d4ed8",{"type":9,"value":460,"toc":507},[461,464,466,469,483,485,488,493,496,502,504],[12,462,463],{},"N Brand Portal is the asset library I built for my own project work, once the same question came up often\nenough to notice: which logo file is the current one. Every team answers it the same wrong way at\nfirst, by asking in chat and waiting for whoever exported it last.",[16,465,19],{"id":18},[12,467,468],{},"Brand assets scatter by default. A logo gets exported once, lives on someone's desktop, and gets\nre-exported slightly differently the next time it is needed because the original is nowhere\nfindable. Color values get retyped by hand into a new file and drift a shade at a time. The fix is\nnot a better naming convention, it is removing the situation a naming convention was trying to\npatch: there has to be exactly one place that is unambiguously current.",[24,470,471,475,479],{},[27,472],{"label":473,"note":474,"value":36},"Place assets live","Replaces a shared drive, a Slack thread, and whoever exported the file last.",[27,476],{"label":477,"note":478,"value":244},"Formats per asset","SVG, PNG, and a print ready PDF, generated from the same source file rather than re-exported by hand.",[27,480],{"label":481,"note":482,"value":41},"Version numbers in filenames","The portal always serves current, so nothing is named final-v3-actually-final.svg.",[16,484,45],{"id":44},[12,486,487],{},"Next.js on the front end, assets stored in blob storage and indexed by metadata rather than folder\nstructure, so a search for a color name or a logo variant returns the same result regardless of\nwho uploaded it or when. Every asset is generated in its three formats at upload time instead of\non request, which keeps the portal fast and keeps a designer from ever exporting a PDF by hand\nagain.",[59,489],{"alt":490,"caption":491,"src":492},"The asset library grid, showing logo variants beside a row of brand color swatches with their hex values.","Search and filter sit above the grid, because a small library still gets slow to scan by eye once it holds every past variant.","\u002Fwork\u002Fn-hub\u002Fshot-library.png",[12,494,495],{},"There is no approval workflow. A small team does not need one, and adding one would slow down the\nexact task, grabbing the current logo, that the portal exists to speed up.",[106,497,499],{"title":498,"tone":109},"The part worth repeating",[12,500,501],{},"A brand portal earns its keep by being the place nobody has to ask about, not by being the place\nthat reviews what goes into it. Governance and speed trade against each other, and a small team\nshould spend that trade on speed.",[16,503,101],{"id":100},[12,505,506],{},"Generating every format once at upload rather than leaving it to whoever needs the asset next.\nThe portal being fast to search matters less than it never producing a slightly wrong export\nagain.",{"title":114,"searchDepth":115,"depth":116,"links":508},[509,510,511],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":100,"depth":115,"text":101},"\u002Fwork\u002Fn-hub\u002Fcover.png","The N Brand Portal asset library, showing a searchable grid of logo files and brand color swatches.",{},6,"\u002Fwork\u002Fn-hub",{"title":457,"description":463},[302,303,305],"ongoing","work\u002Fn-hub","An internal brand portal holding the logos, color tokens, and templates a small team actually needs, so the approved asset stops living in an eight month old Slack thread.","TxrXx26GwrLmXSWMyygtGf6iWbmtUx6cf0aW5TEOqo4",{"id":524,"title":525,"accent":526,"body":527,"completedAt":580,"cover":581,"coverAlt":582,"description":531,"extension":125,"external":126,"liveUrl":583,"meta":584,"navigation":129,"order":585,"path":586,"role":132,"seo":587,"stack":588,"status":139,"stem":590,"summary":591,"year":142,"__hash__":592},"work\u002Fwork\u002Freductivist.md","Reductivist","#44403c",{"type":9,"value":528,"toc":575},[529,532,534,537,551,553,556,561,564,570,572],[12,530,531],{},"Reductivist sells three things and calls that a catalog. Most commerce templates assume growth\ntoward more SKUs, more categories, more filters, and apply that assumption from day one, which\nmakes three well made objects look like an unfinished store rather than a finished one.",[16,533,19],{"id":18},[12,535,536],{},"A catalog this size does not need a category nav, a filter sidebar, or a comparison table, and\nbuilding those in anyway because commerce sites are supposed to have them produces a homepage\nthat apologizes for its own size. The actual brief was smaller than a commerce platform expects:\nsell three objects well, and do not let the platform's assumptions about scale outgrow the\ncatalog.",[24,538,539,543,547],{},[27,540],{"label":541,"note":542,"value":244},"Products at launch","A cross pendant and two utility tools, published with nothing added to pad the count.",[27,544],{"label":545,"note":546,"value":36},"Hero carousel","Lifestyle and product photography share one slot instead of splitting into a gallery nobody scrolls.",[27,548],{"label":549,"note":550,"value":41},"Category pages","A flat grid, because a browse by category nav solves a catalog size this store does not have.",[16,552,45],{"id":44},[12,554,555],{},"Next.js on the front end, with the image pipeline carrying most of the design weight since the\nphotography is the product pitch. Stripe handles checkout directly rather than sitting behind a\nfull commerce platform, which keeps the store's entire technical surface proportional to three\nproducts instead of the thousand a typical platform is built to expect.",[59,557],{"alt":558,"caption":559,"src":560},"The flat product grid showing all three Reductivist objects with no category navigation above it.","Three products, one grid, no category nav standing between a visitor and the thing they came to look at.","\u002Fwork\u002Freductivist\u002Fshot-catalog.png",[12,562,563],{},"The commerce decision that mattered was the one made by omission. A platform with filters and\ncomparison tables spends its interface budget solving a browsing problem this catalog does not\nhave yet, and a store that size inherits that unused complexity as clutter rather than capability.",[106,565,567],{"title":566,"tone":109},"The right platform is the smallest one",[12,568,569],{},"A catalog of three does not need the same commerce stack as a catalog of three thousand. Matching\nthe platform to the current size, not the hoped for one, is what keeps the store looking finished\ninstead of early.",[16,571,101],{"id":100},[12,573,574],{},"Resisting the instinct to build category infrastructure before there is a catalog that needs it.\nThe store can grow into a bigger platform later; it should not carry one's assumptions now.",{"title":114,"searchDepth":115,"depth":116,"links":576},[577,578,579],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},{"id":100,"depth":115,"text":101},"2026-06","\u002Fwork\u002Freductivist\u002Fcover.png","The Reductivist home page, showing a hero carousel above a three item product grid.","https:\u002F\u002Freductivist.com",{},7,"\u002Fwork\u002Freductivist",{"title":525,"description":531},[302,589],"Stripe","work\u002Freductivist","A minimalist goods brand selling a handful of well made objects, where the real commerce problem was resisting the urge to add a fourth product to the homepage.","RSDNXTwqe0kpRQuPGMDbn718jcc2V89CdJG1VBerkSY",{"id":594,"title":595,"accent":596,"body":597,"completedAt":658,"cover":659,"coverAlt":660,"description":601,"extension":125,"external":126,"liveUrl":126,"meta":661,"navigation":129,"order":662,"path":663,"role":664,"seo":665,"stack":666,"status":139,"stem":672,"summary":673,"year":142,"__hash__":674},"work\u002Fwork\u002Fpattern-library.md","Pattern Library","#3730a3",{"type":9,"value":598,"toc":653},[599,602,604,607,622,626,629,632,637,640,646,650],[12,600,601],{},"Patterns are recipes, components are ingredients, and a design system is the cookbook. Most\nteams document the ingredients thoroughly and never write down a single recipe, which is why the\nsame navigation argument gets held twice a year.",[16,603,19],{"id":18},[12,605,606],{},"A component library tells you that a button exists and what states it has. It cannot tell you\nwhether this screen wants a button at all. That knowledge lives in people's heads, leaves when\nthey leave, and gets rebuilt from scratch by whoever arrives next. The gap is not a missing\ncomponent, it is a missing named solution to a recurring problem, together with the context that\nmakes it the right answer here and the wrong one two screens over.",[24,608,609,614,618],{},[27,610],{"label":611,"note":612,"value":613},"Documented patterns","Each one written against a fixed template rather than free form notes.","422",[27,615],{"label":616,"note":617,"value":244},"Platform tracks","Web, mobile, and the cross cutting concerns that belong to neither.",[27,619],{"label":620,"note":621,"value":31},"Mobile conventions compared","iOS Human Interface Guidelines and Material 3 divergence is called out per pattern.",[16,623,625],{"id":624},"how-it-is-organised","How it is organised",[12,627,628],{},"Every entry follows the same anatomy: the problem, the context where it applies, the solution,\nthe accessibility requirements, and the cases where the pattern is the wrong choice. That last\nsection is the one people skip writing and the one they come back for.",[12,630,631],{},"The mobile track calls out where the two platform conventions actually disagree, rather than\npicking one and pretending it is universal. A back gesture, a sheet dismissal, and a destructive\nconfirmation are three places where following one platform's guidance on the other produces an\napp that feels subtly wrong and no one can say why.",[59,633],{"alt":634,"caption":635,"src":636},"The pattern browser with the search field focused and category filters visible along the top.","The browser is a single generated HTML file with search and filters, so the library opens instantly and works with no build step or network.","\u002Fwork\u002Fpattern-library\u002Fshot-browser.png",[12,638,639],{},"The cross platform track holds motion, AI interfaces, and a persuasion section that includes a\ntaxonomy of deceptive patterns. Naming a dark pattern precisely is what lets a team decline to\nbuild one without the conversation becoming a matter of taste.",[68,641],{"component":642,"note":643,"origin":644,"title":645},"TransitionDemo","Chrome runs the real View Transitions morph. Everywhere else the same state change happens without the tween, and under reduced motion it is skipped entirely. That progressive story is the pattern, not a caveat attached to it.","the pattern library motion track","Shared element transition, the pattern demonstrating itself",[16,647,649],{"id":648},"what-it-changed","What it changed",[12,651,652],{},"Having the recipes written down moved design review from opinion to reference. The question stops\nbeing whether someone likes the interaction and becomes whether this is the pattern for this\nproblem, and if not, which one is.",{"title":114,"searchDepth":115,"depth":116,"links":654},[655,656,657],{"id":18,"depth":115,"text":19},{"id":624,"depth":115,"text":625},{"id":648,"depth":115,"text":649},"2026-04","\u002Fwork\u002Fpattern-library\u002Fcover.png","The pattern library browser, showing a search field above a filtered grid of pattern cards.",{},8,"\u002Fwork\u002Fpattern-library","Research, writing, and the browser that reads it",{"title":595,"description":601},[667,668,669,670,671],"Markdown","Vanilla JS","iOS HIG","Material 3","WCAG 2.2","work\u002Fpattern-library","Four hundred and twenty two documented interaction patterns across web, mobile, and cross platform concerns, with the rationale that makes each one work.","41UK-LdAizeVKG0rRT8VX6YqnBTrnfdmhfPd7aoUz8Q",{"id":676,"title":677,"accent":678,"body":679,"completedAt":736,"cover":737,"coverAlt":738,"description":683,"extension":125,"external":126,"liveUrl":126,"meta":739,"navigation":129,"order":740,"path":741,"role":742,"seo":743,"stack":744,"status":139,"stem":748,"summary":749,"year":142,"__hash__":750},"work\u002Fwork\u002Fflow-society-mx.md","Flow Society MX","#166534",{"type":9,"value":680,"toc":732},[681,684,686,689,704,706,709,714,717,722,729],[12,682,683],{},"Flow Society runs small group e-bike rides in Bosque La Primavera, plus a membership and a two\nday camping trip. Before this build the bookings arrived through direct messages, and the person\nanswering them was also the person leading the ride.",[16,685,19],{"id":18},[12,687,688],{},"A tour operator's real constraint is not the marketing page, it is the fact that a departure has\na fixed number of seats and a fixed number of bikes, and both are consumed by a booking that has\nnot been confirmed yet. Any form that ignores the held state will eventually sell the same seat\ntwice on a Saturday morning, and the person who finds out is standing in a car park.",[24,690,691,695,700],{},[27,692],{"label":693,"note":694,"value":164},"Steps in the booking flow","Date, trip basics, riders, equipment, logistics, review.",[27,696],{"label":697,"note":698,"value":699},"Fields captured","Including accessory rentals, which used to be a separate message thread.","12",[27,701],{"label":702,"note":703,"value":330},"Experiences seeded","Brunch rides, the hybrid couples format, membership, and the camping trip.",[16,705,45],{"id":44},[12,707,708],{},"Prisma over Postgres holds experiences, departures, bikes, and bookings. Availability is derived\nfrom the departure rather than stored on it, so a held booking and a confirmed one both reduce\nwhat the calendar offers without anyone remembering to decrement a counter. The form is React\nHook Form with Zod schemas shared between the client and the server action, which means the\nvalidation the visitor sees and the validation the database trusts are the same rules.",[59,710],{"alt":711,"caption":712,"src":713},"The booking flow at step three, showing rider details and a running summary of the trip.","The running summary on the right stays filled in across all six steps, so nobody reaches the review screen unsure what they picked.","\u002Fwork\u002Fflow-society-mx\u002Fshot-booking.png",[12,715,716],{},"Sold out and held departures stay on the calendar. Removing them makes the season look emptier\nthan it is and invites the question of whether the operator runs rides at all that month.",[68,718],{"component":719,"note":720,"origin":677,"title":721},"FilterDemo","Try the level filter. Unavailable departures stay visible, stay in the count, and refuse the click, which is what a paper calendar with a line through it has always done.","Departure picker with unavailable dates left in",[12,723,724,725,728],{},"The operator dashboard at ",[50,726,727],{},"\u002Fadmin"," confirms or declines bookings and blocks dates. Email runs\nthrough Resend, with a console fallback so the whole flow can be developed and demonstrated\nwithout an account or an API key.",[12,730,731],{},"::callout{title=\"A note on the free tier\"\n}\nEvery service in the stack sits inside a free plan: Neon for Postgres, Resend for mail. That was\na requirement, not a shortcut. A small operator will abandon a system whose\nrunning cost arrives before its first booking does.\n::",{"title":114,"searchDepth":115,"depth":116,"links":733},[734,735],{"id":18,"depth":115,"text":19},{"id":44,"depth":115,"text":45},"2026-08","\u002Fwork\u002Fflow-society-mx\u002Fcover.png","The Flow Society MX booking page, showing a departure calendar beside a trail photograph.",{},9,"\u002Fwork\u002Fflow-society-mx","Product design and full stack build",{"title":677,"description":683},[135,136,745,746,747,214],"Prisma 6","PostgreSQL","Radix UI","work\u002Fflow-society-mx","An e-bike tour operator in Jalisco, with a six step booking flow, a per departure availability calendar, and an operator dashboard behind it.","5GP-xfidXkZW2hCaT2NG0xybqPRpGePuvtAMCksH9cg",{"id":752,"title":753,"accent":754,"body":755,"completedAt":865,"cover":866,"coverAlt":867,"description":759,"extension":125,"external":126,"liveUrl":868,"meta":869,"navigation":129,"order":870,"path":871,"role":132,"seo":872,"stack":873,"status":139,"stem":879,"summary":880,"year":142,"__hash__":881},"work\u002Fwork\u002Fcytron-motorcycles.md","Cytron Motorcycles","#a21caf",{"type":9,"value":756,"toc":858},[757,760,762,765,768,784,792,796,799,806,812,816,819,822,825,831,835,838,841,847,853,855],[12,758,759],{},"Cytron builds custom streetfighters out of a workshop in Taito City, and every word of that\nsentence is invented. The brand has no factory and no catalogue. What it has is a range of ten\nbikes, a header in six languages, an enquiry form that reaches somebody, and a homepage that now\nruns in a browser rather than inside a prototyping tool.",[16,761,19],{"id":18},[12,763,764],{},"A concept brand is the easiest thing in the world to design badly, because nothing pushes back.\nThere is no client to say the bikes do not look like that, no CMS to say the copy will not fit,\nand no engineer to ask what happens on hover. The work either invents its own resistance or it\nproduces a very pretty picture of a website that could not be built.",[12,766,767],{},"So the constraint I set before drawing anything was that the file had to behave like a prototype,\nand that whatever the prototype claimed had to survive being built. Every state a visitor could\nreach existed first as a real variant and later as a real component, and nothing was allowed to\nstay a note in the margin about what would happen if you moused over the card.",[24,769,770,775,780],{},[27,771],{"label":772,"note":773,"value":774},"Models in the range","CT-1 holds the hero. CT-2 through CT-10 fill the grid underneath it.","10",[27,776],{"label":777,"note":778,"value":779},"Components with a second state","Every card, nav item, button, and scroll cue drawn twice, so every state was real before it was built.","16",[27,781],{"label":782,"note":783,"value":164},"Languages in the header","English plus five more, each named in its own language, with a currency switch beside them.",[78,785],{"alt":786,"caption":787,"poster":788,"ratio":789,"src":790,"webm":791},"The Cytron hero cycling through its hover states: the nav items, the language menu opening, the enquiry arrow turning from diagonal to horizontal, and the chat bubble opening into a Let us talk label.","Twenty seconds of the hero doing nothing but change state. The language menu opens about a third of the way in, which is where both of the words the design file could never catch are sitting in plain view.","\u002Fwork\u002Fcytron-motorcycles\u002Freel-poster.png","4 \u002F 3","\u002Fwork\u002Fcytron-motorcycles\u002Freel.mp4","\u002Fwork\u002Fcytron-motorcycles\u002Freel.webm",[16,793,795],{"id":794},"why-sveltekit-and-not-the-usual-thing","Why SvelteKit and not the usual thing",[12,797,798],{},"Almost everything else I ship is React on Next.js, and reaching for it again would have answered\na question I had already answered several times. Cytron is a page whose entire argument is\nmotion. A card has to grow without shoving its neighbours, a lime wireframe line has to run the\nlength of the document without tearing, and the whole thing has to hold its frame rate on a mid\nrange phone. That is a budget problem before it is a component problem.",[12,800,801,802,805],{},"Svelte compiles its components away, so what ships is the work the page does rather than a\nruntime plus the work the page does. Svelte 5 runes hold the hover and variant state, form\nactions in ",[50,803,804],{},"+page.server.ts"," take the enquiry without an API route sitting in front of them, and\nGSAP ScrollTrigger drives the single continuous vector that crosses every section. The six\nlocales come out of Paraglide, which compiles messages the same way Svelte compiles components,\nso a language nobody picked costs the visitor nothing at all.",[59,807],{"alt":808,"caption":809,"ratio":810,"src":811},"The Cytron models grid, nine motorcycles in three rows of three, each labelled CT-2 through CT-10 in lime monospace.","Nine bikes, one card component, nine instances. The label under each one is the only thing that differs, which is what makes the grid a system rather than nine separate drawings.","1440 \u002F 1180","\u002Fwork\u002Fcytron-motorcycles\u002Fshot-models.png",[16,813,815],{"id":814},"drawn-as-a-prototype-then-built-as-one","Drawn as a prototype, then built as one",[12,817,818],{},"The model card is where the discipline paid for itself twice. In the file its default is 360 by\n248 and its hover is 400 by 275, drawn side by side in a component set rather than described in a\ncomment. In the build that pair collapses to one component and one scale transform, because the\ntwo frames share a ratio and the design meant a bigger painting rather than a bigger box.",[12,820,821],{},"The enquiry button does the same thing, and its hover does something specific. The arrow rotates\nfrom a diagonal to a horizontal, a forty five degree turn that only becomes a decision once\nsomebody has drawn both frames. A sticky note parked beside the button on the canvas says people\nare more likely to click a button that has an arrow on it, which stayed true once the button was\na real button that real hands could reach.",[12,823,824],{},"The six feature icons are held together by the same habit. A second note on the canvas set the\nrule that they match in complexity and in whether they are stroked or filled, and that each one\nis named for what it is used for rather than for what it draws. Six icons is exactly the number\nwhere that rule stops being obvious and starts being necessary, and it is the reason they left\nthe file as one sprite rather than six separate arguments.",[59,826],{"alt":827,"caption":828,"ratio":829,"src":830},"Two alternating story blocks on the Cytron page, a night rider photograph beside body copy and an engine cutout beside more copy, with thin lime wireframe lines crossing the black background.","The thin lime lines running behind everything are one continuous vector spanning the whole page, so the sections read as cuts through a single object rather than a stack of separate bands.","1440 \u002F 1300","\u002Fwork\u002Fcytron-motorcycles\u002Fshot-story.png",[16,832,834],{"id":833},"what-the-build-caught-and-the-file-could-not","What the build caught and the file could not",[12,836,837],{},"A design file cannot tell you that you got a word wrong. The language menu lists its options in\ntheir own languages, which is the right instinct and the reason the pattern works at all, and the\ndrawn version then got two of the six wrong in the same breath. French is written francés, which\nis the Spanish word for it, and Chinese is written 中国人, which names a person rather than a\nlanguage. Nothing inside Figma can raise its hand about either. A locale file can, and did: the\nkeys were right and two of the strings were not, and neither survived being typed into a\ncatalogue where every language has to sit in the same column.",[12,839,840],{},"The same gap showed up in the feature copy, where three of the six descriptions were still lorem\nipsum, and in the testimonial, where the quote was the words \"It was a very good experience\"\nattributed to Dave. In a picture of a website those read as finished. In a built one they are the\nfirst thing a content pass deletes, and that is exactly what happened to them.",[59,842],{"alt":843,"caption":844,"ratio":845,"src":846},"The Cytron contact section, with a dark map of Taito City pinned in lime beside a four field enquiry form and a lime send button.","The map is styled down to two greys so the lime pin is the only saturated thing in the frame, and the form is four underlines rather than four heavy boxes.","1440 \u002F 1040","\u002Fwork\u002Fcytron-motorcycles\u002Fshot-contact.png",[106,848,850],{"title":849,"tone":109},"A prototype is an argument you can lose",[12,851,852],{},"Drawing both states of sixteen components takes longer than drawing one state of sixteen\ncomponents, and the payment for it arrives later, when somebody sits down to build the thing and\nthe file answers the question instead of shrugging at it. A picture of a website can only ever\nagree with you.",[16,854,101],{"id":100},[12,856,857],{},"The rule that a state has to exist before it can be claimed. It is the same rule I would apply to\na component library, and applying it inside a design file is what stopped that file from being a\nmood board with a nav bar on top. It is also what made the build short. Ten bikes, six languages,\nand one continuous lime line took less time to write than the file took to draw, because every\nquestion the code was going to ask had already been settled on the canvas.",{"title":114,"searchDepth":115,"depth":116,"links":859},[860,861,862,863,864],{"id":18,"depth":115,"text":19},{"id":794,"depth":115,"text":795},{"id":814,"depth":115,"text":815},{"id":833,"depth":115,"text":834},{"id":100,"depth":115,"text":101},"2026-09","\u002Fwork\u002Fcytron-motorcycles\u002Fcover.png","The Cytron home page hero, showing the CT-1 motorcycle lit magenta and orange in a graffitied garage beneath a lime wordmark and a three item nav.","https:\u002F\u002Fcytron-kohl.vercel.app",{},10,"\u002Fwork\u002Fcytron-motorcycles",{"title":753,"description":759},[874,875,876,877,878],"SvelteKit 2","Svelte 5","GSAP ScrollTrigger","Paraglide","Vercel","work\u002Fcytron-motorcycles","A Tokyo motorcycle brand invented end to end, drawn as a working prototype and then built as the site that prototype had been arguing for.","phdxjplfQEkPk-rCmyb1L3MtgKgY3lSH9wVuWP69KEI",1789230933746]