my blog https://overreacted.io

Compare changes

Choose any two refs to compare.

Changed files
+2232 -1115
.tangled
workflows
app
public
a-complete-guide-to-useeffect
a-lean-syntax-primer
hire-me-in-japan
how-imports-work-in-rsc
how-to-fix-any-bug
impossible-components
introducing-rsc-explorer
lean-for-javascript-developers
open-social
react-as-a-ui-runtime
the-math-is-haunted
the-two-reacts
where-its-at
+17
.tangled/workflows/deploy.yaml
··· 1 + engine: nixery 2 + when: 3 + - event: ["push", "pull_request"] 4 + branch: ["main"] 5 + 6 + dependencies: 7 + nixpkgs: 8 + - nodejs 9 + 10 + steps: 11 + - name: build site 12 + command: | 13 + npm install 14 + npm run build 15 + - name: deploy 16 + command: | 17 + npx --yes wrangler pages deploy --branch main --project-name overreacted ./out/
+4 -3
app/Link.tsx
··· 27 27 ...rest 28 28 }: { 29 29 className?: string; 30 - children: React.ReactNode; 30 + children?: React.ReactNode; 31 31 style?: React.CSSProperties; 32 32 href: string; 33 33 target?: string; 34 34 } & React.ComponentProps<typeof NextLink>) { 35 35 const router = useRouter(); 36 36 const [isNavigating, trackNavigation] = useTransition(); 37 - if (!target && !href.startsWith("/") && !href.startsWith("#")) { 37 + const isExternal = /^https?:\/\//.test(href); 38 + if (!target && isExternal) { 38 39 target = "_blank"; 39 40 } 40 41 return ( ··· 50 51 }); 51 52 } 52 53 }} 53 - className={[className, `scale-100 active:scale-100`].join(" ")} 54 + className={[className, "scale-100 active:scale-100"].join(" ")} 54 55 style={{ 55 56 ...style, 56 57 transform: isNavigating ? "scale(1)" : "",
+16
app/TextLink.tsx
··· 1 + import Link from "./Link"; 2 + 3 + export default function TextLink({ 4 + className, 5 + ...props 6 + }: React.ComponentProps<typeof Link>) { 7 + return ( 8 + <Link 9 + {...props} 10 + className={[ 11 + "underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]", 12 + className, 13 + ].join(" ")} 14 + /> 15 + ); 16 + }
+1 -1
app/[slug]/layout.tsx
··· 4 4 return ( 5 5 <> 6 6 {children} 7 - <footer className="mt-12"> 7 + <footer className="mt-20"> 8 8 <HomeLink /> 9 9 </footer> 10 10 </>
+50 -141
app/[slug]/markdown.css
··· 1 + /* CSS vars for code block customization (used by Wrapper components) */ 1 2 .markdown { 2 - line-height: 28px; 3 - --path: none; 4 - --radius-top: 12px; 5 - --radius-bottom: 12px; 6 - --padding-top: 1rem; 7 - --padding-bottom: 1rem; 8 - } 9 - 10 - .markdown p { 11 - @apply pb-8; 12 - } 13 - 14 - .markdown a:not(.tip):not(.linked-heading) { 15 - @apply border-b-[1px] border-[--link] text-[--link]; 16 - } 17 - 18 - .markdown hr { 19 - @apply pt-8 opacity-60 dark:opacity-10; 20 - } 21 - 22 - .markdown h2 { 23 - @apply mt-2 pb-8 text-3xl font-bold; 24 - } 25 - 26 - .markdown h3 { 27 - @apply mt-2 pb-8 text-2xl font-bold; 28 - } 29 - 30 - .markdown h4 { 31 - @apply mt-2 pb-8 text-xl font-bold; 32 - } 33 - 34 - .markdown :is(h1, h2, h3, h4) a:is(:hover, :focus, :active)::before, 35 - .markdown :is(h1, h2, h3, h4):is(:target, :focus) a::before { 36 - content: "#"; 37 - position: absolute; 38 - transform: translate(-1em); 39 - opacity: 0.7; 40 - } 41 - 42 - .markdown :not(pre) > code { 43 - border-radius: 10px; 44 - background: var(--inlineCode-bg); 45 - color: var(--inlineCode-text); 46 - padding: 0.15em 0.2em 0.05em; 47 - white-space: normal; 48 - } 49 - 50 - .markdown pre { 51 - @apply -mx-4 mb-8 overflow-y-auto p-4 text-sm; 52 - clip-path: var(--path); 53 - border-top-right-radius: var(--radius-top); 54 - border-top-left-radius: var(--radius-top); 55 - border-bottom-right-radius: var(--radius-bottom); 56 - border-bottom-left-radius: var(--radius-bottom); 57 - padding-top: var(--padding-top); 58 - padding-bottom: var(--padding-bottom); 59 - } 60 - 61 - .markdown pre code { 62 - width: auto; 63 - } 64 - 65 - .markdown blockquote { 66 - @apply relative -left-2 -ml-4 mb-8 pl-4; 67 - font-style: italic; 68 - border-left: 3px solid hsla(0, 0%, 0%, 0.9); 69 - border-left-color: inherit; 70 - opacity: 0.8; 71 - } 72 - 73 - .markdown blockquote p { 74 - margin: 0; 75 - padding: 0; 76 - } 77 - 78 - .markdown p img { 79 - margin-bottom: 0; 80 - } 81 - 82 - .markdown ul:not(.unstyled) { 83 - @apply list-inside md:list-outside list-disc; 84 - margin-top: 0; 85 - padding-bottom: 0; 86 - padding-left: 0; 87 - padding-right: 0; 88 - padding-top: 0; 89 - margin-bottom: 1.75rem; 90 - list-style-image: none; 91 - } 92 - 93 - .markdown li:not(.unstyled) { 94 - margin-bottom: calc(1.75rem / 2); 95 - } 96 - 97 - .markdown img { 98 - @apply mb-8; 99 - max-width: 100%; 3 + line-height: 28px; 4 + --path: none; 5 + --radius-top: 12px; 6 + --radius-bottom: 12px; 7 + --padding-top: 1rem; 8 + --padding-bottom: 1rem; 100 9 } 101 10 102 11 .markdown iframe { 103 - @apply mb-8; 104 - max-width: 100%; 105 - } 106 - 107 - .markdown ol { 108 - @apply mb-8 list-inside md:list-outside list-decimal; 12 + width: calc(100% + 2rem) !important; 13 + margin-left: -1rem; 109 14 } 110 15 111 - .markdown input { 112 - color: #222; 16 + @media (prefers-color-scheme: dark) { 17 + .markdown iframe { 18 + border-color: #444 !important; 19 + } 113 20 } 114 21 115 - .markdown table { 116 - width: 100%; 117 - border-collapse: collapse; 118 - margin-bottom: 1.5rem; 22 + @media (max-width: 672px) { 23 + .markdown pre, 24 + .markdown iframe { 25 + width: calc(100% + 2.5rem) !important; 26 + margin-left: -1.25rem; 27 + margin-right: -1.25rem; 28 + } 119 29 } 120 30 121 - .markdown th, 122 - .markdown td { 123 - border: 1px solid #dcdcdc; 124 - padding: 8px; 125 - text-align: left; 31 + @media (max-width: 550px) { 32 + .markdown pre { 33 + border-radius: 0 !important; 34 + } 35 + .markdown iframe { 36 + border-left: none !important; 37 + border-right: none !important; 38 + } 126 39 } 127 40 128 - @media (prefers-color-scheme: dark) { 129 - .markdown input { 130 - color: #111; 131 - } 132 - } 133 - 41 + /* Code line highlighting - data-attribute from rehype-pretty-code */ 134 42 .markdown pre [data-highlighted-line] { 135 - margin-left: -16px; 136 - margin-right: -16px; 137 - padding-left: 12px; 138 - border-left: 4px solid #ffa7c4; 139 - background-color: #022a4b; 140 - display: block; 141 - padding-right: 1em; 43 + margin-left: -16px; 44 + margin-right: -16px; 45 + padding-left: 12px; 46 + border-left: 4px solid #ffa7c4; 47 + background-color: #022a4b; 48 + display: block; 49 + padding-right: 1em; 142 50 } 143 51 52 + /* Tip button styles */ 144 53 .tip { 145 - @apply inline-block px-8 py-4 font-sans font-semibold text-xl rounded-full shadow-xl transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-opacity-50 border-b-[1px] text-white overflow-clip transition-transform; 146 - border-color: var(--link); 54 + @apply inline-block px-8 py-4 font-sans font-semibold text-xl rounded-full shadow-xl transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-opacity-50 border-b-[1px] text-white overflow-clip transition-transform; 55 + border-color: var(--link); 147 56 } 148 57 149 58 .tip.tip-sm { 150 - @apply inline-block px-6 py-2 text-lg shadow-md; 59 + @apply inline-block px-6 py-2 text-lg shadow-md; 151 60 } 152 61 153 62 .tip-bg { 154 - background-image: linear-gradient(45deg, var(--purple), var(--pink)); 155 - position: absolute; 156 - top: 0; 157 - left: 0; 158 - right: 0; 159 - bottom: 0; 160 - z-index: -1; 63 + background-image: linear-gradient(45deg, var(--purple), var(--pink)); 64 + position: absolute; 65 + top: 0; 66 + left: 0; 67 + right: 0; 68 + bottom: 0; 69 + z-index: -1; 161 70 } 162 71 163 72 @media (prefers-color-scheme: dark) { 164 - .tip-bg { 165 - filter: brightness(0.46); 166 - } 73 + .tip-bg { 74 + filter: brightness(0.46); 75 + } 167 76 }
+166
app/[slug]/markdown.tsx
··· 1 + "use client"; 2 + 3 + export function P(props: React.ComponentProps<"p">) { 4 + return <p {...props} />; 5 + } 6 + 7 + export function H2({ id, children, ...props }: React.ComponentProps<"h2">) { 8 + return ( 9 + <h2 10 + id={id} 11 + className="group relative text-3xl font-bold mt-2" 12 + {...props} 13 + > 14 + <a href={`#${id}`} className="no-underline text-inherit"> 15 + <span 16 + aria-hidden 17 + className="absolute -translate-x-[1em] opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 group-[:target]:opacity-70" 18 + > 19 + # 20 + </span> 21 + {children} 22 + </a> 23 + </h2> 24 + ); 25 + } 26 + 27 + export function H3({ id, children, ...props }: React.ComponentProps<"h3">) { 28 + return ( 29 + <h3 30 + id={id} 31 + className="group relative text-2xl font-bold mt-2" 32 + {...props} 33 + > 34 + <a href={`#${id}`} className="no-underline text-inherit"> 35 + <span 36 + aria-hidden 37 + className="absolute -translate-x-[1em] opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 group-[:target]:opacity-70" 38 + > 39 + # 40 + </span> 41 + {children} 42 + </a> 43 + </h3> 44 + ); 45 + } 46 + 47 + export function H4({ id, children, ...props }: React.ComponentProps<"h4">) { 48 + return ( 49 + <h4 50 + id={id} 51 + className="group relative text-xl font-bold mt-2" 52 + {...props} 53 + > 54 + <a href={`#${id}`} className="no-underline text-inherit"> 55 + <span 56 + aria-hidden 57 + className="absolute -translate-x-[1em] opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 group-[:target]:opacity-70" 58 + > 59 + # 60 + </span> 61 + {children} 62 + </a> 63 + </h4> 64 + ); 65 + } 66 + 67 + export function Blockquote(props: React.ComponentProps<"blockquote">) { 68 + return ( 69 + <blockquote 70 + className="relative -left-2 -ml-4 pl-4 italic opacity-80 border-l-[3px] border-current" 71 + {...props} 72 + /> 73 + ); 74 + } 75 + 76 + export function UL(props: React.ComponentProps<"ul">) { 77 + return <ul className="list-inside md:list-outside list-disc" {...props} />; 78 + } 79 + 80 + export function OL(props: React.ComponentProps<"ol">) { 81 + return ( 82 + <ol className="list-inside md:list-outside list-decimal" {...props} /> 83 + ); 84 + } 85 + 86 + export function LI(props: React.ComponentProps<"li">) { 87 + return <li className="mb-3 last:mb-0" {...props} />; 88 + } 89 + 90 + export function Pre({ 91 + style, 92 + ...props 93 + }: React.ComponentProps<"pre">) { 94 + return ( 95 + <pre 96 + className="-mx-4 overflow-y-auto p-4 text-sm" 97 + {...props} 98 + style={{ 99 + ...style, 100 + clipPath: "var(--path, none)", 101 + borderTopLeftRadius: "var(--radius-top, 12px)", 102 + borderTopRightRadius: "var(--radius-top, 12px)", 103 + borderBottomLeftRadius: "var(--radius-bottom, 12px)", 104 + borderBottomRightRadius: "var(--radius-bottom, 12px)", 105 + paddingTop: "var(--padding-top, 1rem)", 106 + paddingBottom: "var(--padding-bottom, 1rem)", 107 + }} 108 + /> 109 + ); 110 + } 111 + 112 + export function Code({ 113 + className, 114 + ...props 115 + }: React.ComponentProps<"code"> & { "data-language"?: string }) { 116 + // Code blocks have data-language from rehype-pretty-code (defaultLang ensures all blocks have it) 117 + if ("data-language" in props) { 118 + return <code className={className} {...props} />; 119 + } 120 + // Inline code styling 121 + return ( 122 + <code 123 + className="rounded-[10px] bg-[--inlineCode-bg] text-[--inlineCode-text] px-[0.2em] py-[0.15em] whitespace-normal" 124 + {...props} 125 + /> 126 + ); 127 + } 128 + 129 + export function Table(props: React.ComponentProps<"table">) { 130 + return <table className="w-full border-collapse" {...props} />; 131 + } 132 + 133 + export function Th(props: React.ComponentProps<"th">) { 134 + return ( 135 + <th 136 + className="border border-gray-300 dark:border-gray-600 p-2 text-left" 137 + {...props} 138 + /> 139 + ); 140 + } 141 + 142 + export function Td(props: React.ComponentProps<"td">) { 143 + return ( 144 + <td 145 + className="border border-gray-300 dark:border-gray-600 p-2 text-left" 146 + {...props} 147 + /> 148 + ); 149 + } 150 + 151 + export function Hr(props: React.ComponentProps<"hr">) { 152 + return <hr className="opacity-60 dark:opacity-10 mt-4" {...props} />; 153 + } 154 + 155 + export function Img(props: React.ComponentProps<"img">) { 156 + return <img className="max-w-full" {...props} />; 157 + } 158 + 159 + export function A(props: React.ComponentProps<"a">) { 160 + return ( 161 + <a 162 + className="border-b border-[--link] text-[--link]" 163 + {...props} 164 + /> 165 + ); 166 + }
+54 -52
app/[slug]/page.tsx
··· 2 2 import { readdir, readFile } from "fs/promises"; 3 3 import matter from "gray-matter"; 4 4 import { MDXRemote } from "next-mdx-remote-client/rsc"; 5 - import Link from "../Link"; 5 + import TextLink from "../TextLink"; 6 6 import { sans } from "../fonts"; 7 7 import remarkSmartpants from "remark-smartypants"; 8 8 import rehypePrettyCode from "rehype-pretty-code"; 9 9 import rehypeSlug from "rehype-slug"; 10 - import rehypeAutolinkHeadings from "rehype-autolink-headings"; 11 10 import { remarkMdxEvalCodeBlock } from "./mdx"; 12 11 import overnight from "overnight/themes/Overnight-Slumber.json"; 13 12 import "./markdown.css"; 14 13 import remarkGfm from "remark-gfm"; 14 + import * as markdown from "./markdown"; 15 15 16 16 overnight.colors["editor.background"] = "var(--code-bg)"; 17 17 ··· 33 33 } 34 34 let Wrapper = postComponents.Wrapper ?? Fragment; 35 35 const { content, data } = matter(file); 36 - const isDraft = new Date(data.date).getFullYear() > new Date().getFullYear(); 37 - const editUrl = `https://github.com/gaearon/overreacted.io/edit/main/public/${encodeURIComponent( 36 + const editUrl = `https://tangled.org/@danabra.mov/overreacted/blob/main/public/${encodeURIComponent( 38 37 slug, 39 - )}/index.md`; 38 + )}/index.md?code=true`; 40 39 return ( 41 40 <> 42 41 <article> ··· 55 54 year: "numeric", 56 55 })} 57 56 </p> 58 - <div className="markdown"> 59 - <div className="mb-8 relative md:-left-6 flex flex-wrap items-baseline"> 60 - {!data.nocta && ( 61 - <a 62 - href="https://ko-fi.com/gaearon" 63 - target="_blank" 64 - className="mt-10 tip tip-sm mr-4" 65 - > 66 - <span className="tip-bg" /> 67 - Pay what you like 68 - </a> 69 - )} 70 - {data.youtube && ( 71 - <a 72 - className="leading-tight mt-4" 73 - href={data.youtube} 74 - target="_blank" 75 - > 76 - <span className="hidden min-[400px]:inline">Watch on </span> 77 - YouTube 78 - </a> 79 - )} 80 - </div> 57 + <div className="markdown flex flex-col gap-8 mt-12"> 58 + {(!data.nocta || data.youtube) && ( 59 + <div className="relative md:-left-6 flex flex-wrap items-baseline gap-4"> 60 + {!data.nocta && ( 61 + <a 62 + href="https://ko-fi.com/gaearon" 63 + target="_blank" 64 + className="tip tip-sm" 65 + > 66 + <span className="tip-bg" /> 67 + Pay what you like 68 + </a> 69 + )} 70 + {data.youtube && ( 71 + <TextLink href={data.youtube}> 72 + <span className="hidden min-[400px]:inline">Watch on </span> 73 + YouTube 74 + </TextLink> 75 + )} 76 + </div> 77 + )} 81 78 82 79 <Wrapper> 80 + <div className="flex flex-col gap-8"> 83 81 <MDXRemote 84 82 source={content} 85 83 components={{ 86 - a: Link, 84 + p: markdown.P, 85 + h2: markdown.H2, 86 + h3: markdown.H3, 87 + h4: markdown.H4, 88 + blockquote: markdown.Blockquote, 89 + ul: markdown.UL, 90 + ol: markdown.OL, 91 + li: markdown.LI, 92 + pre: markdown.Pre, 93 + code: markdown.Code, 94 + table: markdown.Table, 95 + th: markdown.Th, 96 + td: markdown.Td, 97 + hr: markdown.Hr, 98 + a: (props: React.ComponentProps<"a">) => ( 99 + <TextLink {...props} href={props.href ?? ""} /> 100 + ), 87 101 img: async ({ src, ...rest }) => { 88 102 if ( 89 103 src && ··· 122 136 finalSrc = `/${slug}/${src}`; 123 137 } 124 138 125 - return <img src={finalSrc} {...rest} />; 139 + return <markdown.Img src={finalSrc} {...rest} />; 126 140 }, 127 141 Video: ({ src, ...rest }) => { 128 142 let finalSrc = src; ··· 147 161 rehypePrettyCode, 148 162 { 149 163 theme: overnight, 164 + defaultLang: { block: "text" }, 150 165 }, 151 166 ], 152 167 [rehypeSlug], 153 - [ 154 - rehypeAutolinkHeadings, 155 - { 156 - behavior: "wrap", 157 - properties: { 158 - className: "linked-heading", 159 - target: "_self", 160 - }, 161 - }, 162 - ], 163 168 ] as any, 164 169 } as any, 165 170 }} 166 171 /> 172 + </div> 167 173 </Wrapper> 168 174 {!data.nocta && ( 169 - <div className="flex flex-wrap items-baseline"> 175 + <div className="flex flex-wrap items-baseline gap-4 relative md:-left-8"> 170 176 <a 171 177 href="https://ko-fi.com/gaearon" 172 178 target="_blank" 173 - className="tip mb-8 relative md:-left-8" 179 + className="tip" 174 180 > 175 181 <span className="tip-bg" /> 176 182 Pay what you like 177 183 </a> 178 - <a 179 - className="leading-tight ml-4 relative md:-left-8" 180 - href="/im-doing-a-little-consulting/" 181 - > 182 - Hire me 183 - </a> 184 + <TextLink href="/hire-me-in-japan/">Hire me</TextLink> 184 185 </div> 185 186 )} 186 - <hr /> 187 + <hr className="opacity-60 dark:opacity-10" /> 187 188 <p> 188 189 {data.bluesky && ( 189 190 <> 190 - <Link href={data.bluesky}>Discuss on Bluesky</Link> 191 + <TextLink href={data.bluesky}>Discuss on Bluesky</TextLink> 191 192 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 192 193 </> 193 194 )} 194 195 {data.youtube && ( 195 196 <> 196 - <Link href={data.youtube}>Watch on YouTube</Link> 197 + <TextLink href={data.youtube}>Watch on YouTube</TextLink> 197 198 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 198 199 </> 199 200 )} 200 - <Link href={editUrl}>Edit on GitHub</Link> 201 + {/* TODO: This should say Edit when Tangled adds an editor. */} 202 + <TextLink href={editUrl}>Fork on Tangled</TextLink> 201 203 </p> 202 204 </div> 203 205 </article>
+1
public/_redirects
··· 1 + /lean-for-javascript-developers/ /a-lean-syntax-primer/
+6 -6
public/a-complete-guide-to-useeffect/index.md
··· 89 89 } 90 90 ``` 91 91 92 - What does it mean? Does `count` somehow “watch” changes to our state and update automatically? That might be a useful first intuition when you learn React but it’s *not* an [accurate mental model](https://overreacted.io/react-as-a-ui-runtime/). 92 + What does it mean? Does `count` somehow “watch” changes to our state and update automatically? That might be a useful first intuition when you learn React but it’s *not* an [accurate mental model](/react-as-a-ui-runtime/). 93 93 94 94 **In this example, `count` is just a number.** It’s not a magic “data binding”, a “watcher”, a “proxy”, or anything else. It’s a good old number like this one: 95 95 ··· 140 140 141 141 The key takeaway is that the `count` constant inside any particular render doesn’t change over time. It’s our component that’s called again — and each render “sees” its own `count` value that’s isolated between renders. 142 142 143 - *(For an in-depth overview of this process, check out my post [React as a UI Runtime](https://overreacted.io/react-as-a-ui-runtime/).)* 143 + *(For an in-depth overview of this process, check out my post [React as a UI Runtime](/react-as-a-ui-runtime/).)* 144 144 145 145 ## Each Render Has Its Own Event Handlers 146 146 ··· 190 190 191 191 Go ahead and [try it yourself!](https://codesandbox.io/s/w2wxl3yo0l) 192 192 193 - If the behavior doesn’t quite make sense to you, imagine a more practical example: a chat app with the current recipient ID in the state, and a Send button. [This article](https://overreacted.io/how-are-function-components-different-from-classes/) explores the reasons in depth but the correct answer is 3. 193 + If the behavior doesn’t quite make sense to you, imagine a more practical example: a chat app with the current recipient ID in the state, and a Send button. [This article](/how-are-function-components-different-from-classes/) explores the reasons in depth but the correct answer is 3. 194 194 195 195 The alert will “capture” the state at the time I clicked the button. 196 196 ··· 394 394 395 395 **Conceptually, you can imagine effects are a *part of the render result*.** 396 396 397 - Strictly saying, they’re not (in order to [allow Hook composition](https://overreacted.io/why-do-hooks-rely-on-call-order/) without clumsy syntax or runtime overhead). But in the mental model we’re building up, effect functions *belong* to a particular render in the same way that event handlers do. 397 + Strictly saying, they’re not (in order to [allow Hook composition](/why-do-hooks-rely-on-call-order/) without clumsy syntax or runtime overhead). But in the mental model we’re building up, effect functions *belong* to a particular render in the same way that event handlers do. 398 398 399 399 --- 400 400 ··· 518 518 519 519 **It doesn’t matter whether you read from props or state “early” inside of your component.** They’re not going to change! Inside the scope of a single render, props and state stay the same. (Destructuring props makes this more obvious.) 520 520 521 - Of course, sometimes you *want* to read the latest rather than captured value inside some callback defined in an effect. The easiest way to do it is by using refs, as described in the last section of [this article](https://overreacted.io/how-are-function-components-different-from-classes/). 521 + Of course, sometimes you *want* to read the latest rather than captured value inside some callback defined in an effect. The easiest way to do it is by using refs, as described in the last section of [this article](/how-are-function-components-different-from-classes/). 522 522 523 523 Be aware that when you want to read the *future* props or state from a function in a *past* render, you’re swimming against the tide. It’s not *wrong* (and in some cases necessary) but it might look less “clean” to break out of the paradigm. This is an intentional consequence because it helps highlight which code is fragile and depends on timing. In classes, it’s less obvious when this happens. 524 524 ··· 628 628 629 629 ## Synchronization, Not Lifecycle 630 630 631 - One of my favorite things about React is that it unifies describing the initial render result and the updates. This [reduces the entropy](https://overreacted.io/the-bug-o-notation/) of your program. 631 + One of my favorite things about React is that it unifies describing the initial render result and the updates. This [reduces the entropy](/the-bug-o-notation/) of your program. 632 632 633 633 Say my component looks like this: 634 634
public/a-lean-syntax-primer/1.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/10.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/11.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/12.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/13.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/14.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/15.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/16.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/17.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/18.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/19.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/2.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/20.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/24.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/25.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/26.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/27.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/3.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/4.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/5.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/6.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/7.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/8.png

This is a binary file and will not be displayed.

public/a-lean-syntax-primer/9.png

This is a binary file and will not be displayed.

+891
public/a-lean-syntax-primer/index.md
··· 1 + --- 2 + title: A Lean Syntax Primer 3 + date: '2025-09-02' 4 + spoiler: Programming with proofs. 5 + bluesky: https://bsky.app/profile/danabra.mov/post/3lxuhfavafc2p 6 + --- 7 + 8 + This is my opinionated syntax primer for the [Lean](https://lean-lang.org/) programming language. It is far from complete and may contain inaccuracies (I'm still learning Lean myself) but this is how I wish I was introduced to it, and what I wish was clarified. 9 + 10 + --- 11 + 12 + ### Why Lean? 13 + 14 + This post assumes you're already eager to learn a bit of Lean. For motivation, I humbly submit to you two takes: [one from me](https://bsky.app/profile/danabra.mov/post/3lxjfxdlow22m) and [one from its creator](https://www.amazon.science/blog/how-the-lean-language-brings-math-to-coding-and-coding-to-math). 15 + 16 + --- 17 + 18 + ### Declaring Definitions 19 + 20 + Let's start by writing a few *definitions*. These can appear at the top level of the file: 21 + 22 + ```lean 23 + def name := "Alice" 24 + def age := 42 25 + ``` 26 + 27 + *([Try it in the online playground.](https://live.lean-lang.org/#codez=CYUwZgBAdghgtiCAuAvBARAQQDYEsDGI6AUKJDAOaKoQAsATMUA))* 28 + 29 + Note you have to write `:=` (assignment) rather than `=`. This is because Lean uses `=` for comparisons. This might remind you of Go or Pascal (if you're old enough). 30 + 31 + Although you haven't written any types explicitly, each definition is typed. To find out their types, you can hover over `name` and `age` in the online playground or [inside VS Code](https://lean-lang.org/install/). You should see `name : String` and `42 : Nat`, respectively. (Going forward, when I say "hover", I'll assume either of those environments.) 32 + 33 + Here, `String` is obviously a string, while `Nat` stands for a "natural number". In Lean, natural numbers include `0`, `1`, `2`, and so on, going up arbitrarily large. 34 + 35 + You could specify types explicitly by writing ` : SomeType` before the `:=`: 36 + 37 + ```lean 38 + def name : String := "Alice" 39 + def age : Nat := 42 40 + ``` 41 + 42 + If you don't, Lean will try to infer the type from what you wrote on the right side. 43 + 44 + --- 45 + 46 + ### Specifying Types 47 + 48 + You've just seen that Lean infers `"Alice"` to be a `String` and `42` to be a `Nat`. A `Nat` could be `0`, `1`, `2`, and so on. What if you try a negative number like `-140`? 49 + 50 + ```lean 51 + def temperature := -140 52 + ``` 53 + 54 + If you hover on `temperature`, you'll see that it's an `Int`. An `Int`, which stands *for integer*, is a built-in type allowing any whole number, negative or positive. 55 + 56 + You could ask for a specific type like `Int` explicitly in the definition: 57 + 58 + ```lean 59 + def roomTemperature : Int := 25 60 + ``` 61 + 62 + If you just wrote `def roomTemperature := 25`, Lean would give you a `Nat`, but adding `: Int` explicitly nudged type inference to try to produce an `Int`. 63 + 64 + Another way to ask for a specific type is to wrap the expression itself: 65 + 66 + ```lean 67 + def roomTemperature := (25 : Int) 68 + ``` 69 + 70 + In both cases, you're saying you really want to get an `Int`. If Lean couldn't figure out how to produce an `Int` from the expression, it would give you a type error. 71 + 72 + --- 73 + 74 + Let's calculate Alice's birth year based on her age: 75 + 76 + ```lean {3} 77 + def name := "Alice" 78 + def age := 42 79 + def birthYear := 2025 - age 80 + ``` 81 + 82 + We need to get `birthYear` somewhere on the screen. If you're following along in the [online playground](https://live.lean-lang.org/), you might realize that your code isn't actually running. 83 + 84 + This is because there are two ways to use Lean. 85 + 86 + One way to use Lean is to run your code. Another way to use Lean is to prove some facts *about* your code. You can also do both--write code and proofs about it. We're going to start by learning to run some code, and then we'll look at writing proofs. 87 + 88 + --- 89 + 90 + ### Running Code 91 + 92 + If you just want to see the result of some expression, add an `#eval` command: 93 + 94 + ```lean {5} 95 + def name := "Alice" 96 + def age := 42 97 + def birthYear := 2025 - age 98 + 99 + #eval birthYear 100 + ``` 101 + 102 + Hovering over this `#eval` in your editor will now say `1983`. Another place where it'll show up is the *InfoView* on the right side of the online playground: 103 + 104 + ![1983 shows up in InfoView](./1.png) 105 + 106 + Note `1983` in the bottom right corner. If you [set up VS Code with the Lean extension](https://lean-lang.org/install/) locally, you can get the same InfoView displayed like this: 107 + 108 + ![Screenshot of InfoView in VS Code](./2.png) 109 + 110 + Lean InfoView is incredibly useful and I suggest to keep it open at all times. 111 + 112 + The `#eval` command is handy for doing inline calculations and to verify your code is working as intended. But maybe you actually want to run a program that outputs something. You can turn a Lean file into a real program by defining `main`: 113 + 114 + ```lean {4} 115 + def name := "Alice" 116 + def age := 42 117 + def birthYear := 2025 - age 118 + def main := IO.println birthYear 119 + ``` 120 + 121 + I intentionally did not say "a `main` function" because `main` is not a function. (You can hover over it to learn the type of the `main` but we won't focus on that today.) 122 + 123 + Let's run our program: 124 + 125 + ```sh 126 + lean --run Scratch.lean 127 + ``` 128 + 129 + Now `1983` appears in the terminal. Alternatively, you could also do this: 130 + 131 + ```sh 132 + lean Scratch.lean -c Scratch.c 133 + ``` 134 + 135 + The C code generated by Lean compiler will include an instruction to print `1983`: 136 + 137 + ```c 138 + LEAN_EXPORT lean_object* _lean_main(lean_object* x_1) { 139 + lean_object* x_2; lean_object* x_3; 140 + x_2 = lean_unsigned_to_nat(1983u); 141 + x_3 = l_IO_println___at___main_spec__0(x_2, x_1); 142 + return x_3; 143 + } 144 + ``` 145 + 146 + Now you see that Lean can be used to write programs. 147 + 148 + --- 149 + 150 + ### Writing Proofs 151 + 152 + Now let's *prove* that `age + birthYear` together add up to `2025`. 153 + 154 + Define a little `theorem` alongside with your program: 155 + 156 + ```lean {5-6} 157 + def name := "Alice" 158 + def age := 42 159 + def birthYear := 2025 - age 160 + 161 + theorem my_theorem : age + birthYear = 2025 := by 162 + sorry 163 + ``` 164 + 165 + A `theorem` is [like](https://proofassistants.stackexchange.com/a/1576) a `def`, but aimed specifically at declaring proofs. 166 + 167 + The declared type of a theorem is a statement that it's supposed to prove. Your job is now to construct a proof of that type, and the Lean "tactic mode" (activated with `by`) provides you with an interactive and concise way to construct such proofs. 168 + 169 + Initially, the InfoView tells you that your goal is `age + birthYear = 2025`: 170 + 171 + ![Goal: age + birthYear = 2025](./7.png) 172 + 173 + However, that's not something you can be sure in directly. What's `age`? Try to `unfold age` to replace `age` in the goal with whatever its definition says: 174 + 175 + ![Goal: 42 + birthYear = 2025](./8.png) 176 + 177 + Note how this made the goal in your InfoView change to `42 + birthYear = 2025`. Okay, but what's a `birthYear`? Let's `unfold birthYear` as well: 178 + 179 + ![Goal: 42 + (2025 - age) = 2025](./9.png) 180 + 181 + You're getting closer; the goal is now `42 + (2025 - age) = 2025`. Unfolding `birthYear` brought back `age`, what's `age` again? Let's `unfold age`: 182 + 183 + ![Goal: 42 + (2025 - 42) = 2025](./10.png) 184 + 185 + At this point the goal is `42 + (2025 - 42) = 2025`, which is a simple arithmetic expression. The built-in `decide` tactic can solve those with gusto: 186 + 187 + ![No goals](./11.png) 188 + 189 + And you're done! You've now proven that `age + birthYear = 2025` without actually having *run* any code. This is being verified *during typechecking.* 190 + 191 + You can verify that editing `age` to another number will not invalidate your proof. However, if you edit `birthYear` to `2023 - age`, the proof no longer typechecks: 192 + 193 + ![tactic 'decide' proved that 42 + (2023 - 42) is false](./12.png) 194 + 195 + Of course, this was all a bit verbose. Instead of doing `unfold` for each definition manually, you can tell the `simp` simplifier to do them recursively for you: 196 + 197 + ![simp [age, birthView] solves the same theorem](./13.png) 198 + 199 + This also proves the goal. 200 + 201 + This example is contrived but I wanted to show you how it feels to step through the code step by step and transform it "outside in". It almost makes you feel like *you're* the computer, logically transforming the code towards the goal. It won't always be so tedious, especially if you have some useful theorems prepared. 202 + 203 + We'll come back to proving things, but for now let's learn some more Lean basics. 204 + 205 + --- 206 + 207 + ### Opening Namespaces 208 + 209 + Have another look at this `main` definition: 210 + 211 + ```lean {4} 212 + def name := "Alice" 213 + def age := 42 214 + def birthYear := 2025 - age 215 + def main := IO.println birthYear 216 + ``` 217 + 218 + Here, `IO.println birthYear` is a function call. `IO` is a namespace, and `println` is a function defined in that namespace. You pass `birthYear` to it. 219 + 220 + You could avoid the need to write `IO.` before it by *opening* the `IO` namespace: 221 + 222 + ```lean {1,6} 223 + open IO 224 + 225 + def name := "Alice" 226 + def age := 42 227 + def birthYear := 2025 - age 228 + def main := println birthYear 229 + ``` 230 + 231 + This doesn't have to be done at the top of the file: 232 + 233 + ```lean {5} 234 + def name := "Alice" 235 + def age := 42 236 + def birthYear := 2025 - age 237 + 238 + open IO 239 + def main := println birthYear 240 + ``` 241 + 242 + It can be a bit confusing that now you have to write `IO.println` anywhere above that `open IO` but you can write `println` directly anywhere below it. As an alternative, you can scope opening `IO` to a specific definition by adding `in`: 243 + 244 + ```lean {5} 245 + def name := "Alice" 246 + def age := 42 247 + def birthYear := 2025 - age 248 + 249 + open IO in 250 + def main := println birthYear 251 + ``` 252 + 253 + This would make the shorter syntax available inside `main` but not outside it. 254 + 255 + We're not using `IO` much so I'll keep referring to `println` as `IO.println` below. 256 + 257 + --- 258 + 259 + ### Passing Arguments 260 + 261 + Now notice how you're passing `birthYear` to the `println` function: 262 + 263 + ```lean {4} 264 + def name := "Alice" 265 + def age := 42 266 + def birthYear := 2025 - age 267 + def main := IO.println birthYear 268 + ``` 269 + 270 + Unlike languages like JavaScript, Lean doesn't use parentheses or commas for function calls. Instead of `f(a, b, c)`, you would write `f a b c` in Lean. 271 + 272 + I need to emphasize this. No commas and no parens are used for function calls! 273 + 274 + However, you would sometimes use parentheses *around* individual arguments. Suppose you replace `birthYear` with `2025 - age` directly in the function call: 275 + 276 + ```lean {3} 277 + def name := "Alice" 278 + def age := 42 279 + def main := IO.println 2025 - age 280 + ``` 281 + 282 + This will lead to an error: 283 + 284 + ![Failed to synthesize HSub (IO Unit) Nat ?m.342](./3.png) 285 + 286 + Lean thinks you're trying to do `(IO.println 2025) - age`, so it looks for a way to subtract `age` (which is a `Nat`) from whatever `IO.println 2025` returns (which happens to be something called `IO Unit`). Lean can't find a subtraction operation (`HSub`) between an `IO Unit` and a `Nat`, so it gives up in frustration. 287 + 288 + To fix this, add parentheses around `2025 - age`: 289 + 290 + ```lean {3} 291 + def name := "Alice" 292 + def age := 42 293 + def main := IO.println (2025 - age) 294 + ``` 295 + 296 + Now instead of `IO.println` "eating" the `2025` argument, it knows that the first argument is the entire `(2025 - age)` expression. You might find it helpful to use `(` and `)` liberally and try removing them to get a feel for when they're necessary. 297 + 298 + Note `(` and `)` here have nothing to do with the `IO.println` function call. Their only purpose is to "group" `2025 - age` together, like you group things in math. 299 + 300 + **In other words, instead of writing `something(f(x, y), a, g(z))` as you might in JavaSript, you would write `something (f x y) a (g z)` in Lean.** 301 + 302 + Read this closely several times and make sure it burns deep in your subconscious. 303 + 304 + --- 305 + 306 + ### Nesting Expressions 307 + 308 + You can't nest Lean definitions. However, if some of your definitions get complex, you can simplify them by declaring some `let` bindings inside of the `def`. 309 + 310 + For example, you could pull `(2025 - age)` from the last example into a `let`: 311 + 312 + ```lean {5-6} 313 + def name := "Alice" 314 + def age := 42 315 + 316 + def main := 317 + let birthYear := 2025 - age 318 + IO.println birthYear 319 + ``` 320 + 321 + Again, note the use of `:=` for assignment. It's `:=`, not `=`! 322 + 323 + You might think that being multiline makes `main` a function, but it doesn't. Adding a `let` binding is just a way to make definitions easier to read. You could use `let` inside of any definition, including the `name` and `age` definitions: 324 + 325 + ```lean {2-3,6-8} 326 + def name := 327 + let namesInPassport := ["Alice", "Babbage", "McDuck"] 328 + namesInPassport[0] 329 + 330 + def age := 331 + let twenty := 20 332 + let one := 1 333 + twenty + twenty + one + one 334 + 335 + def main := 336 + let birthYear := 2025 - age 337 + IO.println birthYear 338 + ``` 339 + 340 + There is no need for a `return` statement here. The last line of a definition becomes its value. This is why the definitions above are equivalent to: 341 + 342 + ```lean 343 + def name := ["Alice", "Babbage", "McDuck"][0] 344 + def age := 20 + 20 + 1 + 1 345 + def main := IO.println (2025 - age) 346 + ``` 347 + 348 + In the end, the right side of any definition unfolds into a single expression, but `let` lets you break down that expression into more readable (and reusable) pieces. 349 + 350 + --- 351 + 352 + ### Declaring Functions 353 + 354 + Currently `birthYear` hardcodes `2025` into the calculation: 355 + 356 + ```lean {3} 357 + def name := "Alice" 358 + def age := 42 359 + def birthYear := 2025 - age 360 + def main := IO.println birthYear 361 + ``` 362 + 363 + You can turn `birthYear` into a *function* definition by declaring a `currentYear` argument immediately after writing `def birthYear` followed by a space: 364 + 365 + ```lean {3} 366 + def name := "Alice" 367 + def age := 42 368 + def birthYear currentYear := currentYear - age 369 + ``` 370 + 371 + This makes `birthYear` a function. You can call it by writing `birthYear 2025`: 372 + 373 + ```lean {4} 374 + def name := "Alice" 375 + def age := 42 376 + def birthYear currentYear := currentYear - age 377 + def main := IO.println (birthYear 2025) 378 + ``` 379 + 380 + Again, the parens around `birthYear 2025` ensure that `IO.println` doesn't try to "eat" the `birthYear` function itself. We want it to "eat" `birthYear 2025`. 381 + 382 + If you hover over `birthYear` now, you'll see that its type is no longer a `Nat`: 383 + 384 + ![](./5.png) 385 + 386 + This is how Lean pretty-prints function types. You see the function name `birthYear`, followed by its arguments (we only have one), `:` and the return type. 387 + 388 + You could write it that way explicitly, too, to clarify your intended types: 389 + 390 + ```lean 391 + def birthYear (currentYear : Nat) : Nat := currentYear - age 392 + ``` 393 + 394 + Note, again, that `(` `)` parentheses in Lean have nothing to do with function calls. You only use the parentheses to treat `(currentYear : Nat)` as a single thing. 395 + 396 + --- 397 + 398 + ### Many Ways to Declare a Function 399 + 400 + You've seen two ways to define the same function, with implicit and explicit types: 401 + 402 + ```lean 403 + /-- Types are implicit here, Lean infers them -/ 404 + def birthYear currentYear := currentYear - age 405 + 406 + /-- Types are explicit here -/ 407 + def birthYear (currentYear : Nat) : Nat := currentYear - age 408 + ``` 409 + 410 + There are even more valid ways to write the same thing with different verbosity. Here is a (non-exhaustive) list of ways to define the same `birthYear` function: 411 + 412 + ```lean 413 + /-- Concise definition -/ 414 + def birthYear currentYear := currentYear - age 415 + def birthYear (currentYear: Nat) := currentYear - age 416 + def birthYear (currentYear: Nat) : Nat := currentYear - age 417 + 418 + /-- Definition set to an anonymous function -/ 419 + def birthYear := fun currentYear => currentYear - age 420 + def birthYear := fun (currentYear: Nat) => currentYear - age 421 + 422 + /-- Definition (with explicit type) set to an anonymous function -/ 423 + def birthYear : Nat → Nat := fun currentYear => currentYear - age 424 + ``` 425 + 426 + This might remind you of `function f() {}` vs `const f = () => ...` in JS. I find the concise syntax most pleasant to read and recommend using it unless you specifically need an anonymous function (e.g. to pass it to another function). 427 + 428 + Here, `Nat → Nat` is the actual type of `birthYear`, no matter which syntax is used. It takes a `Nat` and it returns a `Nat`, so it's `Nat → Nat`. The fancy `→` arrow is typed by writing `\to` followed by a space in the [playground](https://live.lean-lang.org/) or with [Lean VSCode](https://lean-lang.org/install/). 429 + 430 + Specifying argument types but inferring the return is often a nice middle ground: 431 + 432 + ```lean 433 + def birthYear (currentYear: Nat) := currentYear - age 434 + ``` 435 + 436 + Just like with `function` declarations vs arrow functions in JavaScript, the level of verbosity and typing that you want to do in each case is mostly up to you. 437 + 438 + (Sidenote: You might also see syntax like `fun x ↦ x * 2` rather than `fun x => x * 2`. Here, `↦` is typed as `\maps`, and mathematicians prefer it aesthetically to `=>`. Lean doesn't distinguish them so you'll see `=>` in codebases like Lean itself while `↦` shows up in "mathy" codebases like Mathlib. They both work with `fun`.) 439 + 440 + --- 441 + 442 + ### Adding Arguments 443 + 444 + Now that you know a dozen ways to write functions, let's get back to this one: 445 + 446 + ```lean {3} 447 + def name := "Alice" 448 + def age := 42 449 + def birthYear currentYear := currentYear - age 450 + def main := IO.println (birthYear 2025) 451 + ``` 452 + 453 + Suppose you want to make `age` an argument to the `birthYear` function. To add an argument, just write it next in the `def birthYear` definition argument list: 454 + 455 + ```lean {2-3} 456 + def name := "Alice" 457 + def birthYear currentYear age := currentYear - age 458 + def main := IO.println (birthYear 2025 42) 459 + ``` 460 + 461 + Actually, this doesn't work, and the error is sucky. 462 + 463 + ![typeclass instance is stuck](./14.png) 464 + 465 + The problem is the issue I described earlier--at this point, Lean has no idea what the types of `currentYear` and `age` might be. Previously, it relied on `age` being an earlier declaration (and inferred it to be a `Nat`) but now Lean is truly stumped. 466 + 467 + The strange error message ("typeclass instance is stuck") alludes to the fact that it's trying to find an implementation of subtraction between two types (searching for that implementation is done via "typeclass search") but it doesn't even know what those types are (that's why you see weird `?m.24` and `?m.12` placeholders). 468 + 469 + The fix to this is to bite the bullet and to actually specify the types: 470 + 471 + ```lean {3} 472 + def name := "Alice" 473 + 474 + def birthYear (currentYear : Nat) (age : Nat) := 475 + currentYear - age 476 + 477 + def main := IO.println (birthYear 2025 42) 478 + ``` 479 + 480 + Now there is no ambiguity. Note that the parentheses here don't mean anything special--it's only a way to separate parameters from each other. Just like when calling functions, we're not using commas so we need parens for grouping. 481 + 482 + When you have multiple parameters of the same type in a row, like `currentYear` and `age` above, you may put them together together under one type declaration: 483 + 484 + ```lean {3} 485 + def name := "Alice" 486 + 487 + def birthYear (currentYear age : Nat) := 488 + currentYear - age 489 + 490 + def main := IO.println (birthYear 2025 42) 491 + ``` 492 + 493 + This doesn't change semantics but is shorter to write. This is particularly useful in mathematics where you might have 3 or 4 parameters that are all `Nat` or such. 494 + 495 + Now that you're specifying all parameter types explicitly, it makes sense to think about them harder and to give them slightly different types. While `age` should remain a `Nat`, `currentYear` makes more sense as an `Int` so that the calculation still works for people born in the BC era or who are over two thousand years old. 496 + 497 + ```lean {3} 498 + def name := "Alice" 499 + 500 + def birthYear (currentYear : Int) (age : Nat) := 501 + currentYear - age 502 + 503 + def main := IO.println (birthYear 2025 42) 504 + ``` 505 + 506 + The non-pretty-formatted type of `birthYear` is `Int → Nat → Int` because it takes an `Int` and also a `Nat`, and returns an `Int`. As in some other functional languages, you can partially apply it--for example, `birthYear 2025` will give you a `Nat → Int` function that you can later call with the remaining `age` argument. 507 + 508 + By now, you might have guessed that there are other increasingly deranged equivalent ways to define the same function that takes an `Int` and a `Nat`: 509 + 510 + ```lean 511 + def birthYear (currentYear : Int) (age : Nat) := currentYear - age 512 + def birthYear (currentYear : Int) := fun (age : Nat) => currentYear - age 513 + def birthYear := fun (currentYear : Int) (age : Nat) => currentYear - age 514 + def birthYear := fun (currentYear : Int) => fun (age : Nat) => currentYear - age 515 + def birthYear: Int → Nat → Int := fun currentYear age => currentYear - age 516 + def birthYear: Int → Nat → Int := fun currentYear => fun age => currentYear - age 517 + ``` 518 + 519 + **Read them closely keeping in mind that they are all exactly equivalent in Lean.** It's confusing to mix syntax within a single definition so we'll stick with this: 520 + 521 + ```lean 522 + def birthYear (currentYear : Int) (age : Nat) := 523 + currentYear - age 524 + ``` 525 + 526 + It's time to revisit proofs. 527 + 528 + --- 529 + 530 + ### Proving For All 531 + 532 + A few sections earlier, you've proven `birthYear + age = 2025` for this code: 533 + 534 + ```lean 535 + def name := "Alice" 536 + def age := 42 537 + def birthYear := 2025 - age 538 + 539 + theorem my_theorem : age + birthYear = 2025 := by 540 + simp [age, birthYear] 541 + ``` 542 + 543 + However, now `birthYear` is a function which takes two arguments: 544 + 545 + ```lean {3} 546 + def name := "Alice" 547 + 548 + def birthYear (currentYear : Int) (age : Nat) := 549 + currentYear - age 550 + ``` 551 + 552 + You could copypaste this theorem for concrete values of `age` and `currentYear`: 553 + 554 + ```lean {6-13} 555 + def name := "Alice" 556 + 557 + def birthYear (currentYear : Int) (age : Nat) := 558 + currentYear - age 559 + 560 + theorem my_theorem : 42 + birthYear 2025 42 = 2025 := by 561 + simp [birthYear] 562 + 563 + theorem my_theorem' : 25 + birthYear 2025 25 = 2025 := by 564 + simp [birthYear] 565 + 566 + theorem my_theorem'' : 77 + birthYear 1980 77 = 1980 := by 567 + simp [birthYear] 568 + ``` 569 + 570 + These proofs typecheck, but there's a feeling that it's not any better than writing tests. What we're hoping to capture is a *universal pattern*, not a bunch of cases. 571 + 572 + The fact is, no matter what `cy` (I'll shorten "current year" to that) and `a` (short for "age") are, `a + birthYear cy a` should be equal to `cy`. Let's capture that: 573 + 574 + ```lean {6-7} 575 + def name := "Alice" 576 + 577 + def birthYear (currentYear : Int) (age : Nat) := 578 + currentYear - age 579 + 580 + theorem my_theorem : a + birthYear cy a = cy := by 581 + sorry 582 + ``` 583 + 584 + Although you haven't declared `a` and `cy`, this is actually valid syntax! The VS Code Lean extension will implicitly insert `{a cy}` arguments after `my_theorem`: 585 + 586 + ![Automatically inserted implicit arguments: a : Nat, cy : Int](./15.png) 587 + 588 + These "implicit" arguments surrounded by `{` `}` curly braces can sometimes be very useful to avoid boilerplate, but in this case they're more confusing than helpful. Let's declare `cy` and `a` explicitly as normal arguments to `my_theorem`: 589 + 590 + ```lean {6} 591 + def name := "Alice" 592 + 593 + def birthYear (currentYear : Int) (age : Nat) := 594 + currentYear - age 595 + 596 + theorem my_theorem (cy : Int) (a : Nat) : a + birthYear cy a = cy := by 597 + sorry 598 + ``` 599 + 600 + Note how declaring `my_theorem` with `cy` and `a` is similar to declaring function arguments--except that you can't actually "run" this theorem. You're just stating that, for any `cy` and `a`, you can produce a proof of `a + birthYear cy a = cy`. 601 + 602 + Let's see if you actually can! 603 + 604 + You start out with the goal of `↑a + birthYear cy a = cy`: 605 + 606 + ![Goal: ↑a + birthYear cy a = cy](./16.png) 607 + 608 + The `⊢` symbol before it tells you that this is the goal, i.e. what you want to prove. Things above it, like `cy : Int` and `a : Nat`, are the things you *already have.* 609 + 610 + You might be wondering: what *are* those `cy : Int` and `a : Nat`. What *are* their values? But the question doesn't make sense. You're writing a proof, so in a sense, you are working with *all possible values at the same time.* You only have their types. 611 + 612 + Have another look at the goal: `↑a + birthYear cy a = cy`. 613 + 614 + What's that up arrow? 615 + 616 + Hover over it in the InfoView: 617 + 618 + ![@Nat.cast Int instNatCastInt a : Int](./17.png) 619 + 620 + The signature is a bit confusing (what's that `@`? what's that `instNatCastInt`?) but you can see that it takes your `a` (which is a `Nat`) and returns an `Int`. So this is how Lean displays the fact that your `a` is being converted to an `Int` so that it can be added with the result of `birthYear cy a` call (which is already an `Int`). 621 + 622 + Okay, cool, the goal is to prove `↑a + birthYear cy a = cy`. How do we do that? 623 + 624 + Well, first, let's `unfold birthYear` to replace it with the implementation: 625 + 626 + ![Goal: ↑a + (cy - ↑a) = cy](./18.png) 627 + 628 + Now the goal becomes `↑a + (cy - ↑a) = cy`. 629 + 630 + You can't use the `decide` tactic from earlier because it only deals with known concrete numbers--it's like a calculator. However, Lean also includes an `omega` tactic that can check equations that include unknown numbers like `a` and `cy`. 631 + 632 + With `omega`, you can complete this proof! 633 + 634 + ![No goals](./19.png) 635 + 636 + Let's have another look at the full code: 637 + 638 + ```lean 639 + def name := "Alice" 640 + 641 + def birthYear (currentYear : Int) (age : Nat) := 642 + currentYear - age 643 + 644 + theorem my_theorem (cy: Int) (a : Nat) : a + birthYear cy a = cy := by 645 + unfold birthYear 646 + omega 647 + ``` 648 + 649 + You have a function called `birthYear` and a theorem called `my_theorem` about its behavior. By convention, you might want to call it `birthYear_spec`, or something more specific like `age_add_birthYear_eq_current_year`. 650 + 651 + In a way it's like a test, but it's a test for *all possible inputs of that function* that runs during typechecking. I think this is incredibly cool. You can verify that incorrectly changing the formula of `birthYear` will cause the proof to no longer typecheck: 652 + 653 + ![omega could not prove the goal](./20.png) 654 + 655 + --- 656 + 657 + ### Universal Quantifier 658 + 659 + You've previously declared `cy` and `a` as arguments for `my_theorem`: 660 + 661 + ```lean 662 + theorem my_theorem (cy: Int) (a : Nat) : a + birthYear cy a = cy := by 663 + unfold birthYear 664 + omega 665 + ``` 666 + 667 + In this case, it's fine to omit their types because they can be exactly inferred by Lean from the `birthYear` call itself, which you already explicitly annotated: 668 + 669 + ```lean {1} 670 + theorem my_theorem cy a : a + birthYear cy a = cy := by 671 + unfold birthYear 672 + omega 673 + ``` 674 + 675 + This theorem says "for all `cy` and `a`, `a + birthYear cy a` is equal to `cy`”. Mathematicians have their own way of writing statements like this, using the *universal quantifier* ∀ (which stands for "for all"): *∀ cy a, a + birthYear(cy, a) = cy*. 676 + 677 + You can restate the above in a mathematician's style with the `∀` quantifier: 678 + 679 + ```lean {1} 680 + theorem my_theorem : ∀ cy a, a + birthYear cy a = cy := by 681 + sorry 682 + ``` 683 + 684 + To type `∀`, write `\all` and press space. 685 + 686 + Now instead of `cy a :`, we have `: ∀ cy a`. The universal quantifier lets the arguments get introduced "later"--pretty much like currying in programming. However, the difference is mostly stylistic. The theorem signature is the same. 687 + 688 + When you use `∀`, you're starting out with a universal statement ("for all `cy` and `a`") so `cy` and `a` aren't in your tactic state yet. Use `intro cy a` to bring them in: 689 + 690 + ![intro cy a brings cy : Int and a : Nat into tactic state](./24.png) 691 + 692 + Again, you don't have their *concrete* values because a proof is supposed to work for all possible values. You don't know anything about them except their types. 693 + 694 + From that point, your goal is `↑a + birthYear cy a = cy`, which you can solve: 695 + 696 + ```lean {3-4} 697 + theorem my_theorem : ∀ cy a, a + birthYear cy a = cy := by 698 + intro cy a 699 + unfold birthYear 700 + omega 701 + ``` 702 + 703 + Notice how `∀ cy a` has essentially moved the declarations of `cy` and `a` "inside" the proof itself. Under the hood, the `intro` tactic generates a nested function with `cy` and `a` arguments, so in the end this `theorem` has the same shape as before adding `∀`. It may feel disorienting that Lean lets you write the same thing in many styles, but this lets the Lean core stay small and evolve faster, while tactics and conventions may evolve independently and vary between projects. 704 + 705 + Using `∀` may not seem particularly appealing in this example, but it is great for nested propositions where anonymous functions would add too much clutter. It can also be convenient to extract a theorem's claim into its own definition: 706 + 707 + ```lean {1,3,4} 708 + def TheLawOfAging : Prop := ∀ cy a, a + birthYear cy a = cy 709 + 710 + theorem my_theorem : TheLawOfAging := by 711 + unfold TheLawOfAging 712 + intro cy a 713 + unfold birthYear 714 + omega 715 + ``` 716 + 717 + To learn what a `Prop` is, check out [my previous article](/beyond-booleans/). 718 + 719 + --- 720 + 721 + ### Implicit Arguments 722 + 723 + Now let us come back to this example, and specifically to this line: 724 + 725 + ```lean {8} 726 + def name := "Alice" 727 + 728 + def birthYear (currentYear : Int) (age : Nat) := 729 + currentYear - age 730 + 731 + def main := 732 + let year := birthYear 2025 42 733 + IO.println year 734 + ``` 735 + 736 + Hover over `IO.println`, and you might freak out: 737 + 738 + ![IO.println.\{u_1\} \{a : Type u_1\} [ToString α] (s: α) : IO Unit](./25.png) 739 + 740 + What the hell is this signature?! Learning to read these signatures will immensely improve your ability to understand Lean APIs and debug errors so I'm going to break it down even though we haven't discussed most of the relevant topics. 741 + 742 + The key insight is that the parts in `{` `}` and `[` `]` are not something you pass to the function directly. That's why you're able to just write `IO.println year`. However, these *are* actual arguments--they're just filled in by Lean itself. 743 + 744 + Command+Click into `println` to see that it declares `{α}` and `[ToString α]`: 745 + 746 + ```lean 747 + def println {α} [ToString α] (s : α) : IO Unit := 748 + print ((toString s).push '\n') 749 + ``` 750 + 751 + So if you Lean fills them in for you, where *are* they coming from? 752 + 753 + The parts in `{` `}` are called *ordinary implicit parameters*, and they are filled in by type inference based on the parameters that have already been passed in. If Lean can't unambiguously fill them in, it will complain and would not typecheck. Here, the `α` argument is implicit, but it's determined by what *you* passed as an explicit `(s : α)` argument. You're printing an `Int`, you passed `(s: Int)`, so `α` is `Int`. This is similar to generics, but in Lean generics are just arguments (often implicit). 754 + 755 + 756 + The parts in `[` `]`, like `[ToString α]`, are called *instance implicit parameters* and are filled in automatically depending on the code you've imported so far. Lean has a mechanism that lets libraries declare implementations of certain intefaces--for example, "how to subtract `Int` and `Int`" or "how to convert `Nat` to `Int`" or "how to turn an `Int` to a `String`”. The `println` function wants a `ToString` implementation that knows how to turn whatever *you* passed (an `Int`) to a string. Lean has a `ToString Int` implementation in core, so that's what `println` gets. 757 + 758 + Finally, the very first `.{}` thing is a "universe" in which this function's type lives. This is rarely something you need to deal with, and is also filled in automatically. 759 + 760 + If you're curious what Lean is filling in (or need to debug some gnarly case that wouldn't typecheck), you can prefix your function call with `@` to force all arguments to be passed explicitly. Start by passing `_` placeholders: 761 + 762 + ```lean {8} 763 + def name := "Alice" 764 + 765 + def birthYear (currentYear : Int) (age : Nat) := 766 + currentYear - age 767 + 768 + def main := 769 + let year := birthYear 2025 42 770 + @IO.println _ _ year 771 + ``` 772 + 773 + Now you can hover over each `_` placeholder to see what Lean has inferred for it. 774 + 775 + For example, the `{α}` implicit parameter was inferred to be `Int`: 776 + 777 + ![Int](./26.png) 778 + 779 + And the `[ToString]` instance implicit parameter received `instToStringInt`: 780 + 781 + ![instToStringInt](./27.png) 782 + 783 + If you want to further debug what's going on, you can fill them in in the code: 784 + 785 + ```lean {8} 786 + def name := "Alice" 787 + 788 + def birthYear (currentYear : Int) (age : Nat) := 789 + currentYear - age 790 + 791 + def main := 792 + let year := birthYear 2025 42 793 + @IO.println Int instToStringInt year 794 + ``` 795 + 796 + That's what you would have to type if Lean couldn't fill them in automatically. 797 + 798 + Now it's easy to Command+Click them and see if they match what you expected. For example, Command+Clicking `instToStringInt` from here will teleport you to the place in the Lean source that actually implements `ToString Int`: 799 + 800 + ```lean 801 + instance : ToString Int where 802 + toString 803 + | Int.ofNat m => toString m 804 + | Int.negSucc m => "-" ++ toString (succ m) 805 + ``` 806 + 807 + Seems legit! When you're done spelunking, you can remove the `@` and the `_`s: 808 + 809 + ```lean {8} 810 + def name := "Alice" 811 + 812 + def birthYear (currentYear : Int) (age : Nat) := 813 + currentYear - age 814 + 815 + def main := 816 + let year := birthYear 2025 42 817 + IO.println year 818 + ``` 819 + 820 + Implicit and instance arguments save a tremenduous amount of boilerplate when operating on complex structures like mathematical objects and proofs. But even here, they let Lean provide a simple, extensible, and strongly-typed `println` API. 821 + 822 + --- 823 + 824 + ### Command+Click Anything 825 + 826 + There's a *lot* more I would've liked to cover but I'm running out of steam--and this article is already long. The good news is, there's a lot you can learn by peeking under the hood on your own because Lean is extremely Command+Click-able. 827 + 828 + You can Command+Click into data types like [`Nat`](https://github.com/leanprover/lean4/blob/ad1a017949674a947f0d6794cbf7130d642c6530/src/Init/Prelude.lean#L1181-L1202) and [`String`](https://github.com/leanprover/lean4/blob/ad1a017949674a947f0d6794cbf7130d642c6530/src/Init/Prelude.lean#L2738-L2752), and even pieces of syntax like `∀`. Much of Lean is implemented in Lean itself, and most of the time what I found ended up both simpler and more interesting than what I expected. 829 + 830 + ```lean 831 + /-- 832 + The natural numbers, starting at zero. 833 + This type is special-cased by both the kernel and the compiler, and overridden with an efficient 834 + implementation. Both use a fast arbitrary-precision arithmetic library (usually 835 + [GMP](https://gmplib.org/)); at runtime, `Nat` values that are sufficiently small are unboxed. 836 + -/ 837 + inductive Nat where 838 + /-- Zero, the smallest natural number. -/ 839 + | zero : Nat 840 + /-- The successor of a natural number `n`. -/ 841 + | succ (n : Nat) : Nat 842 + ``` 843 + 844 + Who would've thought that numbers are just a recursively generated enum? 845 + 846 + Turns out, a language built with recursively defined types like this can express and prove facts like that the [area of a circle is π * r ^ 2](https://github.com/leanprover-community/mathlib4/blob/aac79004bad67a82c18ab7d39c609529f4159d6f/Archive/Wiedijk100Theorems/AreaOfACircle.lean#L81-L130), or that [it takes 23 people to exceed a 50% chance of same birthday](https://github.com/leanprover-community/mathlib4/blob/aac79004bad67a82c18ab7d39c609529f4159d6f/Archive/Wiedijk100Theorems/BirthdayProblem.lean#L65-L81), or that [you can't cube a cube](https://github.com/leanprover-community/mathlib4/blob/aac79004bad67a82c18ab7d39c609529f4159d6f/Archive/Wiedijk100Theorems/CubingACube.lean#L533-L546). But you can also write normal programs with it that compile to C. [Async/await](https://lean-lang.org/fro/roadmap/1900-1-1-the-lean-fro-year-3-roadmap/) is in the works. 847 + 848 + Maybe you could create something in the middle--programs interleaved with proofs? You can see this pattern in the Lean core itself: here's a [`List.append` (`++`) function](https://github.com/leanprover/lean4/blob/c83237baf78d6930d40e556a5cacdca149c4b198/src/Init/Data/List/Basic.lean#L614-L616), and here are proofs that [`(as ++ bs).length = as.length + bs.length`](https://github.com/leanprover/lean4/blob/c83237baf78d6930d40e556a5cacdca149c4b198/src/Init/Data/List/Basic.lean#L661-L664) and [`(as ++ bs) ++ cs = as ++ (bs ++ cs)`](https://github.com/leanprover/lean4/blob/c83237baf78d6930d40e556a5cacdca149c4b198/src/Init/Data/List/Basic.lean#L666-L669) written alongside it. In other words, using the Lean data structures in code means also lets you reuse known facts about those structures to prove claims about your own code. 849 + 850 + --- 851 + 852 + ### Programming With Proofs 853 + 854 + Lean is built on old ideas and a lot of prior art, but there are both novel and pragmatic twists. Sometimes I can't tell if I'm writing Go or Haskell, Rocq or C#, OCaml or Python. In its giddily unrestrained ambition it reminds me of [Nemerle](https://en.wikipedia.org/wiki/Nemerle), but it also feels solid and grounded. It's being used in both industry and academia, including by [DeepMind](https://deepmind.google/discover/blog/ai-solves-imo-problems-at-silver-medal-level/) and [Amazon](https://aws.amazon.com/blogs/opensource/lean-into-verified-software-development/). It's been a while since I felt excited about a programming language (mind you--a *pure* functional programming language). 855 + 856 + Anyway, here's a little program that counts up and down alongside some proofs. 857 + 858 + ```lean 859 + def append (xs ys : List a) : List a := Id.run do 860 + let mut out := ys 861 + for x in xs.reverse do 862 + out := x :: out 863 + return out 864 + 865 + theorem append_abcd : append ["a", "b"] ["c", "d"] = ["a", "b", "c", "d"] := by 866 + simp [append] 867 + 868 + theorem append_length (xs ys : List a) : (append xs ys).length = xs.length + ys.length := by 869 + simp [append] 870 + 871 + 872 + def count_up_and_down n := 873 + let up := List.range (n) 874 + let down := (List.range (n + 1)).reverse 875 + append up down 876 + 877 + theorem count_up_and_down_length n : (count_up_and_down n).length = n * 2 + 1 := by 878 + simp only [count_up_and_down, append_length, List.length_range, List.length_reverse] 879 + omega 880 + 881 + 882 + def main := do 883 + IO.println "Enter a number: " 884 + let stdin ← IO.getStdin 885 + let input ← stdin.getLine 886 + let n := input.trim.toNat! 887 + let sequence := count_up_and_down n 888 + IO.println sequence 889 + ``` 890 + 891 + I've never written code alongside proofs like this before. Have you?
+123
public/hire-me-in-japan/index.md
··· 1 + --- 2 + title: Hire Me in Japan 3 + date: '2025-11-11' 4 + spoiler: I'm looking for a new job. 5 + nocta: true 6 + --- 7 + 8 + My sabbatical is soon coming to an end, and I am looking for a new job. 9 + 10 + In particular, I am looking for a job at a company that would like to **sponsor a working visa for me in Japan**, where I'd like to relocate within the next year. 11 + 12 + If you can sponsor a software engineering visa in Japan and think I might be a good fit, please email `dan.abramov.japan@gmail.com`. Below I'll recap some of my past work, with more details re: what I'm looking for near the end of this page. 13 + 14 + [*(Skip to the end)*](#looking-forward) 15 + 16 + --- 17 + 18 + ### Past Work 19 + 20 + Hi! My name is Dan Abramov. 21 + 22 + I started programming about 20 years ago and then I couldn't stop--so I've been doing software for over 15 years professionally. I've dabbled in different languages, but the vast majority of my recent work has been in JavaScript and TypeScript. 23 + 24 + Here's a few things I have worked on over the years. 25 + 26 + #### 2025: Consulting 27 + 28 + This year, [I've been doing consulting](/im-doing-a-little-consulting/), so there isn't much I can say publicly. 29 + 30 + Mostly, I've been consulting teams using React on how to approach engineering challenges related to performance and state management in complex apps. 31 + 32 + On the side, I've been [contributing](https://github.com/teorth/analysis/pulls?q=is%3Apr+author%3Agaearon+is%3Aclosed) to Terence Tao's [Analysis textbook in Lean](https://github.com/teorth/analysis). 33 + 34 + #### 2023–2025: The Bluesky app 35 + 36 + From 2023 to 2025, I worked on the official [Bluesky](https://bsky.app/) client app (in React Native). Most of my engineering work focused on [improving the app quality](https://github.com/bluesky-social/social-app/issues/1675), for example: 37 + 38 + - [Implementing animations for the lightbox](https://github.com/bluesky-social/social-app/pull/6048) 39 + - [Synchronizing feed swipe animations with gestures](https://github.com/bluesky-social/social-app/pull/6868) 40 + - [Introducing custom lint rules to prevent runtime crashes](https://github.com/bluesky-social/social-app/pull/3398) 41 + - [Shaving off seconds from Android start time](https://github.com/bluesky-social/social-app/pull/1756) 42 + 43 + Some of this work required multi-week deep dives and sprawling refactors. 44 + 45 + - [Rewriting the lightbox to work well on Android](https://github.com/bluesky-social/social-app/pull/1624) 46 + - [Reworking the cross-platform layout to work well on the web](https://github.com/bluesky-social/social-app/pull/2126) 47 + - [Revamping how threads show up in the feed](https://github.com/bluesky-social/social-app/pull/4871) 48 + - [Revamping the post composer into a thread composer](https://github.com/bluesky-social/social-app/pull/5962) 49 + 50 + The Bluesky app is open source; here's a link to my [other merged pull requests](https://github.com/bluesky-social/social-app/pulls?q=is%3Apr+author%3Agaearon+is%3Amerged+). 51 + 52 + We've used a lot of open source software at Bluesky, and sometimes it could be difficult to trace down where a bug is coming from. Occasionally, I've had to do deep dive investigations into the underlying projects to file bugs or fixes there. 53 + 54 + - [Reducing the Reanimated serialization traffic](https://github.com/bluesky-social/social-app/pull/6219) 55 + - [Fixing a 10 year old ScrollView bug in React Native](https://github.com/facebook/react-native/pull/47591) 56 + - [Documenting the difficulties in getting gestures to work with ScrollView](https://github.com/software-mansion/react-native-gesture-handler/issues/2616#issuecomment-2525238174) 57 + 58 + In addition to my individual engineering work, I've done some engineering management and mentoring on the application team. This included: 59 + 60 + - Helping the team get a natural mental model for working with React. 61 + - Mentoring the application team on how to root cause particularly gnarly issues. 62 + - Pushing for a closer collaboration with the backend team so that the seams from the client/server organizational split don't "show up" as poor UX in the product. 63 + 64 + I've also helped the team explain the AT protocol to a broader community--first in a [talk](https://www.youtube.com/watch?v=F1sJW6nTP6E) last year, which then crystallized into my recent article called [Open Social](/open-social/). 65 + 66 + #### 2015–2023: React at Meta/Facebook 67 + 68 + Before Bluesky, I used to work on the React team at Meta (formerly Facebook). Some of the more visible projects I was involved with include: 69 + 70 + - The [React Documentation](https://react.dev/), which I co-wrote with [Rachel Lee Nabors](https://nearestnabors.com/) and [others](https://react.dev/community/docs-contributors). My work on it included designing the Learn section curriculum, iterating on the Reference page structure, a decent chunk of technical work on the website itself, and much of the actual writing, including the design of examples and challenges. 71 + - The public messaging and rollout of React Hooks, including the original documentation for React Hooks and the conference talk introducing them. 72 + - Implementing Fast Refresh (hot reloading / live editing) as a first-class React feature. It feels like a distant past, but we used to press Cmd+R to see our edits. 73 + - Co-creating [Create React App](https://legacy.reactjs.org/blog/2016/07/22/create-apps-with-no-configuration.html), which ended the "JavaScript fatigue" (allegedly). 74 + 75 + I've also done [some other technical work on React over the years](https://github.com/facebook/react/pulls?q=is%3Apr+author%3Agaearon+is%3Amerged+), which mostly consisted of fixing bugs and [occasionally](https://legacy.reactjs.org/blog/2017/12/15/improving-the-repository-infrastructure.html) revamping the build infrastructure. 76 + 77 + Outside of my job, I co-created [Just JavaScript](https://justjavascript.com/), which is a half-book half-course introduction to thinking in JavaScript, aiming to be both whimsical *and* rigorous. 78 + 79 + #### Before 2015 80 + 81 + I haven't done many publicly notable things before getting hired at Facebook, other than accidentally co-creating [Redux](https://redux.js.org/) as a demo for my conference talk. 82 + 83 + I've previously worked at a small product company that didn't succeed doing some JavaScript and C#, and before that at an outsourcing firm where I mostly did C#. 84 + 85 + I also created [React Hot Loader](https://gaearon.github.io/react-hot-loader/) (a very early precursor to Fast Refresh, now built into React), [React DnD](https://react-dnd.github.io/react-dnd/), and [normalizr](https://github.com/paularmstrong/normalizr) (which was used by Twitter for some time). 86 + 87 + --- 88 + 89 + ### Looking Forward 90 + 91 + As you can tell from the above, most of my engineering expertise lies in the field of UI engineering, web development in particular, and of course in using React. I'd love an opportunity to share my expertise and to help you create better apps. 92 + 93 + I enjoy sharing what I learned, which is why I blog and do [talks](https://danabra.mov/). Although I prefer when I can work on open source software, this will not be a requirement for me. 94 + 95 + Currently I feel most comfortable with JavaScript/TypeScript. I've [recently](/the-math-is-haunted/) [picked](/beyond-booleans/) [up](/a-lean-syntax-primer/) some Lean, and working with it professionally seems alluring (if unlikely). I've been using LLMs quite a bit recently too and am open to working with them more. In general, I'm open to learning new things on the job (for example, I've learned React Native while at Bluesky!) but a new stack may require some ramp-up time. 96 + 97 + I care about the *quality* of what I'm working on, whether it's an app, a developer tool, or a course. I hope to find a job where caring about quality is appreciated. I know fixing things "the right way" isn't always possible, but I feel most useful when I'm able to dig into the root causes, and work with others on solving them. 98 + 99 + To sum up, I'm looking for a software engineering job... in Japan. 100 + 101 + --- 102 + 103 + ### Why Japan 104 + 105 + Why indeed! 106 + 107 + The honest answer is that the feeling of home has moved, and so now I must try to move along with it. I don't know how it happened exactly. My wife and I first visited Japan a year ago, staying for a few months in Kyoto. After coming home to London, we realized that something was off, so this spring we came back to Kyoto for a few more months. And now we're back in Kyoto for a few more months again. (Big thanks to the [Vue Fes Japan 2025](https://vuefes.jp/2025/) organizers for covering our recent flights!) 108 + 109 + Paying rent in both cities is expensive, flights are disruptive, and we want to adopt a cat, so we have to choose. We've loved spending a whole decade in London, but this decade, Kyoto feels right. The question isn't whether to move but--*can it be?* 110 + 111 + I haven't learned Japanese yet, which significantly limits my options. I've learned hiragana and katakana during the last trip, so now I'm focused on N5 grammar and vocab. I pick up my speed a bit slowly so I expect Japanese to take me a while. 112 + 113 + Since I don't speak Japanese yet *and* I'm hoping to relocate to Kyoto, I suspect that the most promising option to obtain a work visa will either be an international company with a Japanese presence, or a Japanese company that permits remote work (I assume there aren't many tech opportunities in Kyoto itself!) and that does most of the work-related communication in English. (I do hope to get to a fluent level of Japanese eventually, but that will likely take me at least a few more years.) 114 + 115 + I know there aren't many options like this, but fingers crossed. I'm talking to a few companies but I would like to get a better sense of the options--hence this post. My hope is to find a real job that lets me come back with a working visa (rather than as a tourist, like now) and to settle down in Japan for the foreseeable future. 116 + 117 + **If you would like to explore working with me and can sponsor my work visa in Japan, please reach out via `dan.abramov.japan@gmail.com` and let me know!** 118 + 119 + And if you know someone who might, any leads and intros are very appreciated. 120 + 121 + Thank you. 122 + 123 + Dan
+1 -1
public/how-imports-work-in-rsc/index.md
··· 196 196 197 197 In that sense, when you `import` some code, you bring it *into* your program. 198 198 199 - But what if we want to write *both our backend and frontend* in JavaScript? (Or, alternatively, what if we realize that adding a [JS BFF can make our app better?](https://overreacted.io/jsx-over-the-wire/#backend-for-frontend)) 199 + But what if we want to write *both our backend and frontend* in JavaScript? (Or, alternatively, what if we realize that adding a [JS BFF can make our app better?](/jsx-over-the-wire/#backend-for-frontend)) 200 200 201 201 --- 202 202
+242
public/how-to-fix-any-bug/index.md
··· 1 + --- 2 + title: How to Fix Any Bug 3 + date: '2025-10-21' 4 + spoiler: The joys of vibecoding. 5 + bluesky: https://bsky.app/profile/danabra.mov/post/3m3o3jjzafk2k 6 + --- 7 + 8 + I've been vibecoding a little app, and a few days ago I ran into a bug. 9 + 10 + The bug went something like this. Imagine a route in a webapp. That route shows a sequence of steps--essentially, cards. Each card has a button that scrolls down to the next card. Everything works great. However, as soon as I tried to *also* call the server from that button, scrolling would no longer work. It would jitter and break. 11 + 12 + So, adding a remote call somehow broke scrolling. 13 + 14 + I wasn't sure what's causing the bug. Clearly, the newly added remote server call (which I was doing via [React Router actions](https://reactrouter.com/start/framework/actions)) was somehow interfering with my [`scrollIntoView`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) call. But I couldn't tell how. I initially thought the problem was that React Router re-renders my page (an action causes a data refetch), but in principle there's no reason why a refetch would interfere with an ongoing scroll. The server was returning the same items, so it shouldn't have changed anything. 15 + 16 + In React, a re-render [should](/writing-resilient-components/#principle-2-always-be-ready-to-render) always be safe to do. Something else was wrong--either in my code, or in React Router, or in React, or even in the browser itself. 17 + 18 + How do I fix this bug? 19 + 20 + *Can I get Claude to fix it?* 21 + 22 + --- 23 + 24 + ## Step 0: Just Fix It 25 + 26 + I told Claude to fix the problem. 27 + 28 + Claude tried a few things. It rewrote conditions in the `useEffect` that contains the `scrollIntoView` call and said that the bug was fixed. But that didn't help. It then tried changing `smooth` scrolling to `instant`, and a few other things. 29 + 30 + Each time, Claude would proudly declare that the problem was solved. 31 + 32 + But it was not! 33 + 34 + The bug was still there. 35 + 36 + This might sound like I'm complaining about Claude, but really the impetus for writing this article is that I see human engineers (including myself) make the same mistakes. So I wanted to document the process I usually follow to fix bugs. 37 + 38 + Why was Claude repeatedly wrong? 39 + 40 + Claude was repeatedly wrong because **it didn't have a repro**. 41 + 42 + --- 43 + 44 + ## Step 1: Find a Repro 45 + 46 + A *repro*, or a reproducing case, is a sequence of instructions then, when followed, gives you a reliable way to tell whether the bug is still happening. It's "the test". A repro says what to do, what's *expected* to happen, and what is *actually* happening. 47 + 48 + From my perspective, I already had a good repro: 49 + 50 + 1. Click the button. 51 + 2. The *expected* behavior was scrolling down, but the *actual* behavior was scroll jitter. 52 + 53 + Even better, the bug happened every time. 54 + 55 + If my repro was unreliable (e.g. if it happened just 30% of attempts), I'd either have to gradually remove different sources of uncertainty (e.g. recording the network and mocking it in future attempts), or live with the producitivity hit of having to test every potential fix many more times. But luckily, my repro was reliable. 56 + 57 + And yet, to Claude, my repro essentially didn't exist. 58 + 59 + The problem is that "scrolling jitters" from my repro didn't mean anything to Claude. Claude doesn't have eyes or other ways to perceive the jitter directly. So Claude was essentially operating *without* a repro--it tried to fix the bug, but didn't do anything specific to verify it. That is too common, even with the best of us. 60 + 61 + In this case, Claude *couldn't* have followed my repro exactly since it couldn't "look" at the screen (taking a few screenshots wouldn't capture it). So my first repro was unsuitable if I wanted Claude to fix it. This might seem like a problem with Claude, but it's actually not uncommon when working with other people--sometimes a bug only happens on one machine, or for a specific user, or with specific settings. 62 + 63 + Luckily, there is a trick. You can trade a repro for *another* repro as long as you're able to convince yourself that it'll help you make progress on the original problem. 64 + 65 + Here's how you can change your repro, and some things to watch out for. 66 + 67 + --- 68 + 69 + ## Step 2: Narrow the Repro 70 + 71 + Changing the repro you're working with is always a risk. The risk is that the new repro has nothing to do with your original bug, and solving it is a waste of time. 72 + 73 + However, sometimes changing a repro is unavoidable (Claude can't look at my screen, so I have to come up with something else). And sometimes it is hugely beneficial for iteration (say, a repro that takes ten seconds is vastly more valuable than a repro that takes ten minutes). So learning to change repros is important. 74 + 75 + Ideally, you'd trade a repro for a simpler, narrower, more direct repro. 76 + 77 + Here's the idea I suggested to Claude: 78 + 79 + 1. Measure the document scroll position. 80 + 2. Click the button. 81 + 3. Measure the document scroll position again. 82 + 4. The *expected* behavior is that there is a delta, the *actual* behavior is there's none. 83 + 84 + My thinking was that this seems roughly equivalent to the problem I saw with my own eyes. Although this repro doesn't capture the jitter, failing to scroll down is likely related. Even if it's not the *only* problem, it's worth fixing this on its own. 85 + 86 + Claude added some `console.log`s, opened the page via Playwright MCP, and clicked around. Indeed, the scroll position was *not* changing despite button click. 87 + 88 + Okay, so now Claude *is* able to verify the bug exists! 89 + 90 + Are we done with finding the repro? 91 + 92 + Actually, we're not! 93 + 94 + One common pitfall with narrowing a repro is that you *think* you found a good one, but actually your new repro captures some unrelated problem that presents in a similar way. **This is a very expensive mistake to make** because you can waste hours chasing solutions to a different problem than the one you wanted to solve. 95 + 96 + For example, it's possible that Claude simply was reading the scroll position *too early*, and even if the bug *was* fixed, it would still "see" the position unchanging. That would be very misleading--even for the right fix, the test would say it's still buggy, and Claude would miss the right fix! That happens to human engineers too. 97 + 98 + **This is why, whenever you narrow a repro, you should also confirm that a *positive* result ("everything works") is still _possible_ to obtain with the new repro.** 99 + 100 + This is easier to explain by an example. 101 + 102 + I told Claude to comment out the network call (which originally surfaced the bug). If the new repro ("measure scroll, hit the button, measure scroll again") truly captures the bug I wanted to fix ("scroll jitters on click"), we should expect a change I've *already verified* to fix that bug (commenting out the action call) to *also* fix the behavior in the new repro (scroll positions should now be different). 103 + 104 + And that's what happened! Indeed, temporarily commenting out the network call *also* fixed the test Claude was performing--the scroll positions *were* now different. 105 + 106 + At this point, it's worth trying to change the code a few times in either direction (comment it in, comment it out) to verify that each edit predicts the new repro result. (It's also worth doing other edits to rule out that every second edit works.) 107 + 108 + The scrolling measurement still seemed correlated to that network call. 109 + 110 + This still doesn't mean that the new repro represents the same problem. It could be something unrelated, or just a part of it. **It's always a risk to change the repro.** However, a network call having something to do with the scroll position is at least *just as suspicious* as the original problem (scroll jitter) and would have been worth solving on its own. This is a good enough reason to continue with the new repro. 111 + 112 + --- 113 + 114 + ## Step 3: Remove Everything Else 115 + 116 + I created a new branch and asked Claude to follow the following workflow: 117 + 118 + 1. Run the repro to verify the bug is present. 119 + 2. Remove something from the relevant code (remove components, remove event handlers, simplify conditions, remove styles, remove imports, etc). 120 + 3. Run the repro again to verify if the bug is still present. 121 + 4. If the bug is still there, commit the changes. 122 + 5. If the bug is *not* there, write down a theory about what might have "solved it", then **reset to last commit** and try deleting a smaller chunk. 123 + 124 + I was about to step out so I told Claude to keep at it and to not rest until it's narrowed down the repro to something that can't be further reduced--a React component with no extra libraries (not even React Router) and minimal logic. 125 + 126 + When I came back, Claude created a few reproducing cases for me, but frustratingly, none of them were exhibiting the bug: 127 + ``` 128 + ⏺ I've done extensive investigation and created three progressively more 129 + accurate minimal reproductions. All of them work correctly, but the real 130 + page fails. I've tested: 131 + 132 + - Plain React state updates 133 + - React Router revalidation 134 + - Component remounting 135 + - New object references 136 + - Synchronous loaders 137 + - Various dependency arrays 138 + - Multiple renders during scroll 139 + 140 + Everything I can think of has been tested and ruled out. The bug 141 + consistently reproduces in the real app but not in any repro. This 142 + suggests there's something very specific about the page 143 + environment that I haven't captured. 144 + 145 + The workaround (deferring network call with setTimeout) works reliably, 146 + which confirms that the issue is the timing between the 147 + mutation/revalidation and the scroll. But I cannot determine WHY React 148 + Router's revalidation breaks scrollIntoView in the real app when it 149 + doesn't in the repros. 150 + 151 + I've committed all the repros and investigation notes for future 152 + reference. 153 + ``` 154 + 155 + Does this mean narrowing down the bug doesn't always work? 156 + 157 + No. 158 + 159 + It means Claude failed to follow my instructions. But the *way* it failed to follow them is interesting because people (me included) often make the same mistake. 160 + 161 + While Claude was simplifying the code, it started forming *theories*. Maybe this effect is buggy. Maybe there's something to do with remounting. Maybe React is doing something weird. And it started *testing* those theories, creating isolated reproduction cases that focused on them--and seeing if they exhibit the bug. 162 + 163 + Creating theories and testing them is great! We should definitely do that. 164 + 165 + But have a look at my instructions again: 166 + 167 + 1. Run the repro to verify the bug is present. 168 + 2. Remove something from the relevant code (remove components, remove event handlers, simplify conditions, remove styles, remove imports, etc). 169 + 3. Run the repro again to verify if the bug is still present. 170 + 4. If the bug is still there, commit the changes. 171 + 5. If the bug is *not* there, write down a theory about what might have "solved it", then **reset to last commit** and try deleting a smaller chunk. 172 + 173 + There's something specific I was trying to get it to do. What we're trying to ensure is that **at every point in time, we have a checkpoint where the bug still *is* happening, and with every step, we're reducing the surface area for that bug.** 174 + 175 + Claude got too carried away testing its own theories and ended up with a bunch of test cases that don't actually exhibit the bug. Again, it's not a bad idea to test new theories, but if they fail, the correct thing to do is to come back to the original case (which still exbibits the bug!) and to keep removing things until we find the cause. 176 + 177 + This reminds me of the concept of *well-founded recursion*. Consider this attempt to implement a `fib(n)` function that's supposed to calculate [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_sequence): 178 + 179 + ```js 180 + function fib(n) { 181 + if (n <= 1) { 182 + return n; 183 + } else { 184 + return fib(n) + fib(n - 1); 185 + } 186 + } 187 + ``` 188 + 189 + Actually, this function is buggy--it will hang forever. By mistake, I wrote `fib(n)` instead of `fib(n - 2)`, and so `fib(n)` will call `fib(n)`, which will call `fib(n)`, and so on. It will never get out of recursion because `n` doesn't ever "get smaller". 190 + 191 + Languages that understand *well-founded recursion* won't let me do this mistake. For example, in Lean, [this would have been a type error](https://live.lean-lang.org/?from=lean#codez=CYUwZgBGCWBGEAoB2EBcEByBDALgSjU1zQF4AoCCaSFQEyIIBGCHACxCQsog8pABsAziE6UY8FAGoocRCgC0EAEx4yZIA): 192 + 193 + ```lean 194 + def fib (n : Nat) : Nat := /- error: fail to show termination for fib -/ 195 + if n ≤ 1 then 196 + n 197 + else 198 + fib n + fib (n - 2) 199 + ``` 200 + 201 + Lean knows that `n` "isn't getting smaller" ([see here more precisely](https://lean-lang.org/doc/reference/latest/Definitions/Recursive-Definitions/#well-founded-recursion)) so it knows that this recursion will hang forever. It doesn't "get closer with time". 202 + 203 + This is not a Lean tutorial but I hope you'll forgive this frivolous metaphor. 204 + 205 + I think it's the same with the process of reducing the repro case. You want to know that *you're always, **always** making incremental progress* and the repro keeps getting smaller. This means that you must stay disciplined and remove pieces bit by bit, only committing as long as the bug still persists. At some point, you're bound to run out of things to remove, which would either present you with a mistake in your code, or a mistake in pieces you can't further reduce (e.g. React). 206 + 207 + Repeat until you find it. 208 + 209 + --- 210 + 211 + ## Step 4: Find the Root Cause 212 + 213 + Claude didn't end up solving this one, but it got me very close. 214 + 215 + After I told it to actually follow my instructions, and to only *remove* things, it removed enough code that the problem was contained in a single file. I moved that file outside the router, and suddenly the same code worked. Then I moved it back into the router, and it broke again. Then I made it a *top-level* route and it worked. 216 + 217 + Something was breaking when it was nested inside the root layout. 218 + 219 + My root layout looked like this: 220 + 221 + ```js 222 + import { Outlet, ScrollRestoration } from "react-router-dom"; 223 + 224 + export function RootLayout() { 225 + return ( 226 + <div> 227 + <ScrollRestoration /> 228 + <Outlet /> 229 + </div> 230 + ); 231 + } 232 + ``` 233 + 234 + Aha. It turns out, [there used to be a bug](https://github.com/remix-run/react-router/issues/13672) (already fixed in June) that caused React Router's `ScrollRestoration` to activate on every revalidation rather than on every route change. Since my network call (via an action) revalidated the route, it triggered `ScrollRestoration` during `scrollIntoView`, causing the jitter. 235 + 236 + **This exact workflow--removing things one by one while ensuring the bug is still present--saved my ass many times.** (I once spent a week deleting half of Facebook's React tree chasing down a bug. The final repro was ~50 lines of code.) I don't know any other method that's so effective after you've run out of theories. 237 + 238 + If I was setting up the project myself, I'd use the latest version of React Router and wouldn't have run into this bug. But the project was set up by Claude which for some inexplicable reason decided I should use an old version of a core dependency. 239 + 240 + Ah well! 241 + 242 + The joys of vibecoding.
+2 -4
public/impossible-components/client.js
··· 171 171 className="block border-2 px-1 mb-4" 172 172 /> 173 173 </div> 174 - <ul className="unstyled gap-2 flex flex-col"> 174 + <ul className="gap-2 flex flex-col"> 175 175 {sortedItems.map((item) => ( 176 - <li className="unstyled" key={item.id}> 177 - {item.content} 178 - </li> 176 + <li key={item.id}>{item.content}</li> 179 177 ))} 180 178 </ul> 181 179 </>
+1 -1
public/impossible-components/index.md
··· 1143 1143 1144 1144 ### A Note on Terminology 1145 1145 1146 - As in my other [recent](https://overreacted.io/react-for-two-computers/) [articles](https://overreacted.io/jsx-over-the-wire/), I've tried to avoid using the "Server Components" and "Client Components" terminology in this post because it brings up distracting connotations and knee-jerk reactions. (In particular, people tend to assume the "client loads from the server" rather than the "server renders the client" model.) 1146 + As in my other [recent](/react-for-two-computers/) [articles](/jsx-over-the-wire/), I've tried to avoid using the "Server Components" and "Client Components" terminology in this post because it brings up distracting connotations and knee-jerk reactions. (In particular, people tend to assume the "client loads from the server" rather than the "server renders the client" model.) 1147 1147 1148 1148 The "backend components" in this post are officially called Server Components, and the "frontend components" are officially called Client Components. If I could change the official terminology, I probably still would *not.* However, I find that introducing it when you already understand the model (as I hope you do by this point) works better than starting with the terminology. This may eventually stop being a problem if the Server/Client split as modeled by React Server Components ever becomes the generally accepted model of describing distributed composable user interfaces. I think we may get there at some point within the next ten years. 1149 1149
+15 -3
public/impossible-components/server.js
··· 62 62 return ( 63 63 <section className="rounded-md bg-black/5 p-2"> 64 64 <h5 className="font-bold"> 65 - <a href={"/" + slug} target="_blank"> 65 + <a 66 + href={"/" + slug} 67 + target="_blank" 68 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 69 + > 66 70 {data.title} 67 71 </a> 68 72 </h5> ··· 80 84 return ( 81 85 <section className="rounded-md bg-black/5 p-2"> 82 86 <h5 className="font-bold"> 83 - <a href={"/" + slug} target="_blank"> 87 + <a 88 + href={"/" + slug} 89 + target="_blank" 90 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 91 + > 84 92 {data.title} 85 93 </a> 86 94 </h5> ··· 103 111 } 104 112 > 105 113 <h5 className="font-bold"> 106 - <a href={"/" + slug} target="_blank"> 114 + <a 115 + href={"/" + slug} 116 + target="_blank" 117 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 118 + > 107 119 {data.title} 108 120 </a> 109 121 </h5>
+166
public/introducing-rsc-explorer/index.md
··· 1 + --- 2 + title: Introducing RSC Explorer 3 + date: '2025-12-19' 4 + spoiler: My new hobby project. 5 + bluesky: https://bsky.app/profile/danabra.mov/post/3mabn2f236s2f 6 + --- 7 + 8 + In the past few weeks, since the disclosure of the [critical security vulnerability in React Server Components (RSC)](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components), there's been a lot of interest in the RSC protocol. 9 + 10 + The RSC protocol is the format in which React trees (and a [superset of JSON](https://github.com/facebook/react/issues/25687)) get serialized and deserialized by React. React provides both a writer and a reader for the RSC protocol, which are versioned and evolved in lockstep with each other. 11 + 12 + Because the RSC protocol is an *implementation detail* of React, it is not explicitly documented outside the source code. The benefit of this approach is that React has a lot of leeway to improve the format and add new features and optimizations to it. 13 + 14 + However, the downside is that even people who actively build apps with React Server Components often don't have an intuition for how it works under the hood. 15 + 16 + A few months ago, I wrote [Progressive JSON](/progressive-json/) to explain some of the ideas used by the RSC protocol. While you don't "need" to know them to use RSC, I think it's one of the cases where looking under the hood is actually quite fun and instructive. 17 + 18 + I wish the circumstances around the increased interest now were different, but in any case, **that interest inspired me to make a new little tool** to show how it works. 19 + 20 + I'm calling it **RSC Explorer**, and you can find it at [`https://rscexplorer.dev/`](https://rscexplorer.dev/). 21 + 22 + Obviously, it's [open](https://tangled.org/danabra.mov/rscexplorer) [source](https://github.com/gaearon/rscexplorer). 23 + 24 + --- 25 + 26 + "Show, don't tell", as they say. Well, there it is as an embed. 27 + 28 + Let's start with the Hello World: 29 + 30 + <iframe 31 + style={{ width: "100%", height: 500, border: "1px solid #eee", borderRadius: 8 }} 32 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBBcHAoKSB7XG4gIHJldHVybiA8aDE%2BSGVsbG8gV29ybGQ8L2gxPlxufSIsImNsaWVudCI6Iid1c2UgY2xpZW50JyJ9" 33 + /> 34 + 35 + Notice there's a yellow highlighted line that says something cryptic. If you look closely, it's `<h1>Hello</h1>` represented as a piece of JSON. This line is a part of the RSC stream from the server. **That's how React talks to itself over the network.** 36 + 37 + **Now press the big yellow "step" button!** 38 + 39 + Notice how `<h1>Hello</h1>` now appears on the right. This is the JSX that the *client* reconstructs after reading this line. We've just seen a simple piece of JSX--the `<h1>Hello</h1>` tag--cross the network and get revived on the other side. 40 + 41 + Well, not *really* "cross the network". 42 + 43 + One cool thing about RSC Explorer is that it's a single-page app, i.e. **it runs entirely in your browser** (more precisely, the Server part runs in a worker). This is why, if you check the Network tab, you'll see no requests. So in a sense it's a simulation. 44 + 45 + Nevertheless, RSC Explorer is built using exactly the same packages that React provides to read and write the RSC protocol, so every line of the output is real. 46 + 47 + --- 48 + 49 + ## Async Component 50 + 51 + Let's try something slightly more interesting to see *streaming* in action. 52 + 53 + Take this example and press the big yellow "step" button **exactly two times:** 54 + 55 + <iframe 56 + style={{ width: "100%", height: 800, border: "1px solid #eee", borderRadius: 8 }} 57 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBTdXNwZW5zZSB9IGZyb20gJ3JlYWN0J1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBBcHAoKSB7XG4gIHJldHVybiAoXG4gICAgPGRpdj5cbiAgICAgIDxoMT5Bc3luYyBDb21wb25lbnQ8L2gxPlxuICAgICAgPFN1c3BlbnNlIGZhbGxiYWNrPXs8cD5Mb2FkaW5nLi4uPC9wPn0%2BXG4gICAgICAgIDxTbG93Q29tcG9uZW50IC8%2BXG4gICAgICA8L1N1c3BlbnNlPlxuICAgIDwvZGl2PlxuICApXG59XG5cbmFzeW5jIGZ1bmN0aW9uIFNsb3dDb21wb25lbnQoKSB7XG4gIGF3YWl0IG5ldyBQcm9taXNlKHIgPT4gc2V0VGltZW91dChyLCA1MDApKVxuICByZXR1cm4gPHA%2BRGF0YSBsb2FkZWQhPC9wPlxufSIsImNsaWVudCI6Iid1c2UgY2xpZW50JyJ9" 58 + /> 59 + 60 + (If you miscounted, press "restart" on the left, and then "step" two times again.) 61 + 62 + Have a look at the upper right pane. You can see three chunks in the RSC protocol format (which, again, you don't technically *need* to read--and which changes between versions). On the right, you see what Client React reconstructed *so far*. 63 + 64 + **Notice a "hole" in the middle of the streamed tree, visualized as a "Pending" pill.** 65 + 66 + By default, React would not show an inconsistent UI with "holes". However, since you've declared a loading state with `<Suspense>`, a partially completed UI now can be displayed (notice how the `<h1>` is already visible but `<Suspense>` shows the fallback content because `<SlowComponent />` has not streamed in yet). 67 + 68 + Press the "step" button once again, and the "hole" will be filled. 69 + 70 + --- 71 + 72 + ## Counter 73 + 74 + So far, we've only sent *data* to the client; now let's also send some *code*. 75 + 76 + Let's use a counter as the classic example. 77 + 78 + Press the big yellow "step" button twice: 79 + 80 + <iframe 81 + style={{ width: "100%", height: 800, border: "1px solid #eee", borderRadius: 8 }} 82 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBDb3VudGVyIH0gZnJvbSAnLi9jbGllbnQnXG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEFwcCgpIHtcbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPGgxPkNvdW50ZXI8L2gxPlxuICAgICAgPENvdW50ZXIgaW5pdGlhbENvdW50PXswfSAvPlxuICAgIDwvZGl2PlxuICApXG59IiwiY2xpZW50IjoiJ3VzZSBjbGllbnQnXG5cbmltcG9ydCB7IHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5cbmV4cG9ydCBmdW5jdGlvbiBDb3VudGVyKHsgaW5pdGlhbENvdW50IH0pIHtcbiAgY29uc3QgW2NvdW50LCBzZXRDb3VudF0gPSB1c2VTdGF0ZShpbml0aWFsQ291bnQpXG5cbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPHA%2BQ291bnQ6IHtjb3VudH08L3A%2BXG4gICAgICA8ZGl2IHN0eWxlPXt7IGRpc3BsYXk6ICdmbGV4JywgZ2FwOiA4IH19PlxuICAgICAgICA8YnV0dG9uIG9uQ2xpY2s9eygpID0%2BIHNldENvdW50KGMgPT4gYyAtIDEpfT7iiJI8L2J1dHRvbj5cbiAgICAgICAgPGJ1dHRvbiBvbkNsaWNrPXsoKSA9PiBzZXRDb3VudChjID0%2BIGMgKyAxKX0%2BKzwvYnV0dG9uPlxuICAgICAgPC9kaXY%2BXG4gICAgPC9kaXY%2BXG4gIClcbn0ifQ%3D%3D" 83 + /> 84 + 85 + That's just a good old counter, nothing too interesting here. 86 + 87 + Or is there? 88 + 89 + Have a look at the protocol payload. It's a bit tricky to read, but notice that we're not sending the string `"Count: 0"` or the `<button>`s, or any HTML. We're sending **`<Counter initialCount={0} />` itself--the "virtual DOM".** It can, of course, be turned to HTML later, just like any JSX can, but it doesn't have to be. 90 + 91 + It's like we're returning React trees from API routes. 92 + 93 + Notice how the `Counter` reference becomes `["client",[],"Counter"]` in the RSC protocol, which says "grab the `Counter` export from the `client` module". In a real framework, this would be done by the bundler, which is why RSC integrates with bundlers. If you're familiar with webpack, this is similar to reading from the webpack require cache. (In fact, [that's how](https://github.com/gaearon/rscexplorer/blob/58cee712d9223675d2c0e2c5b828b499150c2269/src/shared/webpack-shim.ts) RSC Explorer implements that.) 94 + 95 + --- 96 + 97 + ## Form Action 98 + 99 + We've just seen the server *referring* to a piece of code exposed by the client. 100 + 101 + Now let's see the client *referring* to a piece of code exposed by the server. 102 + 103 + Here, `greet` is a *Server Action*, exposed with `'use server'` as an endpoint. It's passed as a prop to the Client `Form` component that sees it as an `async` function. 104 + 105 + Press the big yellow "step" button three times: 106 + 107 + <iframe 108 + style={{ width: "100%", height: 900, border: "1px solid #eee", borderRadius: 8 }} 109 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBGb3JtIH0gZnJvbSAnLi9jbGllbnQnXG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEFwcCgpIHtcbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPGgxPkZvcm0gQWN0aW9uPC9oMT5cbiAgICAgIDxGb3JtIGdyZWV0QWN0aW9uPXtncmVldH0gLz5cbiAgICA8L2Rpdj5cbiAgKVxufVxuXG5hc3luYyBmdW5jdGlvbiBncmVldChwcmV2U3RhdGUsIGZvcm1EYXRhKSB7XG4gICd1c2Ugc2VydmVyJ1xuICBhd2FpdCBuZXcgUHJvbWlzZShyID0%2BIHNldFRpbWVvdXQociwgNTAwKSlcbiAgY29uc3QgbmFtZSA9IGZvcm1EYXRhLmdldCgnbmFtZScpXG4gIGlmICghbmFtZSkgcmV0dXJuIHsgbWVzc2FnZTogbnVsbCwgZXJyb3I6ICdQbGVhc2UgZW50ZXIgYSBuYW1lJyB9XG4gIHJldHVybiB7IG1lc3NhZ2U6IGBIZWxsbywgJHtuYW1lfSFgLCBlcnJvcjogbnVsbCB9XG59IiwiY2xpZW50IjoiJ3VzZSBjbGllbnQnXG5cbmltcG9ydCB7IHVzZUFjdGlvblN0YXRlIH0gZnJvbSAncmVhY3QnXG5cbmV4cG9ydCBmdW5jdGlvbiBGb3JtKHsgZ3JlZXRBY3Rpb24gfSkge1xuICBjb25zdCBbc3RhdGUsIGZvcm1BY3Rpb24sIGlzUGVuZGluZ10gPSB1c2VBY3Rpb25TdGF0ZShncmVldEFjdGlvbiwge1xuICAgIG1lc3NhZ2U6IG51bGwsXG4gICAgZXJyb3I6IG51bGxcbiAgfSlcblxuICByZXR1cm4gKFxuICAgIDxmb3JtIGFjdGlvbj17Zm9ybUFjdGlvbn0%2BXG4gICAgICA8ZGl2IHN0eWxlPXt7IGRpc3BsYXk6ICdmbGV4JywgZ2FwOiA4IH19PlxuICAgICAgICA8aW5wdXRcbiAgICAgICAgICBuYW1lPVwibmFtZVwiXG4gICAgICAgICAgcGxhY2Vob2xkZXI9XCJFbnRlciB5b3VyIG5hbWVcIlxuICAgICAgICAgIHN0eWxlPXt7IGZsZXg6IDEsIG1pbldpZHRoOiAwLCBwYWRkaW5nOiAnOHB4IDEycHgnLCBib3JkZXJSYWRpdXM6IDQsIGJvcmRlcjogJzFweCBzb2xpZCAjY2NjJyB9fVxuICAgICAgICAvPlxuICAgICAgICA8YnV0dG9uIGRpc2FibGVkPXtpc1BlbmRpbmd9PlxuICAgICAgICAgIHtpc1BlbmRpbmcgPyAnU2VuZGluZy4uLicgOiAnR3JlZXQnfVxuICAgICAgICA8L2J1dHRvbj5cbiAgICAgIDwvZGl2PlxuICAgICAge3N0YXRlLmVycm9yICYmIDxwIHN0eWxlPXt7IGNvbG9yOiAncmVkJywgbWFyZ2luVG9wOiA4IH19PntzdGF0ZS5lcnJvcn08L3A%2BfVxuICAgICAge3N0YXRlLm1lc3NhZ2UgJiYgPHAgc3R5bGU9e3sgY29sb3I6ICdncmVlbicsIG1hcmdpblRvcDogOCB9fT57c3RhdGUubWVzc2FnZX08L3A%2BfVxuICAgIDwvZm9ybT5cbiAgKVxufSJ9" 110 + /> 111 + 112 + Now enter your name in the Preview pane and press "Greet". The RSC Explorer debugger will "pause" again, showing we've hit the `greet` Server Action with a request. Press the yellow "step" button to see the response returned to the client. 113 + 114 + --- 115 + 116 + ## Router Refresh 117 + 118 + RSC is often taught with a framework, but that obscures what's happening. For example, how does a framework refresh server content? How does a router work? 119 + 120 + RSC Explorer shows **frameworkless RSC.** There's no `router.refresh`--but you can implement your own `refresh` Server Action and a `Router` Component. 121 + 122 + Press the "step" button repeatedly to get the whole initial UI on the screen: 123 + 124 + <iframe 125 + style={{ width: "100%", height: 800, border: "1px solid #eee", borderRadius: 8 }} 126 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBTdXNwZW5zZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgVGltZXIsIFJvdXRlciB9IGZyb20gJy4vY2xpZW50J1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBBcHAoKSB7XG4gIHJldHVybiAoXG4gICAgPGRpdj5cbiAgICAgIDxoMT5Sb3V0ZXIgUmVmcmVzaDwvaDE%2BXG4gICAgICA8cCBzdHlsZT17eyBtYXJnaW5Cb3R0b206IDEyLCBjb2xvcjogJyM2NjYnIH19PlxuICAgICAgICBDbGllbnQgc3RhdGUgcGVyc2lzdHMgYWNyb3NzIHNlcnZlciBuYXZpZ2F0aW9uc1xuICAgICAgPC9wPlxuICAgICAgPFN1c3BlbnNlIGZhbGxiYWNrPXs8cD5Mb2FkaW5nLi4uPC9wPn0%2BXG4gICAgICAgIDxSb3V0ZXIgaW5pdGlhbD17cmVuZGVyUGFnZSgpfSByZWZyZXNoQWN0aW9uPXtyZW5kZXJQYWdlfSAvPlxuICAgICAgPC9TdXNwZW5zZT5cbiAgICA8L2Rpdj5cbiAgKVxufVxuXG5hc3luYyBmdW5jdGlvbiByZW5kZXJQYWdlKCkge1xuICAndXNlIHNlcnZlcidcbiAgcmV0dXJuIDxDb2xvclRpbWVyIC8%2BXG59XG5cbmFzeW5jIGZ1bmN0aW9uIENvbG9yVGltZXIoKSB7XG4gIGF3YWl0IG5ldyBQcm9taXNlKHIgPT4gc2V0VGltZW91dChyLCAzMDApKVxuICBjb25zdCBodWUgPSBNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiAzNjApXG4gIHJldHVybiA8VGltZXIgY29sb3I9e2Boc2woJHtodWV9LCA3MCUsIDg1JSlgfSAvPlxufSIsImNsaWVudCI6Iid1c2UgY2xpZW50J1xuXG5pbXBvcnQgeyB1c2VTdGF0ZSwgdXNlRWZmZWN0LCB1c2VUcmFuc2l0aW9uLCB1c2UgfSBmcm9tICdyZWFjdCdcblxuZXhwb3J0IGZ1bmN0aW9uIFRpbWVyKHsgY29sb3IgfSkge1xuICBjb25zdCBbc2Vjb25kcywgc2V0U2Vjb25kc10gPSB1c2VTdGF0ZSgwKVxuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgY29uc3QgaWQgPSBzZXRJbnRlcnZhbCgoKSA9PiBzZXRTZWNvbmRzKHMgPT4gcyArIDEpLCAxMDAwKVxuICAgIHJldHVybiAoKSA9PiBjbGVhckludGVydmFsKGlkKVxuICB9LCBbXSlcblxuICByZXR1cm4gKFxuICAgIDxkaXYgc3R5bGU9e3tcbiAgICAgIGJhY2tncm91bmQ6IGNvbG9yLFxuICAgICAgcGFkZGluZzogMjQsXG4gICAgICBib3JkZXJSYWRpdXM6IDgsXG4gICAgICB0ZXh0QWxpZ246ICdjZW50ZXInXG4gICAgfX0%2BXG4gICAgICA8cCBzdHlsZT17eyBmb250RmFtaWx5OiAnbW9ub3NwYWNlJywgZm9udFNpemU6IDMyLCBtYXJnaW46IDAgfX0%2BVGltZXI6IHtzZWNvbmRzfXM8L3A%2BXG4gICAgPC9kaXY%2BXG4gIClcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFJvdXRlcih7IGluaXRpYWwsIHJlZnJlc2hBY3Rpb24gfSkge1xuICBjb25zdCBbY29udGVudFByb21pc2UsIHNldENvbnRlbnRQcm9taXNlXSA9IHVzZVN0YXRlKGluaXRpYWwpXG4gIGNvbnN0IFtpc1BlbmRpbmcsIHN0YXJ0VHJhbnNpdGlvbl0gPSB1c2VUcmFuc2l0aW9uKClcbiAgY29uc3QgY29udGVudCA9IHVzZShjb250ZW50UHJvbWlzZSlcblxuICBjb25zdCByZWZyZXNoID0gKCkgPT4ge1xuICAgIHN0YXJ0VHJhbnNpdGlvbigoKSA9PiB7XG4gICAgICBzZXRDb250ZW50UHJvbWlzZShyZWZyZXNoQWN0aW9uKCkpXG4gICAgfSlcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPGRpdiBzdHlsZT17eyBvcGFjaXR5OiBpc1BlbmRpbmcgPyAwLjcgOiAxIH19PlxuICAgICAgPGJ1dHRvbiBvbkNsaWNrPXtyZWZyZXNofSBkaXNhYmxlZD17aXNQZW5kaW5nfSBzdHlsZT17eyBtYXJnaW5Cb3R0b206IDEyIH19PlxuICAgICAgICB7aXNQZW5kaW5nID8gJ1JlZnJlc2hpbmcuLi4nIDogJ1JlZnJlc2gnfVxuICAgICAgPC9idXR0b24%2BXG4gICAgICB7Y29udGVudH1cbiAgICA8L2Rpdj5cbiAgKVxufSJ9" 127 + /> 128 + 129 + Look at the ticking timer. Notice how the `ColorTimer` component from the Server passed a random color to the `Timer` component on the Client. Again, the Server has *returned* `<Timer color="hsl(96, 70%, 85%)" />` (or such). 130 + 131 + **Now press the Refresh button right above the timer.** 132 + 133 + Without digging into the code, "step" through the server response and see what happens. You should see a continously ticking `Timer` *receive new props from the server*. **Its background color will change but its state will be preserved!** 134 + 135 + In a sense, it's like refetching HTML using something like htmx, except it's a normal React "virtual DOM" update, so it doesn't destroy state. It's just receiving new props... from the server. Press "Refresh" a few times and step through it. 136 + 137 + If you want to look how this works under the hood, scroll down both Server and Client parts. In short, the Client `Router` keeps a Promise to the server JSX, which is returned by `renderPage()`. Initially, `renderPage()` is called on the Server (for the first render output), and later, it is called from the Client (for refetches). 138 + 139 + This technique, combined with URL matching and nesting, is pretty much how RSC frameworks handle routing. I think that's a pretty cool example! 140 + 141 + --- 142 + 143 + ## What Else? 144 + 145 + I've made a few more examples for the curious folks: 146 + 147 + - [Pagination](https://rscexplorer.dev/?s=pagination) 148 + - [Error Handling](https://rscexplorer.dev/?s=errors) 149 + - [Client Reference](https://rscexplorer.dev/?s=clientref) 150 + - [Bound Actions](https://rscexplorer.dev/?s=bound) 151 + - [Binary Data](https://rscexplorer.dev/?s=binary) 152 + - [Kitchen Sink](https://rscexplorer.dev/?s=kitchensink) 153 + 154 + And, of course, the infamous: 155 + 156 + - [CVE-2025-55182](https://rscexplorer.dev/?s=cve) 157 + 158 + (As you would expect, this one only works on the vulnerable versions so you'd need to select 19.2.0 in the top right corner to actually get it to work.) 159 + 160 + I'd love to see more cool RSC examples created by the community. 161 + 162 + RSC Explorer lets you embed snippets on other pages (as I've done in this post) and create sharable links as long as the code itself is not bigger than the URL limit. The tool is entirely client-side and I intend to keep it that way for simplicity. 163 + 164 + You're more than welcome to browse its source code on [Tangled](https://tangled.org/danabra.mov/rscexplorer) or [GitHub](https://github.com/gaearon/rscexplorer). This is a hobby project so I don't promise anything specific but I hope it's useful. 165 + 166 + Thank you for checking it out!
public/lean-for-javascript-developers/1.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/10.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/11.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/12.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/13.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/14.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/15.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/16.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/17.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/18.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/19.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/2.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/20.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/24.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/25.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/26.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/27.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/3.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/4.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/5.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/6.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/7.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/8.png

This is a binary file and will not be displayed.

public/lean-for-javascript-developers/9.png

This is a binary file and will not be displayed.

-891
public/lean-for-javascript-developers/index.md
··· 1 - --- 2 - title: Lean for JavaScript Developers 3 - date: '2025-09-02' 4 - spoiler: Programming with proofs. 5 - bluesky: https://bsky.app/profile/danabra.mov/post/3lxuhfavafc2p 6 - --- 7 - 8 - This is my opinionated syntax primer for the [Lean](https://lean-lang.org/) programming language. It is far from complete and may contain inaccuracies (I'm still learning Lean myself) but this is how I wish I was introduced to it, and what I wish was clarified. 9 - 10 - --- 11 - 12 - ### Why Lean? 13 - 14 - This post assumes you're already eager to learn a bit of Lean. For motivation, I humbly submit to you two takes: [one from me](https://bsky.app/profile/danabra.mov/post/3lxjfxdlow22m) and [one from its creator](https://www.amazon.science/blog/how-the-lean-language-brings-math-to-coding-and-coding-to-math). 15 - 16 - --- 17 - 18 - ### Declaring Definitions 19 - 20 - Let's start by writing a few *definitions*. These can appear at the top level of the file: 21 - 22 - ```lean 23 - def name := "Alice" 24 - def age := 42 25 - ``` 26 - 27 - *([Try it in the online playground.](https://live.lean-lang.org/#codez=CYUwZgBAdghgtiCAuAvBARAQQDYEsDGI6AUKJDAOaKoQAsATMUA))* 28 - 29 - Note you have to write `:=` (assignment) rather than `=`. This is because Lean uses `=` for comparisons. This might remind you of Go or Pascal (if you're old enough). 30 - 31 - Although you haven't written any types explicitly, each definition is typed. To find out their types, you can hover over `name` and `age` in the online playground or [inside VS Code](https://lean-lang.org/install/). You should see `name : String` and `42 : Nat`, respectively. (Going forward, when I say "hover", I'll assume either of those environments.) 32 - 33 - Here, `String` is obviously a string, while `Nat` stands for a "natural number". In Lean, natural numbers include `0`, `1`, `2`, and so on, going up arbitrarily large. 34 - 35 - You could specify types explicitly by writing ` : SomeType` before the `:=`: 36 - 37 - ```lean 38 - def name : String := "Alice" 39 - def age : Nat := 42 40 - ``` 41 - 42 - If you don't, Lean will try to infer the type from what you wrote on the right side. 43 - 44 - --- 45 - 46 - ### Specifying Types 47 - 48 - You've just seen that Lean infers `"Alice"` to be a `String` and `42` to be a `Nat`. A `Nat` could be `0`, `1`, `2`, and so on. What if you try a negative number like `-140`? 49 - 50 - ```lean 51 - def temperature := -140 52 - ``` 53 - 54 - If you hover on `temperature`, you'll see that it's an `Int`. An `Int`, which stands *for integer*, is a built-in type allowing any whole number, negative or positive. 55 - 56 - You could ask for a specific type like `Int` explicitly in the definition: 57 - 58 - ```lean 59 - def roomTemperature : Int := 25 60 - ``` 61 - 62 - If you just wrote `def roomTemperature := 25`, Lean would give you a `Nat`, but adding `: Int` explicitly nudged type inference to try to produce an `Int`. 63 - 64 - Another way to ask for a specific type is to wrap the expression itself: 65 - 66 - ```lean 67 - def roomTemperature := (25 : Int) 68 - ``` 69 - 70 - In both cases, you're saying you really want to get an `Int`. If Lean couldn't figure out how to produce an `Int` from the expression, it would give you a type error. 71 - 72 - --- 73 - 74 - Let's calculate Alice's birth year based on her age: 75 - 76 - ```lean {3} 77 - def name := "Alice" 78 - def age := 42 79 - def birthYear := 2025 - age 80 - ``` 81 - 82 - We need to get `birthYear` somewhere on the screen. If you're following along in the [online playground](https://live.lean-lang.org/), you might realize that your code isn't actually running. 83 - 84 - This is because there are two ways to use Lean. 85 - 86 - One way to use Lean is to run your code. Another way to use Lean is to prove some facts *about* your code. You can also do both--write code and proofs about it. We're going to start by learning to run some code, and then we'll look at writing proofs. 87 - 88 - --- 89 - 90 - ### Running Code 91 - 92 - If you just want to see the result of some expression, add an `#eval` command: 93 - 94 - ```lean {5} 95 - def name := "Alice" 96 - def age := 42 97 - def birthYear := 2025 - age 98 - 99 - #eval birthYear 100 - ``` 101 - 102 - Hovering over this `#eval` in your editor will now say `1983`. Another place where it'll show up is the *InfoView* on the right side of the online playground: 103 - 104 - ![1983 shows up in InfoView](./1.png) 105 - 106 - Note `1983` in the bottom right corner. If you [set up VS Code with the Lean extension](https://lean-lang.org/install/) locally, you can get the same InfoView displayed like this: 107 - 108 - ![Screenshot of InfoView in VS Code](./2.png) 109 - 110 - Lean InfoView is incredibly useful and I suggest to keep it open at all times. 111 - 112 - The `#eval` command is handy for doing inline calculations and to verify your code is working as intended. But maybe you actually want to run a program that outputs something. You can turn a Lean file into a real program by defining `main`: 113 - 114 - ```lean {4} 115 - def name := "Alice" 116 - def age := 42 117 - def birthYear := 2025 - age 118 - def main := IO.println birthYear 119 - ``` 120 - 121 - I intentionally did not say "a `main` function" because `main` is not a function. (You can hover over it to learn the type of the `main` but we won't focus on that today.) 122 - 123 - Let's run our program: 124 - 125 - ```sh 126 - lean --run Scratch.lean 127 - ``` 128 - 129 - Now `1983` appears in the terminal. Alternatively, you could also do this: 130 - 131 - ```sh 132 - lean Scratch.lean -c Scratch.c 133 - ``` 134 - 135 - The C code generated by Lean compiler will include an instruction to print `1983`: 136 - 137 - ```c 138 - LEAN_EXPORT lean_object* _lean_main(lean_object* x_1) { 139 - lean_object* x_2; lean_object* x_3; 140 - x_2 = lean_unsigned_to_nat(1983u); 141 - x_3 = l_IO_println___at___main_spec__0(x_2, x_1); 142 - return x_3; 143 - } 144 - ``` 145 - 146 - Now you see that Lean can be used to write programs. 147 - 148 - --- 149 - 150 - ### Writing Proofs 151 - 152 - Now let's *prove* that `age + birthYear` together add up to `2025`. 153 - 154 - Define a little `theorem` alongside with your program: 155 - 156 - ```lean {5-6} 157 - def name := "Alice" 158 - def age := 42 159 - def birthYear := 2025 - age 160 - 161 - theorem my_theorem : age + birthYear = 2025 := by 162 - sorry 163 - ``` 164 - 165 - A `theorem` is [like](https://proofassistants.stackexchange.com/a/1576) a `def`, but aimed specifically at declaring proofs. 166 - 167 - The declared type of a theorem is a statement that it's supposed to prove. Your job is now to construct a proof of that type, and the Lean "tactic mode" (activated with `by`) provides you with an interactive and concise way to construct such proofs. 168 - 169 - Initially, the InfoView tells you that your goal is `age + birthYear = 2025`: 170 - 171 - ![Goal: age + birthYear = 2025](./7.png) 172 - 173 - However, that's not something you can be sure in directly. What's `age`? Try to `unfold age` to replace `age` in the goal with whatever its definition says: 174 - 175 - ![Goal: 42 + birthYear = 2025](./8.png) 176 - 177 - Note how this made the goal in your InfoView change to `42 + birthYear = 2025`. Okay, but what's a `birthYear`? Let's `unfold birthYear` as well: 178 - 179 - ![Goal: 42 + (2025 - age) = 2025](./9.png) 180 - 181 - You're getting closer; the goal is now `42 + (2025 - age) = 2025`. Unfolding `birthYear` brought back `age`, what's `age` again? Let's `unfold age`: 182 - 183 - ![Goal: 42 + (2025 - 42) = 2025](./10.png) 184 - 185 - At this point the goal is `42 + (2025 - 42) = 2025`, which is a simple arithmetic expression. The built-in `decide` tactic can solve those with gusto: 186 - 187 - ![No goals](./11.png) 188 - 189 - And you're done! You've now proven that `age + birthYear = 2025` without actually having *run* any code. This is being verified *during typechecking.* 190 - 191 - You can verify that editing `age` to another number will not invalidate your proof. However, if you edit `birthYear` to `2023 - age`, the proof no longer typechecks: 192 - 193 - ![tactic 'decide' proved that 42 + (2023 - 42) is false](./12.png) 194 - 195 - Of course, this was all a bit verbose. Instead of doing `unfold` for each definition manually, you can tell the `simp` simplifier to do them recursively for you: 196 - 197 - ![simp [age, birthView] solves the same theorem](./13.png) 198 - 199 - This also proves the goal. 200 - 201 - This example is contrived but I wanted to show you how it feels to step through the code step by step and transform it "outside in". It almost makes you feel like *you're* the computer, logically transforming the code towards the goal. It won't always be so tedious, especially if you have some useful theorems prepared. 202 - 203 - We'll come back to proving things, but for now let's learn some more Lean basics. 204 - 205 - --- 206 - 207 - ### Opening Namespaces 208 - 209 - Have another look at this `main` definition: 210 - 211 - ```lean {4} 212 - def name := "Alice" 213 - def age := 42 214 - def birthYear := 2025 - age 215 - def main := IO.println birthYear 216 - ``` 217 - 218 - Here, `IO.println birthYear` is a function call. `IO` is a namespace, and `println` is a function defined in that namespace. You pass `birthYear` to it. 219 - 220 - You could avoid the need to write `IO.` before it by *opening* the `IO` namespace: 221 - 222 - ```lean {1,6} 223 - open IO 224 - 225 - def name := "Alice" 226 - def age := 42 227 - def birthYear := 2025 - age 228 - def main := println birthYear 229 - ``` 230 - 231 - This doesn't have to be done at the top of the file: 232 - 233 - ```lean {5} 234 - def name := "Alice" 235 - def age := 42 236 - def birthYear := 2025 - age 237 - 238 - open IO 239 - def main := println birthYear 240 - ``` 241 - 242 - It can be a bit confusing that now you have to write `IO.println` anywhere above that `open IO` but you can write `println` directly anywhere below it. As an alternative, you can scope opening `IO` to a specific definition by adding `in`: 243 - 244 - ```lean {5} 245 - def name := "Alice" 246 - def age := 42 247 - def birthYear := 2025 - age 248 - 249 - open IO in 250 - def main := println birthYear 251 - ``` 252 - 253 - This would make the shorter syntax available inside `main` but not outside it. 254 - 255 - We're not using `IO` much so I'll keep referring to `println` as `IO.println` below. 256 - 257 - --- 258 - 259 - ### Passing Arguments 260 - 261 - Now notice how you're passing `birthYear` to the `println` function: 262 - 263 - ```lean {4} 264 - def name := "Alice" 265 - def age := 42 266 - def birthYear := 2025 - age 267 - def main := IO.println birthYear 268 - ``` 269 - 270 - Unlike languages like JavaScript, Lean doesn't use parentheses or commas for function calls. Instead of `f(a, b, c)`, you would write `f a b c` in Lean. 271 - 272 - I need to emphasize this. No commas and no parens are used for function calls! 273 - 274 - However, you would sometimes use parentheses *around* individual arguments. Suppose you replace `birthYear` with `2025 - age` directly in the function call: 275 - 276 - ```lean {3} 277 - def name := "Alice" 278 - def age := 42 279 - def main := IO.println 2025 - age 280 - ``` 281 - 282 - This will lead to an error: 283 - 284 - ![Failed to synthesize HSub (IO Unit) Nat ?m.342](./3.png) 285 - 286 - Lean thinks you're trying to do `(IO.println 2025) - age`, so it looks for a way to subtract `age` (which is a `Nat`) from whatever `IO.println 2025` returns (which happens to be something called `IO Unit`). Lean can't find a subtraction operation (`HSub`) between an `IO Unit` and a `Nat`, so it gives up in frustration. 287 - 288 - To fix this, add parentheses around `2025 - age`: 289 - 290 - ```lean {3} 291 - def name := "Alice" 292 - def age := 42 293 - def main := IO.println (2025 - age) 294 - ``` 295 - 296 - Now instead of `IO.println` "eating" the `2025` argument, it knows that the first argument is the entire `(2025 - age)` expression. You might find it helpful to use `(` and `)` liberally and try removing them to get a feel for when they're necessary. 297 - 298 - Note `(` and `)` here have nothing to do with the `IO.println` function call. Their only purpose is to "group" `2025 - age` together, like you group things in math. 299 - 300 - **In other words, instead of writing `something(f(x, y), a, g(z))` as you might in JavaSript, you would write `something (f x y) a (g z)` in Lean.** 301 - 302 - Read this closely several times and make sure it burns deep in your subconscious. 303 - 304 - --- 305 - 306 - ### Nesting Expressions 307 - 308 - You can't nest Lean definitions. However, if some of your definitions get complex, you can simplify them by declaring some `let` bindings inside of the `def`. 309 - 310 - For example, you could pull `(2025 - age)` from the last example into a `let`: 311 - 312 - ```lean {5-6} 313 - def name := "Alice" 314 - def age := 42 315 - 316 - def main := 317 - let birthYear := 2025 - age 318 - IO.println birthYear 319 - ``` 320 - 321 - Again, note the use of `:=` for assignment. It's `:=`, not `=`! 322 - 323 - You might think that being multiline makes `main` a function, but it doesn't. Adding a `let` binding is just a way to make definitions easier to read. You could use `let` inside of any definition, including the `name` and `age` definitions: 324 - 325 - ```lean {2-3,6-8} 326 - def name := 327 - let namesInPassport := ["Alice", "Babbage", "McDuck"] 328 - namesInPassport[0] 329 - 330 - def age := 331 - let twenty := 20 332 - let one := 1 333 - twenty + twenty + one + one 334 - 335 - def main := 336 - let birthYear := 2025 - age 337 - IO.println birthYear 338 - ``` 339 - 340 - There is no need for a `return` statement here. The last line of a definition becomes its value. This is why the definitions above are equivalent to: 341 - 342 - ```lean 343 - def name := ["Alice", "Babbage", "McDuck"][0] 344 - def age := 20 + 20 + 1 + 1 345 - def main := IO.println (2025 - age) 346 - ``` 347 - 348 - In the end, the right side of any definition unfolds into a single expression, but `let` lets you break down that expression into more readable (and reusable) pieces. 349 - 350 - --- 351 - 352 - ### Declaring Functions 353 - 354 - Currently `birthYear` hardcodes `2025` into the calculation: 355 - 356 - ```lean {3} 357 - def name := "Alice" 358 - def age := 42 359 - def birthYear := 2025 - age 360 - def main := IO.println birthYear 361 - ``` 362 - 363 - You can turn `birthYear` into a *function* definition by declaring a `currentYear` argument immediately after writing `def birthYear` followed by a space: 364 - 365 - ```lean {3} 366 - def name := "Alice" 367 - def age := 42 368 - def birthYear currentYear := currentYear - age 369 - ``` 370 - 371 - This makes `birthYear` a function. You can call it by writing `birthYear 2025`: 372 - 373 - ```lean {4} 374 - def name := "Alice" 375 - def age := 42 376 - def birthYear currentYear := currentYear - age 377 - def main := IO.println (birthYear 2025) 378 - ``` 379 - 380 - Again, the parens around `birthYear 2025` ensure that `IO.println` doesn't try to "eat" the `birthYear` function itself. We want it to "eat" `birthYear 2025`. 381 - 382 - If you hover over `birthYear` now, you'll see that its type is no longer a `Nat`: 383 - 384 - ![](./5.png) 385 - 386 - This is how Lean pretty-prints function types. You see the function name `birthYear`, followed by its arguments (we only have one), `:` and the return type. 387 - 388 - You could write it that way explicitly, too, to clarify your intended types: 389 - 390 - ```lean 391 - def birthYear (currentYear : Nat) : Nat := currentYear - age 392 - ``` 393 - 394 - Note, again, that `(` `)` parentheses in Lean have nothing to do with function calls. You only use the parentheses to treat `(currentYear : Nat)` as a single thing. 395 - 396 - --- 397 - 398 - ### Many Ways to Declare a Function 399 - 400 - You've seen two ways to define the same function, with implicit and explicit types: 401 - 402 - ```lean 403 - /-- Types are implicit here, Lean infers them -/ 404 - def birthYear currentYear := currentYear - age 405 - 406 - /-- Types are explicit here -/ 407 - def birthYear (currentYear : Nat) : Nat := currentYear - age 408 - ``` 409 - 410 - There are even more valid ways to write the same thing with different verbosity. Here is a (non-exhaustive) list of ways to define the same `birthYear` function: 411 - 412 - ```lean 413 - /-- Concise definition -/ 414 - def birthYear currentYear := currentYear - age 415 - def birthYear (currentYear: Nat) := currentYear - age 416 - def birthYear (currentYear: Nat) : Nat := currentYear - age 417 - 418 - /-- Definition set to an anonymous function -/ 419 - def birthYear := fun currentYear => currentYear - age 420 - def birthYear := fun (currentYear: Nat) => currentYear - age 421 - 422 - /-- Definition (with explicit type) set to an anonymous function -/ 423 - def birthYear : Nat → Nat := fun currentYear => currentYear - age 424 - ``` 425 - 426 - This might remind you of `function f() {}` vs `const f = () => ...` in JS. I find the concise syntax most pleasant to read and recommend using it unless you specifically need an anonymous function (e.g. to pass it to another function). 427 - 428 - Here, `Nat → Nat` is the actual type of `birthYear`, no matter which syntax is used. It takes a `Nat` and it returns a `Nat`, so it's `Nat → Nat`. The fancy `→` arrow is typed by writing `\to` followed by a space in the [playground](https://live.lean-lang.org/) or with [Lean VSCode](https://lean-lang.org/install/). 429 - 430 - Specifying argument types but inferring the return is often a nice middle ground: 431 - 432 - ```lean 433 - def birthYear (currentYear: Nat) := currentYear - age 434 - ``` 435 - 436 - Just like with `function` declarations vs arrow functions in JavaScript, the level of verbosity and typing that you want to do in each case is mostly up to you. 437 - 438 - (Sidenote: You might also see syntax like `fun x ↦ x * 2` rather than `fun x => x * 2`. Here, `↦` is typed as `\maps`, and mathematicians prefer it aesthetically to `=>`. Lean doesn't distinguish them so you'll see `=>` in codebases like Lean itself while `↦` shows up in "mathy" codebases like Mathlib. They both work with `fun`.) 439 - 440 - --- 441 - 442 - ### Adding Arguments 443 - 444 - Now that you know a dozen ways to write functions, let's get back to this one: 445 - 446 - ```lean {3} 447 - def name := "Alice" 448 - def age := 42 449 - def birthYear currentYear := currentYear - age 450 - def main := IO.println (birthYear 2025) 451 - ``` 452 - 453 - Suppose you want to make `age` an argument to the `birthYear` function. To add an argument, just write it next in the `def birthYear` definition argument list: 454 - 455 - ```lean {2-3} 456 - def name := "Alice" 457 - def birthYear currentYear age := currentYear - age 458 - def main := IO.println (birthYear 2025 42) 459 - ``` 460 - 461 - Actually, this doesn't work, and the error is sucky. 462 - 463 - ![typeclass instance is stuck](./14.png) 464 - 465 - The problem is the issue I described earlier--at this point, Lean has no idea what the types of `currentYear` and `age` might be. Previously, it relied on `age` being an earlier declaration (and inferred it to be a `Nat`) but now Lean is truly stumped. 466 - 467 - The strange error message ("typeclass instance is stuck") alludes to the fact that it's trying to find an implementation of subtraction between two types (searching for that implementation is done via "typeclass search") but it doesn't even know what those types are (that's why you see weird `?m.24` and `?m.12` placeholders). 468 - 469 - The fix to this is to bite the bullet and to actually specify the types: 470 - 471 - ```lean {3} 472 - def name := "Alice" 473 - 474 - def birthYear (currentYear : Nat) (age : Nat) := 475 - currentYear - age 476 - 477 - def main := IO.println (birthYear 2025 42) 478 - ``` 479 - 480 - Now there is no ambiguity. Note that the parentheses here don't mean anything special--it's only a way to separate parameters from each other. Just like when calling functions, we're not using commas so we need parens for grouping. 481 - 482 - When you have multiple parameters of the same type in a row, like `currentYear` and `age` above, you may put them together together under one type declaration: 483 - 484 - ```lean {3} 485 - def name := "Alice" 486 - 487 - def birthYear (currentYear age : Nat) := 488 - currentYear - age 489 - 490 - def main := IO.println (birthYear 2025 42) 491 - ``` 492 - 493 - This doesn't change semantics but is shorter to write. This is particularly useful in mathematics where you might have 3 or 4 parameters that are all `Nat` or such. 494 - 495 - Now that you're specifying all parameter types explicitly, it makes sense to think about them harder and to give them slightly different types. While `age` should remain a `Nat`, `currentYear` makes more sense as an `Int` so that the calculation still works for people born in the BC era or who are over two thousand years old. 496 - 497 - ```lean {3} 498 - def name := "Alice" 499 - 500 - def birthYear (currentYear : Int) (age : Nat) := 501 - currentYear - age 502 - 503 - def main := IO.println (birthYear 2025 42) 504 - ``` 505 - 506 - The non-pretty-formatted type of `birthYear` is `Int → Nat → Int` because it takes an `Int` and also a `Nat`, and returns an `Int`. As in some other functional languages, you can partially apply it--for example, `birthYear 2025` will give you a `Nat → Int` function that you can later call with the remaining `age` argument. 507 - 508 - By now, you might have guessed that there are other increasingly deranged equivalent ways to define the same function that takes an `Int` and a `Nat`: 509 - 510 - ```lean 511 - def birthYear (currentYear : Int) (age : Nat) := currentYear - age 512 - def birthYear (currentYear : Int) := fun (age : Nat) => currentYear - age 513 - def birthYear := fun (currentYear : Int) (age : Nat) => currentYear - age 514 - def birthYear := fun (currentYear : Int) => fun (age : Nat) => currentYear - age 515 - def birthYear: Int → Nat → Int := fun currentYear age => currentYear - age 516 - def birthYear: Int → Nat → Int := fun currentYear => fun age => currentYear - age 517 - ``` 518 - 519 - **Read them closely keeping in mind that they are all exactly equivalent in Lean.** It's confusing to mix syntax within a single definition so we'll stick with this: 520 - 521 - ```lean 522 - def birthYear (currentYear : Int) (age : Nat) := 523 - currentYear - age 524 - ``` 525 - 526 - It's time to revisit proofs. 527 - 528 - --- 529 - 530 - ### Proving For All 531 - 532 - A few sections earlier, you've proven `birthYear + age = 2025` for this code: 533 - 534 - ```lean 535 - def name := "Alice" 536 - def age := 42 537 - def birthYear := 2025 - age 538 - 539 - theorem my_theorem : age + birthYear = 2025 := by 540 - simp [age, birthYear] 541 - ``` 542 - 543 - However, now `birthYear` is a function which takes two arguments: 544 - 545 - ```lean {3} 546 - def name := "Alice" 547 - 548 - def birthYear (currentYear : Int) (age : Nat) := 549 - currentYear - age 550 - ``` 551 - 552 - You could copypaste this theorem for concrete values of `age` and `currentYear`: 553 - 554 - ```lean {6-13} 555 - def name := "Alice" 556 - 557 - def birthYear (currentYear : Int) (age : Nat) := 558 - currentYear - age 559 - 560 - theorem my_theorem : 42 + birthYear 2025 42 = 2025 := by 561 - simp [birthYear] 562 - 563 - theorem my_theorem' : 25 + birthYear 2025 25 = 2025 := by 564 - simp [birthYear] 565 - 566 - theorem my_theorem'' : 77 + birthYear 1980 77 = 1980 := by 567 - simp [birthYear] 568 - ``` 569 - 570 - These proofs typecheck, but there's a feeling that it's not any better than writing tests. What we're hoping to capture is a *universal pattern*, not a bunch of cases. 571 - 572 - The fact is, no matter what `cy` (I'll shorten "current year" to that) and `a` (short for "age") are, `a + birthYear cy a` should be equal to `cy`. Let's capture that: 573 - 574 - ```lean {6-7} 575 - def name := "Alice" 576 - 577 - def birthYear (currentYear : Int) (age : Nat) := 578 - currentYear - age 579 - 580 - theorem my_theorem : a + birthYear cy a = cy := by 581 - sorry 582 - ``` 583 - 584 - Although you haven't declared `a` and `cy`, this is actually valid syntax! The VS Code Lean extension will implicitly insert `{a cy}` arguments after `my_theorem`: 585 - 586 - ![Automatically inserted implicit arguments: a : Nat, cy : Int](./15.png) 587 - 588 - These "implicit" arguments surrounded by `{` `}` curly braces can sometimes be very useful to avoid boilerplate, but in this case they're more confusing than helpful. Let's declare `cy` and `a` explicitly as normal arguments to `my_theorem`: 589 - 590 - ```lean {6} 591 - def name := "Alice" 592 - 593 - def birthYear (currentYear : Int) (age : Nat) := 594 - currentYear - age 595 - 596 - theorem my_theorem (cy : Int) (a : Nat) : a + birthYear cy a = cy := by 597 - sorry 598 - ``` 599 - 600 - Note how declaring `my_theorem` with `cy` and `a` is similar to declaring function arguments--except that you can't actually "run" this theorem. You're just stating that, for any `cy` and `a`, you can produce a proof of `a + birthYear cy a = cy`. 601 - 602 - Let's see if you actually can! 603 - 604 - You start out with the goal of `↑a + birthYear cy a = cy`: 605 - 606 - ![Goal: ↑a + birthYear cy a = cy](./16.png) 607 - 608 - The `⊢` symbol before it tells you that this is the goal, i.e. what you want to prove. Things above it, like `cy : Int` and `a : Nat`, are the things you *already have.* 609 - 610 - You might be wondering: what *are* those `cy : Int` and `a : Nat`. What *are* their values? But the question doesn't make sense. You're writing a proof, so in a sense, you are working with *all possible values at the same time.* You only have their types. 611 - 612 - Have another look at the goal: `↑a + birthYear cy a = cy`. 613 - 614 - What's that up arrow? 615 - 616 - Hover over it in the InfoView: 617 - 618 - ![@Nat.cast Int instNatCastInt a : Int](./17.png) 619 - 620 - The signature is a bit confusing (what's that `@`? what's that `instNatCastInt`?) but you can see that it takes your `a` (which is a `Nat`) and returns an `Int`. So this is how Lean displays the fact that your `a` is being converted to an `Int` so that it can be added with the result of `birthYear cy a` call (which is already an `Int`). 621 - 622 - Okay, cool, the goal is to prove `↑a + birthYear cy a = cy`. How do we do that? 623 - 624 - Well, first, let's `unfold birthYear` to replace it with the implementation: 625 - 626 - ![Goal: ↑a + (cy - ↑a) = cy](./18.png) 627 - 628 - Now the goal becomes `↑a + (cy - ↑a) = cy`. 629 - 630 - You can't use the `decide` tactic from earlier because it only deals with known concrete numbers--it's like a calculator. However, Lean also includes an `omega` tactic that can check equations that include unknown numbers like `a` and `cy`. 631 - 632 - With `omega`, you can complete this proof! 633 - 634 - ![No goals](./19.png) 635 - 636 - Let's have another look at the full code: 637 - 638 - ```lean 639 - def name := "Alice" 640 - 641 - def birthYear (currentYear : Int) (age : Nat) := 642 - currentYear - age 643 - 644 - theorem my_theorem (cy: Int) (a : Nat) : a + birthYear cy a = cy := by 645 - unfold birthYear 646 - omega 647 - ``` 648 - 649 - You have a function called `birthYear` and a theorem called `my_theorem` about its behavior. By convention, you might want to call it `birthYear_spec`, or something more specific like `age_add_birthYear_eq_current_year`. 650 - 651 - In a way it's like a test, but it's a test for *all possible inputs of that function* that runs during typechecking. I think this is incredibly cool. You can verify that incorrectly changing the formula of `birthYear` will cause the proof to no longer typecheck: 652 - 653 - ![omega could not prove the goal](./20.png) 654 - 655 - --- 656 - 657 - ### Universal Quantifier 658 - 659 - You've previously declared `cy` and `a` as arguments for `my_theorem`: 660 - 661 - ```lean 662 - theorem my_theorem (cy: Int) (a : Nat) : a + birthYear cy a = cy := by 663 - unfold birthYear 664 - omega 665 - ``` 666 - 667 - In this case, it's fine to omit their types because they can be exactly inferred by Lean from the `birthYear` call itself, which you already explicitly annotated: 668 - 669 - ```lean {1} 670 - theorem my_theorem cy a : a + birthYear cy a = cy := by 671 - unfold birthYear 672 - omega 673 - ``` 674 - 675 - This theorem says "for all `cy` and `a`, `a + birthYear cy a` is equal to `cy`”. Mathematicians have their own way of writing statements like this, using the *universal quantifier* ∀ (which stands for "for all"): *∀ cy a, a + birthYear(cy, a) = cy*. 676 - 677 - You can restate the above in a mathematician's style with the `∀` quantifier: 678 - 679 - ```lean {1} 680 - theorem my_theorem : ∀ cy a, a + birthYear cy a = cy := by 681 - sorry 682 - ``` 683 - 684 - To type `∀`, write `\all` and press space. 685 - 686 - Now instead of `cy a :`, we have `: ∀ cy a`. The universal quantifier lets the arguments get introduced "later"--pretty much like currying in programming. However, the difference is mostly stylistic. The theorem signature is the same. 687 - 688 - When you use `∀`, you're starting out with a universal statement ("for all `cy` and `a`") so `cy` and `a` aren't in your tactic state yet. Use `intro cy a` to bring them in: 689 - 690 - ![intro cy a brings cy : Int and a : Nat into tactic state](./24.png) 691 - 692 - Again, you don't have their *concrete* values because a proof is supposed to work for all possible values. You don't know anything about them except their types. 693 - 694 - From that point, your goal is `↑a + birthYear cy a = cy`, which you can solve: 695 - 696 - ```lean {3-4} 697 - theorem my_theorem : ∀ cy a, a + birthYear cy a = cy := by 698 - intro cy a 699 - unfold birthYear 700 - omega 701 - ``` 702 - 703 - Notice how `∀ cy a` has essentially moved the declarations of `cy` and `a` "inside" the proof itself. Under the hood, the `intro` tactic generates a nested function with `cy` and `a` arguments, so in the end this `theorem` has the same shape as before adding `∀`. It may feel disorienting that Lean lets you write the same thing in many styles, but this lets the Lean core stay small and evolve faster, while tactics and conventions may evolve independently and vary between projects. 704 - 705 - Using `∀` may not seem particularly appealing in this example, but it is great for nested propositions where anonymous functions would add too much clutter. It can also be convenient to extract a theorem's claim into its own definition: 706 - 707 - ```lean {1,3,4} 708 - def TheLawOfAging : Prop := ∀ cy a, a + birthYear cy a = cy 709 - 710 - theorem my_theorem : TheLawOfAging := by 711 - unfold TheLawOfAging 712 - intro cy a 713 - unfold birthYear 714 - omega 715 - ``` 716 - 717 - To learn what a `Prop` is, check out [my previous article](/beyond-booleans/). 718 - 719 - --- 720 - 721 - ### Implicit Arguments 722 - 723 - Now let us come back to this example, and specifically to this line: 724 - 725 - ```lean {8} 726 - def name := "Alice" 727 - 728 - def birthYear (currentYear : Int) (age : Nat) := 729 - currentYear - age 730 - 731 - def main := 732 - let year := birthYear 2025 42 733 - IO.println year 734 - ``` 735 - 736 - Hover over `IO.println`, and you might freak out: 737 - 738 - ![IO.println.\{u_1\} \{a : Type u_1\} [ToString α] (s: α) : IO Unit](./25.png) 739 - 740 - What the hell is this signature?! Learning to read these signatures will immensely improve your ability to understand Lean APIs and debug errors so I'm going to break it down even though we haven't discussed most of the relevant topics. 741 - 742 - The key insight is that the parts in `{` `}` and `[` `]` are not something you pass to the function directly. That's why you're able to just write `IO.println year`. However, these *are* actual arguments--they're just filled in by Lean itself. 743 - 744 - Command+Click into `println` to see that it declares `{α}` and `[ToString α]`: 745 - 746 - ```lean 747 - def println {α} [ToString α] (s : α) : IO Unit := 748 - print ((toString s).push '\n') 749 - ``` 750 - 751 - So if you Lean fills them in for you, where *are* they coming from? 752 - 753 - The parts in `{` `}` are called *ordinary implicit parameters*, and they are filled in by type inference based on the parameters that have already been passed in. If Lean can't unambiguously fill them in, it will complain and would not typecheck. Here, the `α` argument is implicit, but it's determined by what *you* passed as an explicit `(s : α)` argument. You're printing an `Int`, you passed `(s: Int)`, so `α` is `Int`. This is similar to generics, but in Lean generics are just arguments (often implicit). 754 - 755 - 756 - The parts in `[` `]`, like `[ToString α]`, are called *instance implicit parameters* and are filled in automatically depending on the code you've imported so far. Lean has a mechanism that lets libraries declare implementations of certain intefaces--for example, "how to subtract `Int` and `Int`" or "how to convert `Nat` to `Int`" or "how to turn an `Int` to a `String`”. The `println` function wants a `ToString` implementation that knows how to turn whatever *you* passed (an `Int`) to a string. Lean has a `ToString Int` implementation in core, so that's what `println` gets. 757 - 758 - Finally, the very first `.{}` thing is a "universe" in which this function's type lives. This is rarely something you need to deal with, and is also filled in automatically. 759 - 760 - If you're curious what Lean is filling in (or need to debug some gnarly case that wouldn't typecheck), you can prefix your function call with `@` to force all arguments to be passed explicitly. Start by passing `_` placeholders: 761 - 762 - ```lean {8} 763 - def name := "Alice" 764 - 765 - def birthYear (currentYear : Int) (age : Nat) := 766 - currentYear - age 767 - 768 - def main := 769 - let year := birthYear 2025 42 770 - @IO.println _ _ year 771 - ``` 772 - 773 - Now you can hover over each `_` placeholder to see what Lean has inferred for it. 774 - 775 - For example, the `{α}` implicit parameter was inferred to be `Int`: 776 - 777 - ![Int](./26.png) 778 - 779 - And the `[ToString]` instance implicit parameter received `instToStringInt`: 780 - 781 - ![instToStringInt](./27.png) 782 - 783 - If you want to further debug what's going on, you can fill them in in the code: 784 - 785 - ```lean {8} 786 - def name := "Alice" 787 - 788 - def birthYear (currentYear : Int) (age : Nat) := 789 - currentYear - age 790 - 791 - def main := 792 - let year := birthYear 2025 42 793 - @IO.println Int instToStringInt year 794 - ``` 795 - 796 - That's what you would have to type if Lean couldn't fill them in automatically. 797 - 798 - Now it's easy to Command+Click them and see if they match what you expected. For example, Command+Clicking `instToStringInt` from here will teleport you to the place in the Lean source that actually implements `ToString Int`: 799 - 800 - ```lean 801 - instance : ToString Int where 802 - toString 803 - | Int.ofNat m => toString m 804 - | Int.negSucc m => "-" ++ toString (succ m) 805 - ``` 806 - 807 - Seems legit! When you're done spelunking, you can remove the `@` and the `_`s: 808 - 809 - ```lean {8} 810 - def name := "Alice" 811 - 812 - def birthYear (currentYear : Int) (age : Nat) := 813 - currentYear - age 814 - 815 - def main := 816 - let year := birthYear 2025 42 817 - IO.println year 818 - ``` 819 - 820 - Implicit and instance arguments save a tremenduous amount of boilerplate when operating on complex structures like mathematical objects and proofs. But even here, they let Lean provide a simple, extensible, and strongly-typed `println` API. 821 - 822 - --- 823 - 824 - ### Command+Click Anything 825 - 826 - There's a *lot* more I would've liked to cover but I'm running out of steam--and this article is already long. The good news is, there's a lot you can learn by peeking under the hood on your own because Lean is extremely Command+Click-able. 827 - 828 - You can Command+Click into data types like [`Nat`](https://github.com/leanprover/lean4/blob/ad1a017949674a947f0d6794cbf7130d642c6530/src/Init/Prelude.lean#L1181-L1202) and [`String`](https://github.com/leanprover/lean4/blob/ad1a017949674a947f0d6794cbf7130d642c6530/src/Init/Prelude.lean#L2738-L2752), and even pieces of syntax like `∀`. Much of Lean is implemented in Lean itself, and most of the time what I found ended up both simpler and more interesting than what I expected. 829 - 830 - ```lean 831 - /-- 832 - The natural numbers, starting at zero. 833 - This type is special-cased by both the kernel and the compiler, and overridden with an efficient 834 - implementation. Both use a fast arbitrary-precision arithmetic library (usually 835 - [GMP](https://gmplib.org/)); at runtime, `Nat` values that are sufficiently small are unboxed. 836 - -/ 837 - inductive Nat where 838 - /-- Zero, the smallest natural number. -/ 839 - | zero : Nat 840 - /-- The successor of a natural number `n`. -/ 841 - | succ (n : Nat) : Nat 842 - ``` 843 - 844 - Who would've thought that numbers are just a recursively generated enum? 845 - 846 - Turns out, a language built with recursively defined types like this can express and prove facts like that the [area of a circle is π * r ^ 2](https://github.com/leanprover-community/mathlib4/blob/aac79004bad67a82c18ab7d39c609529f4159d6f/Archive/Wiedijk100Theorems/AreaOfACircle.lean#L81-L130), or that [it takes 23 people to exceed a 50% chance of same birthday](https://github.com/leanprover-community/mathlib4/blob/aac79004bad67a82c18ab7d39c609529f4159d6f/Archive/Wiedijk100Theorems/BirthdayProblem.lean#L65-L81), or that [you can't cube a cube](https://github.com/leanprover-community/mathlib4/blob/aac79004bad67a82c18ab7d39c609529f4159d6f/Archive/Wiedijk100Theorems/CubingACube.lean#L533-L546). But you can also write normal programs with it that compile to C. [Async/await](https://lean-lang.org/fro/roadmap/1900-1-1-the-lean-fro-year-3-roadmap/) is in the works. 847 - 848 - Maybe you could create something in the middle--programs interleaved with proofs? You can see this pattern in the Lean core itself: here's a [`List.append` (`++`) function](https://github.com/leanprover/lean4/blob/c83237baf78d6930d40e556a5cacdca149c4b198/src/Init/Data/List/Basic.lean#L614-L616), and here are proofs that [`(as ++ bs).length = as.length + bs.length`](https://github.com/leanprover/lean4/blob/c83237baf78d6930d40e556a5cacdca149c4b198/src/Init/Data/List/Basic.lean#L661-L664) and [`(as ++ bs) ++ cs = as ++ (bs ++ cs)`](https://github.com/leanprover/lean4/blob/c83237baf78d6930d40e556a5cacdca149c4b198/src/Init/Data/List/Basic.lean#L666-L669) written alongside it. In other words, using the Lean data structures in code means also lets you reuse known facts about those structures to prove claims about your own code. 849 - 850 - --- 851 - 852 - ### Programming With Proofs 853 - 854 - Lean is built on old ideas and a lot of prior art, but there are both novel and pragmatic twists. Sometimes I can't tell if I'm writing Go or Haskell, Rocq or C#, OCaml or Python. In its giddily unrestrained ambition it reminds me of [Nemerle](https://en.wikipedia.org/wiki/Nemerle), but it also feels solid and grounded. It's being used in both industry and academia, including by [DeepMind](https://deepmind.google/discover/blog/ai-solves-imo-problems-at-silver-medal-level/) and [Amazon](https://aws.amazon.com/blogs/opensource/lean-into-verified-software-development/). It's been a while since I felt excited about a programming language (mind you--a *pure* functional programming language). 855 - 856 - Anyway, here's a little program that counts up and down alongside some proofs. 857 - 858 - ```lean 859 - def append (xs ys : List a) : List a := Id.run do 860 - let mut out := ys 861 - for x in xs.reverse do 862 - out := x :: out 863 - return out 864 - 865 - theorem append_abcd : append ["a", "b"] ["c", "d"] = ["a", "b", "c", "d"] := by 866 - simp [append] 867 - 868 - theorem append_length (xs ys : List a) : (append xs ys).length = xs.length + ys.length := by 869 - simp [append] 870 - 871 - 872 - def count_up_and_down n := 873 - let up := List.range (n) 874 - let down := (List.range (n + 1)).reverse 875 - append up down 876 - 877 - theorem count_up_and_down_length n : (count_up_and_down n).length = n * 2 + 1 := by 878 - simp only [count_up_and_down, append_length, List.length_range, List.length_reverse] 879 - omega 880 - 881 - 882 - def main := do 883 - IO.println "Enter a number: " 884 - let stdin ← IO.getStdin 885 - let input ← stdin.getLine 886 - let n := input.trim.toNat! 887 - let sequence := count_up_and_down n 888 - IO.println sequence 889 - ``` 890 - 891 - I've never written code alongside proofs like this before. Have you?
+9 -9
public/open-social/index.md
··· 107 107 108 108 ![Alice can't leave Facebook without erasing herself and her content out of existence.](./10-full.svg) 109 109 110 - The web Alice created--who she follows, what she likes, what she has posted--is trapped in a box that's owned by somebody else. To leave it is to leave it *behind*. 110 + The web Alice created--who she follows, what she likes, what she has posted--is trapped in a box that's owned by somebody else. To leave is to leave it *behind*. 111 111 112 112 On an individual level, it might not be a huge deal. 113 113 ··· 157 157 158 158 ![Alice and Bob have pieces of data.](./12-full.svg) 159 159 160 - **These aren't rows in somebody's database. This is a web of hyperlinked JSON.** Just like every HTML page has an `https://` URI so other pages can link to it, every JSON record has an `at://` URI, so any other JSON record can link to it. (On this and other illustrations, `@alice.com` is a shorthand for `at://alice.com`.) The `at://` protocol is a bunch of conventions on top of DNS, HTTP, and JSON. 160 + **These aren't rows in somebody's database. This is a web of hyperlinked JSON.** Just like every HTML page has an `https://` URI so other pages can link to it, every JSON record has an [`at://` URI](/where-its-at/), so any other JSON record can link to it. (On this and other illustrations, `@alice.com` is a shorthand for `at://alice.com`.) The `at://` protocol is [a bunch of conventions](https://www.ietf.org/archive/id/draft-newbold-at-architecture-00.html) on top of DNS, HTTP, and JSON. 161 161 162 162 Now have a look at the arrows between their records. Alice follows Bob, so she has a `follow` record linking to Bob's `profile` record. Bob commented on Alice's post, so he has a `comment` record that links to Alice's `post` record. Alice liked his comment, so she has a `like` record with a link to his `comment` record. Everything Alice creates stays in her repo under her control, everything Bob creates stays in his repo under his control, and links express the connections--just like in HTML. 163 163 ··· 171 171 172 172 ![Alice seamlessly moves her repository data to a different host.](./14-full.svg) 173 173 174 - *(This requires a modicum of technical skill today but it's getting [more accessible](https://pdsmoover.com/info.html).)* 174 + *(This requires a modicum of technical skill today but it's getting [more accessible](https://pdsmoover.com/info).)* 175 175 176 176 Just like with moving a personal site, changing where her repo is being served from doesn't require cooperation from the previous host. It also doesn't disrupt her ability to log into apps and doesn't break any links. The web repairs itself: 177 177 ··· 195 195 196 196 ![The Tangled app calls createRecord in a repo. A star appears.](./16-full.svg) 197 197 198 - When you create a publication on [Leaflet](https://leaflet.pub), Leaflet puts it in *your* repo: 198 + When you create a publication on [Leaflet](https://leaflet.pub), Leaflet puts it into *your* repo: 199 199 200 200 ![The Leaflet app calls createRecord in a repo. A publication appears.](./17-full.svg) 201 201 202 202 You get the idea. 203 203 204 - Over time, your repo grows to be a collection of data from different open social apps. This data is open by default--if you wanted to look at my Bluesky posts, or Tangled stars, or Leaflet publications, you wouldn't need to hit these applications' APIs. You could just [hit my personal repository and enumerate all of its records](https://atproto-browser.vercel.app/at/danabra.mov). 204 + Over time, your repo grows to be a collection of data from different open social apps. This data is open by default--if you wanted to look at my Bluesky posts, or Tangled stars, or Leaflet publications, you wouldn't need to hit these applications' APIs. You could just [hit my personal repository and enumerate all of its records](https://pdsls.dev/at://danabra.mov). 205 205 206 206 To avoid naming collisions, the data in the repository is grouped by the format: 207 207 208 208 ![Alice's repo contents, separated by record type.](./18-full.svg) 209 209 210 - In any user's repo, Bluesky posts go with other Bluesky posts, Leaflet publications go with Leaflet publications, Tangled stars go with Tangled stars, and so on. Each data format is controlled and evolved by developers of the relevant application. 210 + In any user's repo, Bluesky posts go with other Bluesky posts, Leaflet publications go with Leaflet publications, Tangled stars go with Tangled stars, and so on. Each data format is [controlled and evolved](https://www.pfrazee.com/blog/why-not-rdf#lexicon) by developers of the relevant application. 211 211 212 212 I've drawn a dotted line to separate them but perhaps this is misleading. 213 213 ··· 231 231 232 232 This is why I like "open social" as a term. 233 233 234 - **Open social frees up our data like open source freed up our code.** Open social ensures that products can get a new life, that people can't be locked out of what they have created, and that *products can be forked and remixed*. You don't need an "everything app" when data from different apps circulates in the open web. 234 + **Open social frees up our data like open source freed up our code.** Open social ensures that old data can get a new life, that people can't be locked out of the web they've created, and that *products can be forked and remixed*. You don't need an "everything app" when data from different apps circulates in the open web. 235 235 236 236 If you're technical, by now you might have a burning question. 237 237 238 238 How the hell does aggregation work?! 239 239 240 - Since every user's records live in *that user's* repository, there are millions (potentially billions?) repositories. How can an app efficiently query, sort, filter, and aggregate information from them? Surely it can't search them on demand. 240 + Since every user's records live in *that user's* repository, there could be millions (potentially billions?) of repositories. How can an app efficiently query, sort, filter, and aggregate information from them? Surely it can't search them on demand. 241 241 242 242 I've previously used a CMS as an analogy--for example, a blogging app could directly write posts to your repository and then read posts from it when someone visits your blog. This "singleplayer" use case would not require aggregation at all. 243 243 ··· 271 271 272 272 An important detail is that commits are cryptographically signed, which means that you don't need to trust a relay or a cache of network data. You can verify that the records haven't been tampered with, and each commit is legitimate. This is why "AT" in "AT Protocol" stands for "authenticated transfer". You're supposed to pronounce it like “@" ("at") though. Don't say "ay-tee" or you'll embarrass me! 273 273 274 - As time goes by, we'll see more infrastructure built around and for open social apps. [Graze](https://www.graze.social/) is letting users build their own algorithmic feeds, and [Slices](https://slices.network/) is an upcoming developer platform that does large-scale repository indexing for you. 274 + As time goes by, we'll see more infrastructure built around and for open social apps. [Graze](https://www.graze.social/) is letting you build algorithmic feeds, [Quickslice](https://quickslice.slices.network/) and [Tap](https://docs.bsky.app/blog/introducing-tap) simplify indexing, [Constellation](https://constellation.microcosm.blue/) and [If This Then AT://](https://app.ifthisthen.at/) provide querying and automation. 275 275 276 276 These are all technical details, though. 277 277
+1 -1
public/react-as-a-ui-runtime/index.md
··· 1083 1083 1084 1084 **Of course, `use` is not actually a syntax.** (It wouldn’t bring much benefit and would create a lot of friction.) 1085 1085 1086 - However, React *does* expect that all calls to Hooks happen only at the top level of a component and unconditionally. These [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) can be enforced with [a linter plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks). There have been heated arguments about this design choice but in practice, I haven’t seen it confusing people. I also wrote about why commonly proposed alternatives [don’t work](https://overreacted.io/why-do-hooks-rely-on-call-order/). 1086 + However, React *does* expect that all calls to Hooks happen only at the top level of a component and unconditionally. These [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) can be enforced with [a linter plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks). There have been heated arguments about this design choice but in practice, I haven’t seen it confusing people. I also wrote about why commonly proposed alternatives [don’t work](/why-do-hooks-rely-on-call-order/). 1087 1087 1088 1088 Internally, Hooks are implemented as [linked lists](https://dev.to/aspittel/thank-u-next-an-introduction-to-linked-lists-4pph). When you call `useState`, we move the pointer to the next item. When we exit the component’s [“call tree” frame](#call-tree), we save the resulting list there until the next render. 1089 1089
+1 -1
public/the-math-is-haunted/index.md
··· 30 30 31 31 To a mathematician's eye, this syntax looks like stating a theorem. We have the `theorem` keyword, the name of our theorem, a colon `:` before its statement, the statement that we'd like to prove, and `:= by` followed by the proof (`sorry` means that we haven't completed the actual proof yet but we're planning to fill it in later). 32 32 33 - But if you're a programmer, you might notice a hint of something else. That `theorem` looks suspiciously like a function. But then what is `2 = 2`? It looks like a return type of that function. But how can `2 = 2` be a *type*? Isn't `2 = 2` just a boolean? And if `2 = 2` really *is* a type, what are the *values* of that `2 = 2` type? [These are very interesting questions](https://overreacted.io/beyond-booleans/), but we'll have to forget about them for now. 33 + But if you're a programmer, you might notice a hint of something else. That `theorem` looks suspiciously like a function. But then what is `2 = 2`? It looks like a return type of that function. But how can `2 = 2` be a *type*? Isn't `2 = 2` just a boolean? And if `2 = 2` really *is* a type, what are the *values* of that `2 = 2` type? [These are very interesting questions](/beyond-booleans/), but we'll have to forget about them for now. 34 34 35 35 Instead, we'll start by inspecting the proof: 36 36
+5 -1
public/the-two-reacts/post-preview.js
··· 9 9 return ( 10 10 <section className="rounded-md bg-black/5 p-2"> 11 11 <h5 className="font-bold"> 12 - <a href={"/" + slug} target="_blank"> 12 + <a 13 + href={"/" + slug} 14 + target="_blank" 15 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 16 + > 13 17 {data.title} 14 18 </a> 15 19 </h5>
+4
public/where-its-at/1-full.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.6822853284357 133.7882486588319" width="1547.3645706568714" height="267.5764973176638"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2dWXNcdTAwMWPHsYXf9StcdTAwMTi8r1Kr9sVvWiiTlrVYtLX4hkNcdTAwMDGSXHUwMDAzXHUwMDEyXCJcYkCYoURKof/u7zRcYlxmplx1MDAxYWT1YKB7XHUwMDE5IdFcdTAwMGVcdTAwMDaFmTlTXZXLyazMxK/v3Lp1e/XyZHH7L7duL1483Ds8eHS69/Ptd/Xzn1x1MDAxNqfLg+MjXnLjfy+Pn58+XHUwMDFj3/lktTpZ/uX999efXHUwMDE4XHUwMDFlXHUwMDFlPzv71OJw8WxxtFryvv/lv2/d+nX8m1dcdTAwMGVcdTAwMWXps3//4Ze8+PreXHUwMDE3j16m7766u//Ti69PTp+PXHUwMDFmXHUwMDFk33S+mNXixWr901x1MDAxN1qEjyZcci66XHUwMDE4kinO1HDx8ktefs+7kFx1MDAwNutszjY4m2q6ePnng0erJ7zFWjO4amtIwVrrXbFcdTAwMTdvebI4ePxkxXt8vPjZ3tHjQ63FXFz8ZLk6PX66+Oj48PhUa/yfksqipvUyXHUwMDFm7D18+vj0+PnRo/V7XHUwMDFluH334MH6PftcdTAwMDeHh/dXL0fk21x1MDAwZk+Pl8v3nuytXHUwMDFlPrndfMs354tufn7x2eUxW7/+XHUwMDE0X/v4ydFiuTw/r/Gnxyd7XHUwMDBmXHUwMDBmVtpcdTAwMWZr1s+hNZ7cezSe0X/WKzvde7a4p0M6en54ePHjg6NHXHUwMDBi7f/tXHUwMDA39/bKxvdcdTAwMWQ9evV9XHUwMDFi718uXHUwMDE2wrAu+lJiiutjWlx1MDAwYlx1MDAxNHvf/vTz46NRuFxctKmU9eFcdTAwMWQsP0akViPm/t7hcrHeba3gzlrcNlbx/OTR3tmHbI7VS2qitWvYw4Ojp+1nXHUwMDBlj1x1MDAxZj694ntGUbwk9ZfO8vhodf/gl3HZZeOnn+w9OzjUtsdccphcdTAwMGZcdTAwMGVcdTAwMGZcdTAwMWXr+W9cdTAwMWYu9i+JN1uwOkCVLl5eXHUwMDFkn6xffVxi3t7B0eJ0ejbHp1x1MDAwN49cdTAwMGaO9lx1MDAwZf/52iXuPV9cdTAwMWR/tVieLXJ1+nxxeVx1MDAwN1x1MDAxNnfPxd6iWONcdTAwMGK/vXuV0v5wz33+4cPPPrhz7293nz4//iB9/o37ZZ7SZlx1MDAwZrZNIfKXXHUwMDBiIU+UNlx1MDAwZdWWWDil5HJZ79daa9NQsq3e1eJ9LWEtUH9q7Syt/Wi+1lx1MDAxNo9d9NZcXFx1MDAxNs1XOlx1MDAxYv3kp1x1MDAxNzprjY/WuEvveHvU9v2T02OOb/E26+1cdTAwMTVrvFx1MDAxOcX96+pvP/z75PjkyVf//Ff9ytz/8f7h93vzXHUwMDE0t+Y4ZJMqupedW+/HWm9LTSZnZ2JGdSd660pcdTAwMThcdTAwMWNcdTAwMWFcdTAwMWSC81x1MDAwNV97XHTiT72dpbd3tvC2MbuEXHUwMDE1rVx1MDAxM7/Ki9n612mudVx1MDAxOXVcdTAwMGa+vp2ae7xcXL3vXHUwMDBmf3npfjhcYkfPlpd9zluow29Y7c1oc95f/qvef7r68Vx1MDAxZi9++vRbf/9cdTAwMWb77935Yp42J7yorTbl4lN1ZeqG02B4OeKLOTmzXHUwMDE2mLVcdTAwMWJcdTAwMGWDz7XyXHUwMDBlk0KIflfy7FxcebD4I6nzX7dQ52CS9DKky3J6Tp5TbH96oc4lh+pS/j1cdTAwMWNxXHUwMDBloe6kzlx1MDAwZpZPX1x1MDAwZXsnJ2+xXHUwMDBlT5d4M4r7cD/9/aOn+58/+fnbk4fPnFvdcfnHeYpbQlx1MDAxZXKwMeKRTa5X+eFcdTAwMWN9yDFmXHUwMDFmk7mCP7s8VONriFx1MDAxNZWLNq5cdTAwMGbxeorLXCLMvvtcdTAwMDMp7lx1MDAxN1sorssmOvTvKsVFpV+nuL5y0N7YtVx1MDAxNtyY3tbgU74stdvq7elz/Vx1MDAxOVx1MDAxZS3eYsW9Yo03o7m/3Lnz48dffnp37+nTcnTw+bPv/Xv2xVRz9085//PM2fjKudvNXHUwMDAzhjybguNdXHUwMDBi6Lnu5sFcdTAwMDabSq7ZppqvyljBsqHXRL5cdTAwMTZcdTAwMTXH/16hu25cYsHYXHUwMDAwTORrTFrD/OmEz05yrcv7W6hyrt5Hn8pVqow9fp0qV47JwpHWj3BTqpycqdlfXHUwMDE247mqfHJ8cDklqz/rf91ay8f4XHUwMDFmXHUwMDE3//7Pu/13v2dcdTAwMDbrS8UnpOg9hCV5ty1AyDXIqVx1MDAxOHinjSXE7Vx1MDAwMNDgamz1iVx1MDAwZtuCmvltP19CXHSuOFtqXGLZli2/31xyfMxEXHUwMDEzXGK/2IZky3af9zjwkFx1MDAxM/7ZWIvUmS3XXHUwMDFmMFx1MDAxMcZcdTAwMTNcdTAwMWTjP9iDsuXXx4Elo1s14bwqtmq7jydWn1xiXCKdsd5cdTAwMWKXtvt0Zu3Oh1x1MDAxOFx1MDAwMlJk3baPXlx1MDAwN4fYmeBjZPUh9D8+6ILAh8S3smy/vkRcdTAwMTjxrFx1MDAxOXwyMOiog/Rsalx1MDAwZlx1MDAxMOFzXGJ+isZcdTAwMWFX2Ya8XHSIcPF1mGY21maXQ1x1MDAxZjC54nN2XHUwMDEyKVx1MDAxM6yrm4BuSFxiKyettzlcdTAwMTSnXHUwMDA36IfIXHUwMDEz+WCI8YoyqGlcdTAwMTMwXHUwMDBlJjin7CxcdTAwMDeYUOBcdTAwMWVgXHUwMDFhSi0oXHUwMDFjqFxirrVuXHUwMDEzL1x1MDAxMyZmg7tDnaJJXWksgzdcdTAwMDVcdTAwMTFMxaB/NjRcdTAwMWJYXHUwMDA2UzjenHPCtXVcdTAwMGaYXHUwMDEzLJmd4Tmsx1xcRt/CVTa2St9rQmS7eHHAXCK5YFx1MDAxMVxuXHUwMDE5uk04h7ygcIhcdTAwMWWWh1xiyXbhXG5cdTAwMWW/8JTVpFx1MDAxOImiY4vHU1x1MDAxYa9cdTAwMWJcdTAwMTBgU1x1MDAxN8/pRiumUHVcdTAwMWOpWJNaPFwiQuhott6ORLBcdTAwMGIoNuGQXHUwMDEz0VxyVpDzroBhXHUwMDE4XHJbxodh5N3kgbfFw+B4Qlx1MDAxZGdYpynF7YpXhoLLKfitklx1MDAxY0HWjnjQMdxcdTAwMTlcdTAwMDYtp5TkXHUwMDE4dz1cdTAwMTGfkLFaK1x1MDAxZlx0ho00oVx1MDAwNbRcdTAwMGXpXHUwMDA0k1x1MDAwMFx1MDAwMtHuul+fXHUwMDA3rK2raDGrg1tsakglWkTcjSlaXHUwMDFm5rmLx1x1MDAwZbJvxKg5WVx1MDAxM4NLLWDwyiZcdTAwMTflJ1x1MDAxM3y3XHUwMDA3XHUwMDE47FAhOui9vKGrjZKgQ5Bl4iVcZkxysa8kwVx1MDAwZkGhs0HpsHV1YmGcXHUwMDEz/2aLYy42d21CQKSN5ZuLM1x1MDAwNmV1XHUwMDEzi2XYOYJy62BsXVx1MDAwM0hcdTAwMDRPkFx1MDAxNlx1MDAxM2Ejxpknblx1MDAxZZf4v1x1MDAxMkFgSrFcbpiiLl7CRWRcZkiCVVx1MDAxMfFP4Fwifi/JykSTu1xch1x1MDAxOCWwzUk3eFx1MDAxNTu8I5qsPbY5OKKa4urOaCWZWFxc1PWgSz7sXHUwMDA2V1x1MDAwN6lsMURmPlx1MDAxMHPthFx1MDAxNkVsMVV4h+p1tLuh2cF4lDYpXHUwMDBihGfLaTc4N6CS4kxcdTAwMTFfnfxEhDGwuok3zksguypcdTAwMGJeRlx1MDAxZKBcdTAwMTiexVk80sTtYrhcbtyY7Vxy8KEuoFx1MDAxZjhcdTAwMDUrN825stLWpsjHXHUwMDEz9L5yLV0jXHUwMDA1XqrKkkFcIrDzJYZcdTAwMTZcdTAwMTB7glx1MDAxZnWAYZV9V2tjXHUwMDE4cOHYb1ZpyqVM7YgnYkDUXmCQaFx1MDAwZkc2XHUwMDAzzrE25MRhd8vEXHUwMDBi1VisxU/J0NZcdTAwMTnnXHUwMDExoJFWylx1MDAwZu3TPm7i2YGzxUJcdTAwMTmPXHUwMDEyXHUwMDEyO8xZXjG6XHUwMDFkwIRj2Gr7uG7AO8WSMWLIwFxmmlx1MDAxYqGdeFx1MDAxOGsrXHUwMDFlXHUwMDBic+VaPLxcdTAwMDWChNfwXHQ/1D+OOGBcdTAwMDNwXHRYXHUwMDE3j+No1odcdTAwMDeQRcZCYOLhpnPWh+rqcIlqa0uaITW4Ymhu5bzwpamvcFx1MDAxOPlEfGtwXHQ4odwoiFhSwplEeFxyKm53xmM7bCGGQDZccqa7dJ1aXHUwMDFmMPGzjMZFyGs/7OjhpcEo58Vp8dC1T1xu+nj4Jtwt8Vx1MDAxZOw+1p1cdTAwMWY4XHUwMDBmOnpcdTAwMThcdTAwMTCURrZrd7ygeNPD4oJcdTAwMWP6rnjQXGZkT2lFLHRI/dB5XHUwMDE0aVx1MDAxMHNcdTAwMTGTjKHEXHUwMDE2MNYx4LRcdTAwMDS0JtQ5Nlx1MDAwMXtcdTAwMDBccqpcdTAwMWVq0sJVVVx1MDAwZlx1MDAxNFx1MDAxNepcdTAwMTlb0Mt5JjCLgyOxXHTOuJklMZiYSoSjRcLl5nhcdTAwMTB2XHUwMDE5XG7m8Zc1XHUwMDExbLV4xFx1MDAwZlx1MDAxZTxcdTAwMTejPEzfJFx1MDAxMElD6nFyXsFC44HBI0JlfVx1MDAxY0XStcxcdTAwMWOfiTFcdTAwMDVvzFx1MDAxOdXY4NlcdTAwMDFbqmyUw12xhf1cdTAwMDfGXHUwMDA3I1usXGbN9y6W3fHwYboslj1ccm0kcy1ATk/RZSiE5sbuimdcdTAwMTWqqC5cdTAwMTRjTGzU0L9rXHUwMDAxZlx1MDAxOTdro7NcdTAwMDXDdFx1MDAwM4BRNztcdTAwMDQzXHUwMDAxfore7Vx1MDAwZYhFgncoXoBX1Fx1MDAxYlx1MDAwMHRVTrtaOD5qXHUwMDEyW0BcdTAwMTlcdTAwMDQk2iDwuZ9cdTAwMGbr4cFkpOKKT1xiMec4zi4gwu4jL0bsYT+c7uOljLXFTCP0mJlcdTAwMWJ4YjiCMtRiJ9Hbvp15MyCGtVx1MDAxMmzDxlx1MDAxY/4uxlx1MDAxOdy3XHUwMDBiaNEgXGZcdTAwMTc+gT9+1zORJ1x0wVx1MDAxNIiM1731XGa21Vx1MDAwMcxcdTAwMDM0uo70V3nbXHUwMDE5zlx1MDAxMz3Bz3mDXHUwMDAxy+KEXHUwMDEzsuCLxVxuwbjgyXPOXHUwMDE4m4VPssrBxCYnXHUwMDE2XHUwMDA3WDRHjHtcdTAwMWaN+VxmO1x1MDAxOOLI9KArtVrfwvlcdTAwMWOJv8TOkehcdTAwMTk6hyNRdtJhqCPeqVme3DS8Q64h1jTjelx1MDAwM0eH91VCW7nN0HJp3LSVz8ScscTUz/Di2DFcdTAwMWZx3PBcdTAwMDBl2bT7cPNIXHUwMDEwKp9KnFx1MDAxMswsLk0ohI3OeHA2fFx1MDAxMjugbIpdg1G1cH/7ktySXHUwMDEy7lqEXFxdi4c3r1x1MDAxNXqZYdQ8R1x1MDAxN1BcdTAwMTVcdTAwMWI+KWOHhVx1MDAwYs2VXHUwMDAwwZfKdDw2Jlri+Vx1MDAxOcxcdTAwMTIqxY5cdTAwMDMmXCLvwiQli1x1MDAxYiDWK1x1MDAxY4v3/eufZFx1MDAwNlx1MDAxMeVcdTAwMDSx5VRqSpNcdTAwMDRgwuKXXHUwMDAymYFcdTAwMTXVfiyXLLF69Vx0Llx1MDAxOJW4nub/xG5y1v2O0nldPK87c0OkkYOSpU3sr6RPybprMU6Zyj5cdTAwMWXRXHUwMDFhzJvgOlfdXHUwMDE0TvBSktOEVCq6sV17mtIwOvPq4NIo6CTXXHUwMDExgi6BXHUwMDFkbNW63M8npjIoQYSDwH6gppP1bYuHvFx1MDAxNM+WXHUwMDEz+1x1MDAxMb6ESfp0S7xcZotcdOi50S1cdTAwMTJ7OElcdTAwMTZti4eGxoxTxGpcdTAwMTJEt1x1MDAxN1Lb44UhO/kvJZl9XGaTK59t8VwiXHUwMDE2XHUwMDEzToFKKX9vyq54Ss6pVNqg9Vj2nc+j4HBSlvGwKlxmqDviXHUwMDE1N2At5a+VprS7il/BnmajLJ+1XHUwMDE4rF2ftpQxtVmlINj6sqv0VZH87JOSLfztd5WWqrDVysFhrlxiXFx3fd46Xshjh9g8ZG/39emGXvWbRpfalyrEr4uXxYU4iapbuDq9/dhcdTAwMTZcdTAwMGb7XHUwMDFi09hKwFx1MDAxNqbdz1x1MDAxN39DgITgYVx1MDAwYkowu+LBjVx0qqBkXHTfmvjMTVx1MDAwMFx1MDAxNjZO0lJ1K7UzXHUwMDFl/petg0vyXHUwMDAx4qqJXHUwMDA0ioR461TlXHUwMDAx5+zXNFx1MDAwMKjrPFWjuEtFi+dweL2KyDtlfUK/oMF4+Fx1MDAxNUFAZmVu+rxcdTAwMThHqK5RVj5GKOKM5/XDpVwijdBcXFFcdTAwMDNoxzS5qlx1MDAxZHDrXTppTcD+RSilcofZtFx1MDAxMoj+wIR0yVx1MDAxNNTjNGP78Ec4pFx1MDAwNKjX1sdcdTAwMTbPKstAbFx0x4Khznhg9b2GqC4rY5NtMjs2QNiUcFwipCF+Mv2AUIBQPNhcdTAwMTh8VzSqXHUwMDAx9Pg/di+OhFxy4tZNXHUwMDA1Wlx1MDAwM+XFJlx1MDAxOJVQsVx1MDAxNttcdTAwMDCqRMpiv9XnXHUwMDA3N58jglJcdTAwMDMpXHUwMDFiO49nis2ZjPGYgYJcdTAwMDUlVU3/usaaOkDtIeXB60KmJVx0yksosZfUXHUwMDAx53M/XGJcdTAwMTEgXHUwMDFmcVx1MDAxNk11RYFrXHUwMDBiKIpNSO2QRr3eXHUwMDA11Fx1MDAxYazkXHUwMDA1y89zbapcdC7fXHUwMDE5lVx1MDAxMLFAiHb/RNhcIoypRX2VNPWbXHUwMDFjKytgLFwi7Fx1MDAwNFxcPri+xHCEXHUwMDBl2dfHWKNrqpJcdTAwMTAnhGUsMVeOtp87XHUwMDE2XnKqXHUwMDFiI4TLMlx1MDAwZVxy3njzl3LJumSJMzZPMYOcuqTQpc3lxUGe3qqGMmRcdTAwMTSub7OAS1xiXHUwMDAzRlDXXHUwMDFkIWxSfPHNnDjYkIPsYOxLi7pnXHUwMDAy1FRcdTAwMTfCqZa6uUBcdTAwMTTOj9EgQZJXLdFcdTAwMWO8pLJggmB01DY1XHUwMDE36JtcbjJQXHUwMDFkjlx1MDAwYss1b4GEVZyfgVx1MDAxZJUmRuKw+Fx1MDAwZU+wRWBBUDHjfOOgUlx1MDAxMIXTXHUwMDE2/WwyqW4w6iCKdSycizPUXHUwMDE3PC/Dwlx1MDAwZanop2WVXHUwMDAzolmDMt+RM+ubl1x1MDAxZZzRXHI7XHUwMDA2tyrZVPspk1x1MDAwZaCRhY5cdTAwMDXLp2pcdTAwMDXVd81cdTAwMDHku1x1MDAxNaGr7js0N/RGt2dOSlwiZYxixrPW+HpI1fEmVe/qMaIuMmbt4ptcdTAwMDDR4Fx1MDAxONhHbFx1MDAxMapcdTAwMWW7STvhZav8tsGyXHUwMDEyiZRNvKCbbPZcdTAwMTZmZCT2u+LFkY9cdTAwMTAgXHUwMDE3JD7NMVpvxiOo0E2m+th54DlK/Ga8oqZcdTAwMWUhpbGnZNbzXt0t8HvgoST4tTDepmdo56zHfVx1MDAxM1x1MDAxY7pcdTAwMTNU1VFktXKdJc/Xw3unwb19crpYLp/z12Y/wfLg2fPDvdXiy1cvT1x1MDAxYUn2lquPjp89O1itXHUwMDE2j75Ujf1Gif2bXHUwMDE2+NrlnS3ut7N3/nr+TWfNKFx1MDAwZr7Nxz+svvvgyZ0v0bhcdTAwMGY+O1rc/en+3P7POqhcdTAwMDRcdTAwMGZ6XHUwMDE4+Pe6aeesXHUwMDE1xatcdTAwMDBcdTAwMDGH4YI7s1x1MDAwMFx1MDAxN69ftKLAulkl/+O0XHTYdm7n/qO1njzepouMwMqU7K5qPSmvXHUwMDFkw1DVR4RbuPnhKdfvPDlvXCJbPVncert7PycrvJlcdTAwMDayj7759LN/n35w75P67MV+/eTu4sO8tz+/gayo8Fx1MDAwZlqRxL3MpTM4byHT7Vx1MDAwZf4jRVx1MDAxMdt1e9iF2nqib7S6QmXGXHUwMDBlj6nWulxuXHUwMDE1V01cdTAwMTfRJZ71UqHJn82gr05yrcVP5muxvLpcdTAwMDKYq1x1MDAwNiBdanOY9HBcdTAwMWJdzNTLfXw3psZKjl+rh/v/rIHM4DS37EDaaCBcdTAwMTMv3PLz/9/9Y35cYkSEMSlfTtDhusHupH9LZ1x1MDAwYpdVU5NuXHUwMDE4tvt8XHUwMDFlNNBBmTyTla3oXHUwMDEzr83P67JcdTAwMWZOaZSXUtHEdp+ug49iXHUwMDFmKixUVmbL5jWrqIBt15hcdTAwMWEz9pBt+XlZ76pcdTAwMDY4S3SW+4mGyceVXHUwMDFhrEqux7G3qvv5wWe2ylnIn9NlfVx1MDAxM3k61ejZ8ZrUjIFkXHUwMDFmXHUwMDEwK1x1MDAxM9TMVJHelNvuXHUwMDAxNyBbXt1kmvLi+vUsdlx1MDAxOPVcdTAwMTDLRVxchzinJr3qObGiMXy8qMC+h0esX1THYD2ypVrzXHUwMDE2LnttPlx1MDAxYmiUbu7B+aFWtdrhXHUwMDE4rVxuztuGhDBcdTAwMTbpXHUwMDA1Y9hcdTAwMTVV83dPNFx1MDAwZVx1MDAxOFx1MDAxZJVxq9pcdTAwMDJS2qxP7XFqv4iVLXZlRpmlLjdj1Vx1MDAxNVjSdX1t8SDjuSpnMyeRUlx1MDAwNtWeusDzmmxcdTAwMWKsNIzlfkFcdTAwMGLUzMOusNQhWvh/1u2r1X1Vi+dyterVccpcdTAwMTnOyIFcdTAwMGVqi7NKSo8tmC1cdTAwMWNcboVEuqyC8dxPMqq3Ua1nXHUwMDE4YqNcdTAwMWK1SWehXHUwMDFhXCLZXG7k3Ke+rin8Ss5cdTAwMTlkK42Foy1cXKhOtsOhuLH0k4JcdTAwMTmexEew0CakMOlz2lx1MDAxYVx1MDAwZttXVGHhNY+yTOFyUFZFO1iS6eM5r6xcdTAwMWFi5fhyNqltfdxcdTAwMWVQNfVIq8PAV91cdTAwMDVN2mu2XHUwMDA1VJ44qttcdTAwMTO+mmLc+YnLkDG/MKZoVDa861x1MDAwM6uYMKmR2/qQ1Gwz7cTaXHUwMDEyUDdB8Fx1MDAxMlx1MDAxZjPEJDRcdTAwMTWv18BT4VpcdTAwMDYri8vltvJke7yibuKapCOam2p3XHUwMDA1XGbsoOx8cvJGSOGuR1x1MDAxY+yQ1LBcdTAwMWIg71x1MDAwZbdZWjzdNyFcdTAwMDBJXHUwMDExTupzl4C/KVx1MDAxOb/qXHUwMDBiioqhnlx1MDAxY/G2gHFApHXB7otTgmRcdTAwMTOvXGbq1YbZqHum1j43XHLoSFJbzHhcdTAwMTXkW6uwPZ6uqVRr5tVgPW30KoZtgPhGvFHsN1x1MDAxN0avXHUwMDA2ZpVcdTAwMDZcdTAwMTIrXHUwMDEyLE76snQnkqNcdTAwMWFcdTAwMTUhpDPaRPDX4o1icMSpvlx1MDAxNemxlCnrJlx1MDAxMFx1MDAxMmNmtLGkXHUwMDAx+Vx1MDAwYuPtZeKcJ+3BrkBsJEpZXHI8XThcdTAwMDRcdTAwMTY8q1x1MDAxMXkjwW/h1EPnjVr84W1zKvlwhepnr2qwzk3lJ2R0bFx1MDAwNVA1KT6uX4hcdTAwMDabNMQrXHUwMDFhi1x1MDAxYTV+ILZ4IY9F1mqLwkD2XHUwMDAxLVx1MDAwNtVrXHUwMDE2KzY6pNTCVU41joZsRil4QlTOXHUwMDFi43l7252pTnPdXHUwMDFk5DiOOutT74SoeE5VKS74k7ctXFzSPYhTOTjOtK+5KassXHUwMDEwOU4qXHUwMDFkXHUwMDBmXHIxdUp3wtOCRthcdTAwMTb1j3Tx4GqqYWWHNFxipLlUV9msrs7UzVx1MDAwNrvqR2mparRERpirXHUwMDFhb9vJXHUwMDAzYUC8XHUwMDE18ER55v5cdTAwMTV9Njwtelx1MDAwNFhWQrpRjKgxXHUwMDBioWpKL3zcdCU56/pcdTAwMDTHlrUyd2mU9jlcXIgyX7hnXHUwMDEz5sSkWVRS9YBWc0zcdHlcdTAwMTljXGZcdTAwMTdhgTDDvtnLoy6p1VxuX8l2N3iJo8pqXHUwMDFk0ZXhjJZvlUCW0Vx1MDAxMKlGOOdJPblcbofBgeyaVPs1x+BllYuxh+pcdTAwMGZuamzEk7JmR3ld1MR+a1JcdTAwMDeOXHUwMDEwXHUwMDFms4dcdTAwMTXDhnpccmPeXHUwMDE5XHUwMDBmc83zXHUwMDEyoPrxxn1nPFx1MDAxNcJYVdewg6ZcdTAwMWZjdfHQV2JcdTAwMDVcdTAwMGVXvZn9ILqDp6Z1nHwo6s2M/WpcdOCC7lx1MDAwMaOmXHUwMDE4JFx1MDAxYibtXHUwMDEx2FaWl9XAjFx1MDAwNZqDR5iFIONcdTAwMDWJPdpcdTAwMDJwOXFcdTAwMTRb41x1MDAxNYIzYYZyoLxFo3th9qyyLVBXpYzqr5O6pVWRMMe2KD4tgVx1MDAxMDWYSXtJkE6bqOrvXHUwMDE581x1MDAwYjB8XHUwMDE4qVwifuZV4eBbuCZbPVx1MDAwMy+qRVGkoNo26L1cdTAwMTZeUClcdTAwMDTs1dTY9lx1MDAwMV5cdTAwMGLPXHUwMDEwXHUwMDAzQjdccus0TYHmNfCKJkJqIIxq2Vx1MDAxYcZ3LThNbqlqpSxqf9xcdTAwMWRPzZNcdTAwMDZZVUFgW7myPZ4oe8aNh2qVxdx5+/KoTlikoLHVO0tLXHUwMDA3Llx1MDAxNKhPUPGc8jJ9htaBg5hp0Fx1MDAwZeKJte+PS+rBXHUwMDE5pztjp+p6X/v89s1w4keqSWL/YCxuXHUwMDBlQXtcdTAwMDOaiuqJMzRcdTAwMDNIMzF2RjO2at6NXHUwMDA2ePpcXGeJsUi1xjdybk1Nptrlo0a6aGpcdTAwMTU8qc9GsaFWJspcdTAwMDSCOl1FTNitd2NcXICYKFx1MDAxZDnDZ5jE8VlcdTAwMThcdTAwMWFOddKDhL9zyHD0XG4k+4FcdTAwMTBwVXlf/6prv+F7hP1cdTAwMWFcdTAwMTZcdTAwMDVHY31cdTAwMDbO18VzXHUwMDAzXHUwMDBl32NcdTAwMDRcdTAwMTB9/j+J01xmjlx1MDAxM1x1MDAwYqtEb+yXyGY/pDI2XHUwMDBlXHUwMDEym5TUPC5RJKRRc1x1MDAxOVNxKjjrPy5sfVx1MDAxY4OFyESx0jZsRsmqXHUwMDBiSIw0pH9jlMd5XGZBXHUwMDBlXHUwMDE16p2mRely60ZmbDSxXTjE1Vx1MDAxOV24XHUwMDEwlE4mOOXhLDvkx8TJXGburXJQ1d6LXHK37EJZ1zTO+Nd9wFx1MDAxY0khboHU1GqVRlx1MDAwZdNcdTAwMWOztM+oJrBcdTAwMTRcdTAwMTP7vZfFqPmzXHUwMDA0i1ZwJG13VNLwJJuMJlx1MDAxOKpcdTAwMGWy30/iiYpVkVa9fFpbt6vfIZTcOIVAwtlcdTAwMTeVooy+7uk1XGZcdTAwMDRdmozXXHUwMDFinZ36fzWwpn9cdTAwMWFcdTAwMDXyr+t81aqq42pyXHUwMDFmsS3eKKxOoio21yaUMFROl8O5jlx1MDAwM4T6gWRV+6Azqnpcclxuelo4QkK+JY7B6YzxZtVccqOUaGYgpK/prb1cdTAwMDZegJ1Bhq30k0AytHg6J82PyJiqXHUwMDE4uqpWNVikajxiVulz21xmXHUwMDE2hnFus9aoMT99ca5cdTAwMWOHSyFcdTAwMWFNrHSxTTFvjTd2k1x1MDAxMCzqXHUwMDBlXHUwMDFhh1/b9pSt78PGflxyZ9BgK9Lucm1cdTAwMDGJXHKccqe6MzP9dmdr2PSk+1xiyWtKTS36dVx1MDAwMKt6UHRnq7Srb1x1MDAxYkCUufFcdTAwMDRLRZZcdTAwMDHJmlHujfNV+jtrq+B1sVx1MDAwNdSNXslcdTAwMGXPpq7nXHUwMDE5gG7IyvJcdTAwMTnda6uwtlx1MDAwNdRQhSyyb/X7zGaVZ4exjrCqZb21+Lr5MVx1MDAxYc7E0pWBmlXaqVpV5Fx1MDAxOS2Y0CGrjnJviyi/mibijNpYfFiM2niIuJoyWsBLY1I1qnpcdTAwMGWgplx1MDAwNfJU6uFcdTAwMGZt04tmiVi1d4exhb9P2KxmcuWgsWHKhYS2cU1cdTAwMTlcdTAwMWWbPH5cdTAwMThCXHUwMDFl3YzRoJpSNV4moKlj8ccmIHpJWIfcK59vZ+SsrFx1MDAxM4vJStZcdTAwMTJcdTAwMTjBsSaASYbIunFcIpqfUebuJDXKR6LJqvidXHUwMDAwuqKOoaBhXHUwMDFjue9KrGZcdTAwMTOYcXBcdTAwMWNsXG5BnDTRaEqIhlSOs25nPLBcbmRr0Vx1MDAxY0zZheaMNYnSuapcdTAwMTlIalszMy6Qk3LKY4dcbnzFNSldtdGrUFx1MDAwNL+ueqF+wbJmjUaPN3aE5zE2vqlqQlx1MDAxOGel9Du80vZ1RIkzjUxQJ1xi3qe5zq9SYjRSYzA0knTG9mVN3VNcdTAwMWRG0mjaTaOAn9GFiipC1ds747rcqedKXHUwMDBiQ+mxrLaB051UXCL0XHUwMDE4f7Ffv1DDKo9UqkbwOlxyXHUwMDFhbG52subd2bFGwqVcdTAwMTT7N0XCUypXuVx1MDAxY2XBmlxiSVx1MDAxZG1WhVx1MDAxYbqET3lGy4YmSiFcclx1MDAxYdauXy5cdTAwMTdcdTAwMWE4me8q/caeXHUwMDE11z9cXFxylJKYqFx1MDAwMkzTclx1MDAxYThdmdioYqFxlGdcdTAwMWZOTZ3i+Dmpc6ShgVx1MDAxMVx1MDAwNzhOX/bqes8z+sHAi5FH1bRuPrspelx1MDAwMW+lSZBqXHUwMDEzXHUwMDE29+hrXHUwMDFhcKWMXHUwMDFkp1ndXHUwMDA0roFDIzwyp8tcdTAwMDQ56j6czDnRllo7okasNHhnbTNcdTAwMWVcdTAwMTKjfo1cdTAwMTlPq3Y1uf6s2ei2XHQv/TgvwFZcdTAwMTlcdTAwMDPdXHUwMDA1z2BcYl7DsFJM6r2LTX7Yc1KceTIqY9TopTloZ21l+j2kmoXU4Dm1zpQ41lx01X5JnfBUd+jHXHUwMDAxjfq9XHUwMDA1m9E0jkj0r2rtZk5HrJdns5IuyFx1MDAxNXvY4ulU8ck+a0bvXGZh6cBcdTAwMDVd2Vx1MDAxMjSqXHUwMDBmcNbjvqam+Vx1MDAxNVx1MDAxY7ZcdTAwMWaGrW8syMyMmeFcdTAwMWS8sbtKg+Yx8yHN8ONvxNNw46Rf0VRcbo4gzKj3tOOv+FCSSSFwTu218aAsicNDXHUwMDEyJlVccnKao71J1MSq3ytNpoZcdTAwMGZcdTAwMDVcdTAwMGI7/irIglx1MDAxZu9PlL4+3jtccu7v1Ynzhlx1MDAwNb52eW/qxPku+6Pl18vDVbpz7/vP/Iv8yzd3P575XHUwMDBinTRcdTAwMTROPb5eibRpI844l1BcdTAwMDP2XHUwMDE1Y1/Rh0MkkNWWmtOY9vjz1zltWcF/sNWvYdNAXHUwMDA1m67qw0mTuv6LXG5+temrPvXmf1x1MDAwYpuq0tbE7LqNOM+Xi9O3vFx1MDAxM2dziVu34rzzyqbc3js5ub9iXHUwMDBmefVMhW//dLD4+cMrZHx//HP7nVcqL1x1MDAxOVx1MDAxZq3Qr7+989t/XHUwMDAxe+9NXHUwMDA0In0=<!-- payload-end --></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><g transform="translate(10 88.41995281069649) rotate(0 55.145973205566406 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">https://</text></g><g transform="translate(234.9639060803529 88.62313663611394) rotate(0 58.43596649169922 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/profile/</text></g><g transform="translate(479.45444109015443 88.64564702659618) rotate(0 142.11392211914062 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/post/3lzy2ji4nms2z</text></g><g transform="translate(119.93913228719248 88.45006504985213) rotate(0 57.189979553222656 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#228be6" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">bsky.app</text></g><g transform="translate(351.4890063087005 88.78824865883189) rotate(0 63.95197296142578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">ruuuuu.de</text></g><g transform="translate(120.97916270110727 87.40003626069483) rotate(0 57.92899915737689 -15.052645088736654)" stroke="none"><path fill="#228be6" d="M -2.67,0 Q -2.67,0 -2.69,-1.07 -2.71,-2.15 -2.78,-3.49 -2.85,-4.83 -2.91,-5.93 -2.97,-7.03 -2.94,-8.45 -2.91,-9.86 -2.48,-11.01 -2.05,-12.16 -1.00,-13.85 0.03,-15.54 1.60,-16.80 3.16,-18.05 4.21,-18.79 5.26,-19.52 6.62,-20.16 7.98,-20.79 10.01,-21.33 12.03,-21.87 13.68,-22.22 15.33,-22.57 16.66,-22.82 17.98,-23.07 19.25,-23.20 20.52,-23.34 21.68,-23.41 22.85,-23.48 23.97,-23.52 25.08,-23.56 26.17,-23.58 27.26,-23.61 28.86,-23.56 30.45,-23.51 32.29,-23.23 34.14,-22.94 35.20,-22.82 36.27,-22.69 38.50,-22.22 40.72,-21.74 42.41,-21.36 44.10,-20.99 45.26,-20.80 46.43,-20.62 47.78,-20.65 49.14,-20.69 50.12,-21.36 51.11,-22.02 51.53,-23.33 51.95,-24.64 51.98,-25.90 52.01,-27.17 53.30,-28.28 54.60,-29.39 55.09,-28.37 55.58,-27.35 56.63,-26.50 57.68,-25.66 58.98,-24.79 60.29,-23.93 61.79,-23.28 63.29,-22.62 64.39,-22.38 65.49,-22.14 67.08,-22.04 68.66,-21.95 70.25,-21.92 71.83,-21.89 73.33,-21.88 74.82,-21.87 76.35,-21.87 77.89,-21.86 79.11,-21.82 80.34,-21.77 81.99,-21.66 83.63,-21.55 85.05,-21.50 86.47,-21.45 87.99,-21.39 89.50,-21.33 90.87,-21.30 92.23,-21.27 93.95,-21.32 95.67,-21.37 96.79,-21.42 97.92,-21.47 99.12,-21.53 100.33,-21.59 102.01,-21.19 103.69,-20.78 105.07,-19.94 106.44,-19.10 107.61,-17.92 108.78,-16.74 109.69,-15.67 110.61,-14.59 111.35,-13.74 112.08,-12.89 112.96,-11.72 113.84,-10.55 114.51,-9.55 115.18,-8.56 115.95,-7.53 116.72,-6.49 117.24,-5.56 117.76,-4.63 118.57,-1.94 119.37,0.74 119.35,1.31 119.32,1.88 119.12,2.42 118.91,2.95 118.54,3.39 118.18,3.84 117.69,4.14 117.20,4.44 116.64,4.57 116.09,4.70 115.52,4.64 114.94,4.59 114.42,4.35 113.90,4.12 113.48,3.73 113.06,3.34 112.78,2.84 112.51,2.33 112.41,1.77 112.31,1.20 112.40,0.64 112.48,0.07 112.74,-0.43 113.01,-0.94 113.42,-1.34 113.83,-1.74 114.35,-1.98 114.87,-2.23 115.43,-2.30 116.00,-2.37 116.57,-2.25 117.13,-2.13 117.62,-1.84 118.12,-1.55 118.49,-1.12 118.87,-0.69 119.09,-0.16 119.30,0.36 119.34,0.93 119.38,1.50 119.23,2.06 119.09,2.61 118.77,3.09 118.45,3.57 118.00,3.92 117.55,4.27 117.01,4.46 116.47,4.65 115.89,4.66 115.32,4.67 114.77,4.49 114.23,4.31 113.77,3.97 113.31,3.63 112.98,3.16 112.65,2.69 112.49,2.14 112.33,1.59 112.33,1.59 112.33,1.59 111.80,-0.17 111.26,-1.94 110.68,-2.93 110.09,-3.91 109.15,-5.30 108.20,-6.69 107.49,-7.66 106.78,-8.62 105.97,-9.61 105.16,-10.60 104.47,-11.45 103.77,-12.30 102.98,-13.14 102.19,-13.99 101.25,-14.64 100.32,-15.29 99.12,-15.36 97.92,-15.42 96.79,-15.47 95.67,-15.52 93.95,-15.57 92.23,-15.61 90.87,-15.59 89.50,-15.56 87.99,-15.50 86.47,-15.44 85.05,-15.39 83.63,-15.34 81.99,-15.23 80.34,-15.12 78.37,-15.07 76.40,-15.02 74.94,-15.02 73.47,-15.02 71.85,-15.02 70.23,-15.01 68.47,-15.03 66.71,-15.05 65.56,-15.10 64.40,-15.15 63.29,-15.32 62.18,-15.50 60.71,-15.97 59.25,-16.45 57.93,-17.09 56.62,-17.73 54.90,-18.76 53.18,-19.79 52.25,-20.51 51.33,-21.23 50.20,-22.48 49.07,-23.74 48.40,-25.28 47.72,-26.82 47.65,-27.90 47.59,-28.98 47.65,-30.30 47.70,-31.63 48.55,-32.89 49.41,-34.15 51.04,-34.51 52.68,-34.87 53.74,-34.56 54.81,-34.25 55.85,-33.37 56.89,-32.49 57.62,-31.49 58.36,-30.49 58.74,-29.08 59.12,-27.67 59.07,-26.36 59.03,-25.05 58.77,-23.77 58.51,-22.49 58.12,-21.38 57.72,-20.28 57.06,-19.13 56.40,-17.98 55.60,-17.14 54.79,-16.30 53.84,-15.68 52.88,-15.07 51.38,-14.74 49.88,-14.42 48.69,-14.50 47.50,-14.57 46.37,-14.73 45.23,-14.88 44.00,-15.21 42.78,-15.54 41.14,-16.03 39.50,-16.53 38.23,-16.82 36.95,-17.12 35.23,-17.53 33.50,-17.93 31.98,-18.07 30.45,-18.22 28.88,-18.16 27.30,-18.11 25.17,-18.13 23.04,-18.14 20.96,-18.08 18.87,-18.01 17.67,-17.81 16.46,-17.61 15.08,-17.27 13.70,-16.92 12.05,-16.23 10.40,-15.53 8.67,-14.55 6.93,-13.57 6.00,-12.72 5.06,-11.87 4.31,-10.71 3.57,-9.56 3.27,-8.29 2.97,-7.03 2.91,-5.93 2.85,-4.83 2.78,-3.49 2.71,-2.15 2.69,-1.07 2.67,0 2.63,0.32 2.59,0.64 2.48,0.94 2.36,1.24 2.18,1.50 2.00,1.77 1.76,1.98 1.51,2.20 1.23,2.35 0.94,2.50 0.63,2.57 0.32,2.65 -0.00,2.65 -0.32,2.65 -0.63,2.57 -0.94,2.50 -1.23,2.35 -1.51,2.20 -1.76,1.98 -2.00,1.77 -2.18,1.50 -2.36,1.24 -2.48,0.94 -2.59,0.63 -2.63,0.31 -2.67,-0.00 -2.67,-0.00 L -2.67,0 Z"></path></g><g transform="translate(123.50627446410363 10) rotate(0 51.897979736328125 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#228be6" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">the app</text></g><g transform="translate(354.7891181149789 89.72562765395332) rotate(0 66.51214547980635 -14.811747175490382)" stroke="none"><path fill="#7950f2" d="M -2.59,0 Q -2.59,0 -2.55,-1.63 -2.50,-3.27 -2.53,-4.37 -2.57,-5.47 -2.65,-6.96 -2.73,-8.45 -2.82,-9.89 -2.91,-11.33 -2.35,-12.80 -1.79,-14.28 -0.99,-15.07 -0.20,-15.87 1.50,-16.71 3.20,-17.56 4.49,-18.07 5.78,-18.58 7.35,-19.05 8.91,-19.52 11.19,-19.90 13.48,-20.29 14.64,-20.39 15.81,-20.49 18.30,-20.59 20.79,-20.69 22.05,-20.69 23.31,-20.69 25.31,-20.72 27.30,-20.75 29.89,-20.68 32.47,-20.62 33.64,-20.62 34.80,-20.62 35.92,-20.62 37.03,-20.63 38.12,-20.63 39.21,-20.64 40.78,-20.76 42.35,-20.89 44.00,-21.04 45.66,-21.19 48.05,-21.57 50.45,-21.96 52.34,-22.39 54.24,-22.82 55.82,-23.31 57.40,-23.80 58.61,-24.20 59.81,-24.60 61.29,-25.04 62.78,-25.49 64.32,-26.11 65.86,-26.73 66.92,-27.21 67.97,-27.69 69.21,-27.49 70.45,-27.29 71.38,-26.63 72.30,-25.96 73.09,-24.95 73.87,-23.93 74.70,-23.07 75.52,-22.21 76.54,-21.55 77.57,-20.89 78.95,-20.55 80.33,-20.20 81.38,-19.98 82.43,-19.77 84.15,-19.54 85.87,-19.31 87.75,-19.07 89.63,-18.84 91.45,-18.68 93.28,-18.52 94.34,-18.38 95.40,-18.24 97.49,-18.07 99.58,-17.89 100.68,-17.76 101.78,-17.62 104.42,-17.31 107.05,-16.99 109.22,-16.72 111.38,-16.44 113.17,-16.21 114.97,-15.98 116.09,-15.75 117.20,-15.52 118.70,-15.19 120.20,-14.85 121.85,-14.41 123.51,-13.97 124.54,-13.66 125.58,-13.34 126.85,-12.86 128.13,-12.38 129.08,-11.79 130.02,-11.20 130.85,-10.47 131.67,-9.74 132.50,-8.87 133.33,-8.01 134.04,-7.15 134.75,-6.30 134.75,-5.40 134.75,-4.50 134.90,-4.10 135.06,-3.70 135.11,-3.28 135.17,-2.86 135.12,-2.44 135.07,-2.01 134.92,-1.61 134.78,-1.21 134.54,-0.86 134.30,-0.51 133.98,-0.22 133.66,0.05 133.29,0.25 132.91,0.45 132.50,0.56 132.09,0.66 131.66,0.67 131.24,0.67 130.82,0.57 130.41,0.47 130.03,0.28 129.65,0.09 129.33,-0.18 129.01,-0.46 128.76,-0.81 128.52,-1.16 128.80,-0.77 129.09,-0.37 128.77,-0.84 128.44,-1.31 128.28,-1.86 128.12,-2.41 128.15,-2.98 128.17,-3.55 128.38,-4.09 128.58,-4.62 128.95,-5.06 129.31,-5.50 129.80,-5.80 130.29,-6.10 130.85,-6.23 131.40,-6.36 131.97,-6.30 132.54,-6.25 133.06,-6.01 133.58,-5.78 134.00,-5.39 134.42,-5.00 134.70,-4.50 134.97,-3.99 135.07,-3.43 135.17,-2.87 135.08,-2.30 134.99,-1.74 134.73,-1.23 134.47,-0.72 134.06,-0.32 133.65,0.06 133.13,0.31 132.61,0.56 132.05,0.62 131.48,0.69 130.92,0.57 130.36,0.46 129.87,0.17 129.37,-0.11 129.00,-0.54 128.62,-0.98 128.41,-1.51 128.19,-2.03 128.15,-2.60 128.11,-3.18 128.26,-3.73 128.41,-4.28 128.72,-4.76 129.04,-5.23 129.49,-5.58 129.94,-5.94 130.48,-6.12 131.02,-6.31 131.60,-6.32 132.17,-6.32 132.71,-6.15 133.25,-5.97 134.00,-5.23 134.75,-4.50 134.90,-4.10 135.06,-3.71 135.11,-3.28 135.17,-2.86 135.12,-2.44 135.07,-2.01 134.92,-1.61 134.78,-1.21 134.54,-0.86 134.30,-0.51 133.98,-0.22 133.66,0.05 133.29,0.25 132.91,0.45 132.50,0.56 132.09,0.66 131.66,0.67 131.24,0.67 130.82,0.57 130.41,0.48 130.03,0.28 129.65,0.09 129.33,-0.18 129.01,-0.46 128.76,-0.81 128.52,-1.16 128.45,-2.18 128.39,-3.19 127.45,-4.25 126.51,-5.30 125.72,-6.04 124.92,-6.77 123.78,-7.19 122.63,-7.60 121.59,-7.97 120.55,-8.33 118.92,-8.80 117.30,-9.28 115.66,-9.64 114.02,-10.01 112.34,-10.28 110.67,-10.55 108.54,-10.82 106.42,-11.09 104.93,-11.17 103.43,-11.24 102.26,-11.29 101.09,-11.34 99.04,-11.52 97.00,-11.70 94.82,-11.84 92.64,-11.98 90.71,-12.11 88.78,-12.24 87.09,-12.43 85.39,-12.62 83.32,-12.83 81.26,-13.05 79.35,-13.36 77.45,-13.68 75.89,-14.21 74.34,-14.74 72.89,-15.65 71.44,-16.57 70.52,-17.47 69.60,-18.37 68.54,-19.57 67.49,-20.78 66.86,-21.64 66.23,-22.51 65.38,-24.04 64.52,-25.57 64.39,-27.53 64.25,-29.49 64.76,-30.64 65.28,-31.79 67.38,-32.45 69.49,-33.12 70.77,-32.73 72.06,-32.34 73.27,-31.26 74.48,-30.18 74.77,-28.46 75.06,-26.74 74.47,-25.46 73.88,-24.19 72.73,-23.24 71.59,-22.30 70.53,-21.82 69.47,-21.34 68.07,-20.81 66.68,-20.28 65.45,-19.80 64.22,-19.32 62.94,-19.00 61.66,-18.67 60.37,-18.33 59.07,-18.00 57.35,-17.54 55.63,-17.08 53.63,-16.67 51.63,-16.26 50.15,-15.91 48.66,-15.55 47.30,-15.33 45.93,-15.10 44.22,-15.01 42.51,-14.93 40.87,-14.89 39.22,-14.84 38.12,-14.85 37.03,-14.86 35.92,-14.86 34.81,-14.86 33.64,-14.86 32.47,-14.86 29.90,-14.79 27.33,-14.72 25.37,-14.73 23.41,-14.73 22.20,-14.69 20.99,-14.65 19.66,-14.55 18.34,-14.46 16.43,-14.26 14.52,-14.06 12.65,-13.69 10.79,-13.33 9.41,-12.96 8.03,-12.59 6.80,-12.15 5.56,-11.71 4.24,-11.47 2.91,-11.23 2.82,-9.84 2.73,-8.45 2.65,-6.96 2.57,-5.47 2.53,-4.37 2.50,-3.27 2.55,-1.63 2.59,0 2.55,0.31 2.52,0.62 2.40,0.91 2.29,1.20 2.12,1.46 1.94,1.72 1.70,1.92 1.47,2.13 1.19,2.28 0.92,2.42 0.61,2.50 0.31,2.57 -0.00,2.57 -0.31,2.57 -0.61,2.50 -0.92,2.42 -1.19,2.28 -1.47,2.13 -1.70,1.92 -1.94,1.72 -2.12,1.46 -2.29,1.20 -2.40,0.91 -2.52,0.62 -2.55,0.31 -2.59,-0.00 -2.59,-0.00 L -2.59,0 Z"></path></g><g transform="translate(355.26859761538435 14.120744112158263) rotate(0 57.35797882080078 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">the user</text></g></svg>
+4
public/where-its-at/10.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496.55975341796875 503.22839841437144" width="993.1195068359375" height="1006.4567968287429"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1aXXPTSFx1MDAxNn3nV6SyL7tVuKf79jdP41x1MDAwNDYwVJiAmVx1MDAwMLs1lZKtTiwsS0aS44Qp/vveVlx1MDAxY0u2ZcdcdTAwMGVmw8xgqkLS3ZKvuu85fc5V//Fob2+/uFx1MDAxZbn9J3v77qpcdTAwMTfEUZhcdTAwMDWT/ce+/dJleZQm2Fx1MDAwNeXfeTrOeuXIflGM8ic//VRdQXrp8OYqXHUwMDE3u6FLilx1MDAxY8f9XHUwMDE3/97b+6P8iT1R6K/Nj95cZrI2PX7LPn7od8U4Py2OO+Wl5aDbYFxud1VUrVc+XGLGhSGGW2NcdTAwMTRQXHUwMDBiVM+6r7G7XHUwMDA12mhitJXSWqZAsFn3JFxuiz5cdTAwMGVcdTAwMTFaXHUwMDEx7NWSXHUwMDBipq0yWs6G9F100S9wXGav2oLkXCL2sdBZS15k6cBcdTAwMWSmcZr5XHUwMDE4/9F1olx1MDAxYnarMLtBb3CRpeMkrI2Bc+jWxpxHcdwprss77/eyNM9b/aDo9fdcdTAwMTe+5d00aLbQPrs2T3Hqq6vway/6icvz2/UqW9NR0Itcbj8/jFbP4WNcdTAwMWO9XGLLNfq9iixcdTAwMGKG7oVfpGRcdTAwMWPHs+YoXHSdn//97ovi2dz3JeH0++bG5875ezBcdTAwMGJMKVx1MDAwYtzMeqqEYqq2PNPmV2lSZlx1MDAxNzBgxlx1MDAxOFG7MMqfYlpcdTAwMTXlfc+DOHfVjPsoni2mXFw97WqpXHUwMDE3nJr+8efWJDk7fCmSfu9o+Dpoz55oLv2CLEsn+7OeL4/X3fdMP7+4yqHVXG5FN/j4+iRcZtwnudl9p79VizBcdTAwMWWFwc2DMi2t4FRxLrma9cdRMlic8TjtXHJcdTAwMWHmpoRcdTAwMTB+Z1x1MDAxOIVPRnHvyfkoXHUwMDFi98cpwNVgKFNdXHUwMDE0XHUwMDE5XGaKqzCt5WaaXHUwMDE0nehzuVxuZq7138Ewin1cdTAwMWHJudu34+jCr+d+7M5rcMVcdTAwMTUtXCKkhll3kY6q3lx1MDAxZd4viFx1MDAxMpct51qaRVx1MDAxN1FcdTAwMTLEb7dcdTAwMGU9XHUwMDE4XHUwMDE36Vx1MDAxYpffXHUwMDA0X2RjV58x9/xcdTAwMTbejIB8VFvRXHUwMDA1cqLG6F/CTrdcdTAwMWRcdTAwMWVcdTAwMWR/uJIuTDrmfDNyXHUwMDEyTFx1MDAxMInEJFx1MDAxOZVcdTAwMWGAL5CT4pRwISnlWkswuuq/ZScuJcElp8wgz1x0XbvDjJzAXHUwMDEwZZSUglxuLqgy1Vx1MDAxYd3NVcz5f38jrnq+OVdxblx1MDAxNbJOM1VZIVZSXHUwMDE11bhcblxciFxunzuiqm9KKdJcdTAwMWFrRVx1MDAxZCDbUsowzVxcTMZ5y1x1MDAwNXlB+in+6OaDa5K4YpJmg2ZGXHUwMDAxYoVcdTAwMDXBgWN2S2G/XHUwMDA3gtn4QXbDL+/OTox+9SFcdTAwMWGcnOSH5+efilOdXGY35Fx1MDAxN+v5xVx1MDAwMEpcdTAwMWYhWVxy+jf8YqUlTCtKQSqpZdV9Sy/MePEjQWmO6T63vVbiR1x1MDAxMFx1MDAwYoZyS1x1MDAxNVKVNuaHXHUwMDE4Wk0wl2ZcdTAwMGIxpLQwYHBcdTAwMTVcdTAwMWJcdTAwMThcdTAwMDZA0sXmW4aRmlpcdTAwMGVCV1x1MDAwM1x1MDAxZV5cdTAwMGJtQDBSXGJB61xi2ZZgflx1MDAwZYMk6GZcdTAwMDFcdTAwMTmml81sopFNkExcdTAwMThIplAksblBXHUwMDBmxCbNUe+GOtYv2MJiLXCH1JZQLVx1MDAxNLVe4FSAmFJcdTAwMDfjhHNcdTAwMDNSM+ONU7WZzbiD4GUoXVxmxfVl1NpcdTAwMDbuYMxcdTAwMTLBrNZKoXxRWlX65fvgXHUwMDBlWGh/QO74XHUwMDA1Tpu5o1x1MDAwMurtgsK05UtcdTAwMTX9lFJcdTAwMDQ6XHUwMDAzKrlR9Ty+XHUwMDE1Lazmjpf9lUJcdTAwMTXKa+S+XHUwMDA1p6xGPaJcdTAwMTBQ+N5cdTAwMDf1ozRapKzqt70qb8o/Zr///rhx9OpkLXuX0rS631x1MDAxMrHFKFxuXHUwMDBl0+EwKvBBT3yQS/xeXHUwMDA0WXGAK1x1MDAxYiVcdTAwMTfz6zetg7zYYN8vSas39lx1MDAxM0BcdLWGXHUwMDFhpSVFhYT/XHUwMDFiqI26XGJGZVx1MDAwMlx1MDAxMkut9qRnXHUwMDE1XG4pqZdyxCXh3UGtr8TUgmphVCC9bkNfQy1mXHUwMDFlXHUwMDEzZiksRXDOLVDFlGVcdTAwMTKfYDlz/WS1PUH1XVx1MDAxMDawXHUwMDE1hr22O+6mkyp91pDlJbMuOuP/6Z+2zs7k0bvD1vHnq1xyi0xGok5cdTAwMTLaKCtcdTAwMDXjXHUwMDBiRSZO0eahXHUwMDA2M1x1MDAxYXU/9TKqQWgxwspcIlx1MDAwNlx1MDAxYVx1MDAwM8agqcpcdTAwMDRcdTAwMTL5WGtcdTAwMTRslmm/zLs1ckVcdTAwMTYk+SjIcKm/XHUwMDAxX4ZB3nf/Z8LU24gt1FncO+lcdTAwMDZilLBSalx1MDAxOdRauFx1MDAxZfdSWuu0XHUwMDEwavWal7+HXHUwMDE26lx1MDAwN0lcdTAwMTi7vX/mk2A0XG66sftXs1wiQpQqYFowyVHd29o+/oCSaF3wu1x1MDAxMUbvXHUwMDBm2q5cdTAwMTWNO5e9dt/ZI1x0XHR/mWyGdYWbXHUwMDAxt1x1MDAwMFpLXHUwMDA1dlFcdTAwMTehjVwiaLukUkjIKIuqiatDXZWbKXCLbNxYs+FEU7xcdTAwMDenXHUwMDA2t0gp+Da66O+J9XhcdTAwMWJjhZ5cdTAwMTa386bKjZVysfVcdTAwMTbraFx1MDAxOVx1MDAxMJzC3q/G/G3RnuZcdTAwMDXu23fCnVx1MDAxOWLRpXMqlPLVcvFdXHUwMDE0bNdGv1x1MDAxYryPsvHwiFx1MDAxZXTfvHudikSO+MmJgGW8Z65X3Fx1MDAwMGxxg9eCMG6FkVx1MDAxNmXUXHUwMDAy6Dn1WzNcdTAwMDUwKHNcdTAwMTju3kuYR1x1MDAwMFx1MDAxM8xcdTAwMWJtJU4/16pm6SsvJFx1MDAwNZGcaVx1MDAwNqiWqICa5fqB+UbMm1x1MDAxNYaoXHTzUoPRXHUwMDEy5VeT8YFcdTAwMWG/LoJcdTAwMWXZXHUwMDE3eVxc0uohdlx1MDAwM/qy/sDqyXxcdTAwMDfo12R3enr9vqPfU1x1MDAxN16NJ1x1MDAxM/aqnVx1MDAwNjhcdTAwMTHbZLeXltRnN1x1MDAwNmbns1x1MDAxYlDhXHUwMDEwq5U3QIxJ0Fxy2a38K1x1MDAwNurdkSdJqVx1MDAxYbJbKVx1MDAwMohcdTAwMTPEiPE6mf7Y0e7IbrtFdjM0WZinvKlSiMrOrsxuMFx1MDAwMlmJP/yWtia717/WuKOIZVx1MDAwNDGCXHUwMDAxmiokVlgsgKPuJehOJaVomr0vXlZrXHUwMDA0XHKdRjZmXHUwMDE2yVmxWq2iym1cblx1MDAwNHUhoFx1MDAxM0BIXHUwMDBiynZcXFx1MDAwMf+2uf1cdTAwMDCVrPY2qVxyXG6tmWSy8T1cdTAwMWIsV7Kq92xcdTAwMTSooLpGsn+pklVrdWr6z3JSVjd8tHDj3dWsNi5cdTAwMGah7/RFIf/mSSBxodPRrDZsWrRCpyQsqm7gXHUwMDFhtDFcXExHbFm0Wv+Gfm++aCX93sxlSVxuIGVViZmFZYg/o2IspoHEgbBB0WpuLndWsTp9ObC0c0o/XHUwMDFh0UE2SvpvPp083czFSn8sSmguXHLzb+2qjeOGXHUwMDE40Vx1MDAxYlx1MDAxMG9RNeBcdTAwMDNquqxogaFDNVxmxfDKU1FoNpRcdTAwMTZcdTAwMWOxy7ifsNrb4z/Blv9cdTAwMDC0eFx1MDAxMG5Bi8ZrUmZcdTAwMWJcdTAwMDWttqt3fH+RRO97r9NcdTAwMDfr9CxSMVRfe78jR82mVaB3skKiL6KWS1FTOVx1MDAwZnvKaNcm9WNyXHUwMDE0tdNfKT/97Vx1MDAxZHeDw+fjTneyIZytJFx1MDAwMqU7blx1MDAwMlJcdTAwMTm6dJDIWMIobqVcdTAwMDBUyYbyM+NEKG1B+Zq9xcX8XHUwMDAx56+Hc7E5nFx1MDAxNWOao/lvXHUwMDE08IyuhDNSs8LtXHUwMDEzdo5mjlx1MDAxYvJXoTl32WXUc8+SsFQ9f1wiZK+MfDcoP357eTLqPT3uXHUwMDFjXHUwMDFm/Dq5js56w+fn3c1QjrqEcK6MUVZYpVx1MDAxNlFuPFx04CBgRmm9fJrHMnT5KFx1MDAwNv2LNu7fdzSDXHUwMDFjd3T8WK5QWLK/MsjN12P8cFx1MDAxYieDXHUwMDEy13gv01x1MDAwNHK+XFyNvsU4XHUwMDE34I92su/vJVx1MDAxMzamL5N0krTzlfiW/lx1MDAwNTBwYbmuXHUwMDFmSX5AfDdGvTW2XHUwMDFmTf3TfjBcdTAwMWF1XG6c2Zn/2L+M3OSg4TDKefnxh6JKZvB578pcdTAwMDNcdTAwMWNfXHUwMDFlfflcdTAwMWaon09iIn0=<!-- payload-end --></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAABhMAA4AAAAAKjgAABf3AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiIbiGYcgXAGYACBFBEICr8srksLUgABNgIkA4EgBCAFgxgHIBuQICMD9YrSSkL2VwncofCVD0BNeYLEraEQxKYuEprPA6E9WuwO5OyKF8P0K6yMkGR2eP7U3v8/+bEbNKmYwoHqVOSkYkRHHrRrb+tx6Nyxk66T+edlz513/1eqacGEoitmZozSxC6rHV9+bUc9oJlr07cNSYRSSOkLqnCVmVzUTpT5cmovvX+TrgOGtC7YVssLyTYLwHa8cFo+/cNdukrXAYHtkMC2nKR/YwexuDjSvJ/Y4uz//36u7iHW/kJCNVS8c0h9e9uA+y/sIfZRa2L1YZZIoiFZpJloskbz0oiUDqmSGoeQKKVhXsw8myraeW/lhdxfJuY2A76Ha8m89QzcDoAAQCYgF4bAulsMEiWIPUJnz4fIgwZmf4hKBMJzW30lEF7riyqA8JHXWA2EMAAAHFLgfqmvBngqupUPvLRDneDz8OBGrnx7kG2n0HCBb1OhwLcfv51ibBTItPotPSiVura/l1MRgasef+Rrg5csX60AqcZ8+2OAt2Nio8tm7GOUQn2urk3MbOZcUZ8EzWsjxZOCsGAQkVFQ0TAwsXDxiYhJyCnYUnqGhFDxZDpcQm1c7Um6nFwEG2ZKDBwQwnRYKCImGhIWVhEaOQEkdVOiggmMpkBoiBGLBwAUpYunZEhMAZxaUp/zNxNRgwLLJUPsPsAjvAICKoFwXzEIUd0MgQjKJYFRoaACHQOYJhCwBQyRgGUj4NjG40EUAXJSM4w/vUQFcMoIj1tJHmC/6gurpQUQEAwKY3Ty5VChENB4IBuogHaVB5LMRGcUPT4TW6a5MAUicwjxAmEi2I6HiOb1CY8hTEPpJBTOGBrTMVKFkKVTBtYiaPSsJc4ALHzzSwVA5Hwg0ImXj+AbMqnhaI4Bx4FM29L8s2FHoWneARsuwjcNkzEvIB3YgdBcwOg9CTE6BJmMIzdefPgJpGUUqUytNsy3v3iyA3A9o6sxzQMVoP598CZdct6Es07727hRew2lQQAIaY4CHVJilamNiF1asu8zfL8uNNSBA5YBU/U2f74+aEiQOwO5udswU6stbHY+K4nTkOTNtuP5BFMd2QnJeqE4ShWlF9g40Xw0GZHm2IxwL6ktoKEKfP+H+Aj9Y/K5SPnXemnkAYLDBeGTvwIi0xBU9A69Pu3Dpc4E6TVtNlBVi3PMt/8sr8iZnP0cJbC4575VtpJEH/+d6W6m0Ra3hIzqAxx8zHXnXXoO6TiC/YI7TeI1pJHRqBnW48osTl8+j1KPQm+61wzXBngg33e2YYvvp6uVxaOX7HAU9gpumWJtwjAJCLdKy6/vP/743Otcp5fMuwHzE+79v3rXhgJkwXFWix4W6DV7pNnxGsoNqjvN+hpae4lpbBn5qzUHUrg5norO05s3t/PXBnE9nDTlaoH64OpK62rD/sgCZJUtrPKbHDs1mHmde8b69ahTMKsheCfMz0LKjO0C0pggBYXA9XvnY2e8u3R7KZsQEL/wETrqLz5fD8/QFra2jahVbt3lUuWxjTRbaHZ97yZfHxtOq4XvYZmlQ+CKQgPoTm2z4H4QNgSZfaXTZAP0eyXYrunRZdoDpK2Yve/YAC+8zHXnAmkD4hICiP2DPZPzkclLSR4nEp9qrVWmPTzYqI/yK+6wF47AKL9oSbz955RsUlereUUTXqCK29cdvC+fy9kW1wSEvnubD6MMngqXulNzqKuQyVBMjr5L9suLZY6ZxkISApFxl/5rI3YHsUVklZiGQlENNRtpZGcHkCfAVrt3W8e2zeXK6mrZmkWpgZ1+oUHmXDA/JxSqOxGlFFICyDQBJFNox3MB+dFZT9aTBOOr6swQAYYCENqMEQHhDVZqtbgsq6yrdis8SfAY78M8xvx8XedTU5BqqQoJEdsDOYjYAEBEE11PpCwrU43hZsGrkjXtTCEU4N4pUdalkzg4kbok6gECRqJKTk4CZIR+IAybR/7Ma2wDkEtzoFdfu2rJymKrtWrwe8l8gUy4E22cCKRwv5V+La8pjjaL/elpsrNzAP5qfBG9Sg93Id2E1LTbMmEzzG8w5tt2LDwr91/meF93mjSiAEaQNmkGlyB0urAXmDATQv6MUOwYj9fHaWu2dZf3ZHird7v/wwnnnKdbgbzKOcdmcCxtURCU3jv3Oh+Oswnvfu+aYJkPjuqxZn9CCCTUpHg48NRvj+w1lO+WW7PHr+/LqWSzf93zCi6wQ4a6iK2B0V+3zLYHvQ+jzGjRvmJOj5r2DYb8vKxKS+OsiCutCl8MZnEg1cCgm9C73DAbrigoxWe9pQx6gFj3HZw5KIjLCbd3dQdLIbGWH4QsyLel4ASe6uy8eCfU7I/qrjKfvT/+2EjeT4rKzmRVjATnEumURo6jJ/vl4KO6vdG7VmjKRBnHnnsYytN+WuNjPNNtzX6dvszXcXMMKfWg6bnArbMBiiftEK2JhsiywRE8rNJelWh2jtY0u6oUlHZWNEn4yyOO40Gpa6T6doqH/ZqDk1WJA1k+ce33tvKLi/Llbd150wSmZiP2XrwxvEYAIZK8A5HuyD/jcqxHtyHqiIJA+/6tYZX8YNeX81JeCp/0BBhOE82+w1TLtzielQt5aTG/epVxzinVCZaQwcwuSUl05/pudjlLmCRn+Z7Ej1DcLQfbNcopdgjpq7QNvglvDBDL85BfDqZo0ex489Bsu4qA621p8G2e6I64ZHA/SQieBCh58kFG1Ehgv9F7IWc2QJamEVFoV0el5QoPApV31f90cxnjteRZM8uintKkTRux5a6/oC7KyoK8EujOdZPCJlXmhHC8rNMpUkCaoxLzVTXQnT4supeP02lpuxbdVlJZXjHG1//XdDsd6BVIiOKlMl22oPoa6bUUAF4BNR0BYYyYqmoMCJAfcstBeY51LW6g2bsPSBTc+mtqILFVYqqVQqfoKWZb4SsrmKcpTu/xaIkWM6dpdhQPkLB59t9avthiFt5ubetO9mbWESCj8KzlSJlmvs8GKHdtEALSAxsF4nYUugSjbwnx82FztnJLzsry4r8ft98dyOhvtMV7o/w1zRSKIK/Q+965K3lw9s5Gw3txcyr6fvXl/ffPp33FBD3NjpE2unZtCMgQzBWpR1ziBCl7pZkutlsODB5BShWz2eIBPl3S8Tg61x2DByjOkUbG8FH35c+lOJGj6uPrXLNPyCgMAXHhZnY9c8zGiZmt/bB2g/3eLef54KKPQmCPUKjYNpvFL6+/j9V0ql+kGawl58nU1HiiB8KRAPYk8OdS5m0aGRyA9WUGNU6LxTkw7D35gIx+sOMYMZ8pKVRgLmvA9pC/4EuVqyq7ylPMx3pNpwNDZe42jaju3JRSmeWfnemcRrA/YYK2mV3oXCNNm6HB0Z085b/VLRe0zdTgu8WMip7XhxTzfb7C0wAHldWbvEZ1lhEn9C32hnJASJ5tN+3wLmWf8xcWSiXLV494ZnGlT9aHiuJdoDpPVzAOVi3lfYd6uRy4VKRFIz2Wcf48f53zCPe7422AbGh/sCBSXy6r/GX+MZdl5g/QUezxXkZ3KIXXadbK6phLg6c8SbnB6fIV0yOgVxUgsElrNKLQA2aVhbHOroXZWtjbAeQvEP7fRsuqutIKZOVAufaNrRnfWpTlWd2B/QK5Ntf0dms8SYMVZsnjAefY2w+r/0aDuAoRoC2eIYS4Bbf6W+yXyAZ44Nr/qNbNNDGSdR2ixeP5J8kojBn+F7eeGMbZV8PsALfXeMFqXafrvXfne5P2dcf4+Kr4npheLtafurOkP1zSDlxqgKG4Na/lyJLlMXlPoECKjEinLe5kIBwr0L1rJFOhuGR0fb4a1rUQ/O8x5i5AJ0vO1w1Z7d+ffM+2QVwNte5WzLjuyn3L8rdJxEjG7LCf42NWaFUqRnZ/CBLCtiQbGnMqfwg1LiDoKLmGrW8O0Eej/qUvPzFzHrkP1Izj3qAsPGSeDynXl0/tEG3C6RhrpMl1yfN/MXwGDPrNHwQArS6Daia9sySGSnmBYDkhVeK+52BiCSg7z1i9S8KzvOKx3x3A3c7duDl/LG2quHeIm2s/8KcBLFD1rq9ScCOhtoL7dsOxpJycMbAIv/x5Cmevr+kYvHRgy1ywdMFn7aL706JN29b9NZRpC3EhFeQti9dq1D+p3PPqgyaSZQG3jj468bYorSsv+Een/yentf4t1djB5Rk2rPs0r+oBzDCWfnLxkIldQImC6+PAMzlrWwHfcAwZuRhQuDGP8e3LgkvN5nPUuoQEMT7ZtvWXaWPE+9YWnmD8XFIwTfsBw3eCC0jkaAwGzj54iUFesTmGaNGVVrybJYhPYD9xUHwfF461p2OKKNgr9sE0bS09giDIb1wVeUh+Lb8/gQPy77mnJknjJUDgoit+Luk7mUOXbxpzEtNoaX0l6FJVdGpIWALdTqmyOrj1EJn25I/pPxowXjZJcCLy+87f6wkHJe68Y4HF/6jiF7OCVJ4Rr5ZxUmkOlYgJmoN0gg58/0pB1tPeIDrlcQHOGTK2wdRZNl4HnKJENttpdkcIewhQn/lyr4YLHvynj2b3yar61cylzyRrJ+aRFG8jyXX78vWadPtqUHN/gI99PwWpTX9sq72TFlX/OiQqxtm56pH0QK0Z/c3tMs38ogLF+qMowZ1MehdG2+bil17NdiuUDNUkysO5iawh0pg48c8oal3vMOWFgcK0f0/McYYXZfV3Cj1PHVYPvFg4b3Tdew6acGLo37gI540dE0DgTTn6pDGcfFZMXJ4ELTXM5a5ik8h3UK8fCwu303fYsqznslPeNm16vd7VqP/AzoT6oZraA+yfAK6MZmuDQhCcL1xFhZmDY3NUEAnaed/DO5oRjsUnslb/tRV9g9bKdA2XjhR6uIEUtw9y/TWPtNp1QA82ktnfkvIRdFEILg2vx+PSknK8b2M1BjvCMbr5LOfRjiPDo/tmCzeqfJevWHkKG+Y3vtlTdbAGY+KN6LlHvhThSmqLHV2QbLGBdoB44Lh+7L8S38n4bsylJl6sSBVv+32fBXM+r04DS60ovbAJxcYgibTzD0Pbtw8spp3N/SttS29VHFhnZx29tON66ynGUW1ssMZ+LnpZt2EifoMEFu2BR4irsFFQmKF5hnVuGC8xw7Jv3nN133N3405BZDLu4ai1nG3Hb5jxbN8dhfIfjD41OSHyE31AvstlA6f53CAdjKJ3tHy7ReWXY5M4TpCT8QeXQsWghRaLY5St9v83M2MxL4anyOAvosZiPgGsc/4O05hSvZOrtdfXJeqyGJegziIL0opTR2C8h1MP12LKhIBIKjQ7RrgRMqGELjRRxHlO4ZRfY6wgiqextDli2kLE2qt8aGNZLXWzKvxr562IUnumJdPH2/8cT2SYXz9e76KMJfxmupH0DyMdZFKOC+NeFtCvfkRN1Uv6uTn2DzXj8uKzYarx48jEm4CJZY8GXziof4J1QSOvvD5FQ4cqsPVqHL81C2xjev41ue8DGheCI8TOaI8hT0U4hCI4pA2HenALY5j0bDj4++l40Qo1sKrIFUpptFKT554iFmkibJ744NhVzwXV1TRz+Ni4eqT2qJz3LsUlZBlnE2wmJBZ5A/vkP5TF5aFxUfyXBcfr4sLsMHFpCJfjXXnjL+r2/KgZrL7xq/p6lYJ9e7aVGxeL0TkabNMft2Ab3miJEjjhrvfiOg77H6+STeAgcfD9nH7Hdg5T39u+PY3TYyulDwQehiq4grj5fPXhhuF6K9yxbcaud0xspefBW//n/aHcmAA1ewT61FNi8JaJk+GdrHfnVoQ6g1dQpOdstOMJmvFYvL+4h4S7Fg3YzSyTX3rVGo0gJo/H/Mkw9E6BgMl+QnQ2ExMHL/b1DJoTaCH6pJa+i/EzAyzJg4RtOWa0fnlZYVIYgoLCdxX/yLiqlOnYOVJ88zpk+2wlEPmvjZWWUVtkhWuuuTwELyDe7rxzPY+mf/iTY4U6vJRCvkjizVUABENx9/aAI5lIBsylfnZkYf11g7kHIPDt1IQ9mFdhuDxTem28N5crt7ClqRvEqU6QH9NYB5b8fvuDREbc1p/HYljjl2nmerXbtkEBjBTGHP6/I4AiEeGsuFScUPLI7sRpTlXNOVe8wh2C3R3LwXbRlQLjQq3H/W1kBlqCoKb3o43WsRng6XhIbnq4ZHTv2W4jVTcJN9vnFSqcvodgPApneV50MY7P+YpUODDxbTiJD1IJmYiLPY4psXAsWqbKsIU2W0Afizd+A+riU3BE0p9CmHTKpY0rlsI9WMRMpLhSfsAtC4cQo2z/Fkr7OGgHI7nK9fCrQC55MCPHZ+2nO6mO379h+zWwQhKmLu9fGnEcnwal/CZZY0duGU389hzOd+2ECwieCYZWxrrYdd5Rz4bha2EppAbYE/hGeecsAIdhooBZXMLUZtrts/AUKF6u9fOJZ7nF1G0y/CwNJy2TYKMRAl0/LRsrd6b6QfpDF2yTa2wnNfIt5QMVh2zMScw45HdI+FhWCq5hnMp+Df+EKSt3KtundVgZFjE3bNzbtDesqkv8pqjWQxbpkMwN9zeRtBtLIEWim4v0xI+MxEED3t6ZBBGrpg9VgLX1tbWTyb8w7M3E7XhQLITiMwmGnum/WEYKbrjJxsPyhdz9vDpUMRmvXochwLPluPLAwqFVvst/DRZxwi2mxH2qt6RI2oYuajtYYjtMNbCxBxVknreCAm3ZyNu6MKM/h40lCBdZ2OQd5q55RJZqPqjnUhYGBG2dOhtCRcIRLmwAWiPrgsrf64lk5KCGJP572KfZdX20Lkmo9THlT4jQqgeZygQsLEdeeB42NFs48bOwcdNHmj5dOj/r3f+Hk16k+umyb+MYdfLubsD2P5uhmjimInXYsS3J+tHLBMnB/Ku39AGsVPJx/5LvS7kjbvZIbbHKc/Kg9uzW3aAv752JfpCXZaL5E2P3C78WqT1eWQ9rZxpbyjrNmCb2vuNnr8dpcjv8oq11sm4wSUZxgdp5D3uK+PFwaEebVDZyaLaP945BPmQkJVFQt7cshWefGZG5wUpVoHv86XyHMYLk54hicYSD/397cQgmdwtV7u5s548JoZhffmamgRs4xtgPf/lnCaz7W3iSPqCA5EBpfQO8EL2gYs6c79e/uvNZwU/ZuE54MeP8/QR05UEZ5ngUDXfLX9tqSDRmLfS7rt3GKJTrPbQoDpykIjLSOd1U6YpZNd5B0FoYtaMwnBWMlF5cMyMxS9Jlwi5z0qdEauuLb9sh4Xp5JOrMlJxwK1rrmXAo4C60IelFnmZgn7lOHaHQhZpOTXFaRPGBpcASsil2dQWmLLI/tljnvzTtRVHyrWFXbksyHlM3TJ+7OzghT4NsbIzXBeiXLe/iXKAz4mVGpi+EwE6GrcUZIRojh2vxZ7TJ8s97f3QRfjge1790SZruMZ0Ic0/M1yfxr7qDGw2yZSo5OTtg8Z6QoYnLpgC5oOCCKKNSm0e08HodGO96zajzNJJLdTGfaV9161LK54DYEowheFFCJ1+siG4PrVM/hwNVtfwDoXu9/AIJobZCItAfjX+valDXneOoZjsqWvRr+uXXRf2w54Lf6CwfTWo+4yYBt5tS3bg5YCyFhBRQmon/E7kLJ7c8zfaKHXgw0ftIQGzURccSpaL5Cxng5pEjBQEIKSVyFu40jTmTMVsJahn/i9M4jmbtkk132VPytMw9co69S4QL3sQhWmkcTnjPnqu+pCHwy/XAr5V3K48xDhQRA7kNRwahyi6lrxOtUw04JaUr2fii/f2oEQzUnyy0ogn/+585yIobuHX4tRAgGBqfI/dH1sIcWvAbPAl5uC4AwFTb7QEA4PbmJx1mfu1YH69TAODBAuo/B6EbJjH68rkVEPxeDks+4wwA0IfLDHGYNVjDpOtn3MrJcgMPHmiXEB6NAPCXV0Xc0rDLmc+8T1YWxwRE88KHnDnnx8dr9DhcTQF1KwRHBQGl8kjAb9byTMohR1BKnlEApaNci0WLyWu2EY/jl/+/VtbCwSWAMvjvT7QG5sJJh4BWAgCg0sTaCsL2qxWM6oAVwslsKwylfCusUEpA/agAMGpVIE+lMsVqVGvkKl6REk0q5amX/JLqNVDaLyzjyY3HUApi5vnb1CrV8Em8wavAIYhX234cyd2R8WRDN0hgFiNIiO9XfBb0JdVqI1AmYWlB4CCAY0lePDqoH8indWWiUCjn5mnh5pJWJWrLxLFhC4qKFEGzQ6EUgvxr9PMrLA==); }</style></defs><g transform="translate(10 247.12272922487136) rotate(0 238.27987670898438 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#be4bdb" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did:plc:fpruhuo22xkm5o7ttr2ktxdo</text></g><g transform="translate(75.72216484801538 404.6572853658249) rotate(0 177.87950917423586 14.343277021703443)"><text x="0" y="20.218283289793156" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="22.94924323472549px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">morel.us-east.host.bsky.network</text></g><g transform="translate(155.72833473598075 75.82628657535406) rotate(0 93.27763366699219 17.464019532254042)"><text x="0" y="24.617281932665158" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="27.94243125160631px" fill="#be4bdb" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">@danabra.mov</text></g><g stroke-linecap="round"><g transform="translate(240.23472287951154 121.6640319594876) rotate(0 0.9435019037955499 59.709888324233816)"><path d="M-1.31 -2.43 C-0.52 18.2, 2.06 101.12, 2.39 121.43 M3.18 2.42 C3.85 22.5, 1.26 97.5, 0.43 117.51" stroke="#be4bdb" stroke-width="2" fill="none"></path></g><g transform="translate(240.23472287951154 121.6640319594876) rotate(0 0.9435019037955499 59.709888324233816)"><path d="M6.25 21.14 C7.6 14.84, 4.14 11.46, -2.48 -2.6 M8.63 21.38 C4.68 12.91, 2.25 4.1, -1.65 -3.27" stroke="#be4bdb" stroke-width="2" fill="none"></path></g><g transform="translate(240.23472287951154 121.6640319594876) rotate(0 0.9435019037955499 59.709888324233816)"><path d="M-10.85 21.68 C-5.78 15.34, -5.53 11.85, -2.48 -2.6 M-8.46 21.92 C-6.53 13.21, -3.08 4.22, -1.65 -3.27" stroke="#be4bdb" stroke-width="2" fill="none"></path></g><g transform="translate(240.23472287951154 121.6640319594876) rotate(0 0.9435019037955499 59.709888324233816)"><path d="M-9 94.09 C-4.36 98.06, -4.13 104.88, -0.74 117.34 M-6.61 94.33 C-4.61 101.98, -1.19 109.35, 0.09 116.67" stroke="#be4bdb" stroke-width="2" fill="none"></path></g><g transform="translate(240.23472287951154 121.6640319594876) rotate(0 0.9435019037955499 59.709888324233816)"><path d="M8.09 94.71 C9.03 98.47, 5.55 105.15, -0.74 117.34 M10.48 94.95 C6.6 102.43, 4.13 109.59, 0.09 116.67" stroke="#be4bdb" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(46.714900744929764 20.781412310127962) rotate(0 90.5606091720565 12.538858845899313)"><text x="0" y="17.67477542917971" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20.062174153438946px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">handle (swappable)</text></g><g transform="translate(280.5523913537145 459.8527230548657) rotate(0 90.8301866196598 11.852831541802743)"><text x="0" y="16.70775134132511" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="18.964530466884348px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">hosting (swappable)</text></g><g stroke-linecap="round" transform="translate(35.29959978171246 10) rotate(0 216.96939798218773 77.26585604552133)"><path d="M-0.88 0 C93.35 -4.38, 184.9 -2.28, 434.42 0.61 M434.29 2.87 C432.89 55.05, 432.43 112.41, 433.56 151.69 M435.26 156.38 C325.71 157.49, 212.69 158.45, 0.88 154.37 M0.12 157.12 C3.76 91.88, -2.12 31.94, -0.06 -0.63" stroke="#1e1e1e" stroke-width="2.5" fill="none" stroke-dasharray="8 10"></path></g><g stroke-linecap="round" transform="translate(36.23059972148258 327.0258694295853) rotate(0 218.34303324080247 83.10126449239306)"><path d="M0 0.8 C137.73 1.72, 278.72 2.37, 437.29 1.32 M439.56 -2.91 C435.14 56.5, 436.08 119.36, 433.84 164.34 M438.52 167.94 C271.86 167.79, 107.71 169.95, -0.16 168.08 M2.59 168.87 C-4.13 119.02, -3.39 78.21, -0.63 3.24" stroke="#1e1e1e" stroke-width="2.5" fill="none" stroke-dasharray="8 10"></path></g><g stroke-linecap="round"><g transform="translate(246.00135796202085 293.77178846214974) rotate(0 -0.7708713597658061 51.108103815570075)"><path d="M1.59 -2.35 C-1.8 39.78, -1.09 77.2, 1.07 104.62 M-1.45 -0.69 C-1.96 29.96, -0.92 58.93, -2.47 103.78" stroke="#be4bdb" stroke-width="2" fill="none"></path></g><g transform="translate(246.00135796202085 293.77178846214974) rotate(0 -0.7708713597658061 51.108103815570075)"><path d="M-9.71 78.93 C-7.79 89.79, -4.17 98.3, -1.17 104.98 M-11.23 79.76 C-8.95 86.92, -5.84 93.31, -2.94 104.56" stroke="#be4bdb" stroke-width="2" fill="none"></path></g><g transform="translate(246.00135796202085 293.77178846214974) rotate(0 -0.7708713597658061 51.108103815570075)"><path d="M7.39 79.31 C2.95 90.03, 0.22 98.4, -1.17 104.98 M5.87 80.13 C3.1 87.31, 1.17 93.59, -2.94 104.56" stroke="#be4bdb" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(210.00747193586722 191.96491679041037) rotate(0 10.854095458984375 9.337157556834654)"><text x="0" y="13.161657292114118" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="14.939452090935434px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did</text></g><g transform="translate(256.5670260475963 345.9010269345822) rotate(0 56.73396301269531 9.337157556834654)"><text x="0" y="13.161657292114118" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="14.939452090935434px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">serviceEndpoint</text></g><g stroke-opacity="0.8" fill-opacity="0.8" transform="translate(264.4970007404554 139.5862672733324) rotate(0 45.536555783690346 9.345888896835959)"><text x="0" y="13.17396498897997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="14.953422234937536px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">alsoKnownAs</text></g></svg>
+4
public/where-its-at/2-full.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 686.381193160727 134.36299530115593" width="1372.762386321454" height="268.72599060231187"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2dWXNcXMeRhd/1K1x1MDAxNJxX66r2xW/WrvFcIlny2JYmXHUwMDFjXHUwMDBlkGySMLHQXHUwMDAwKFJS6L/P+VxuJFx1MDAxYahLsarRVJhcdTAwMTFcdTAwMWHbo7HR6ETdqlxcTmadzPvjO+++e+fi+yebO799987m+b2Do8P7Z1x1MDAwN8/u/Iaff7c5Oz88PdFHrv3v89OnZ/fabz66uHhy/tv3399+Y7l3enz5rc3R5nhzcnGu3/tf/e933/2x/VOfXHUwMDFj3ue7X93/5tnT55u/n/3uz395+MWfjr44Pvnm8lx1MDAwZrZfermYi83zi+1Pn7NcYp+jW6LJJuRYTFxi6erj7/Xxe946t7iS9O/oQvDGXX3+7PD+xSP9jo1mMc7XYE0wxlezXHUwMDE18Whz+PDRhX7Hx6ufXHUwMDFknDw8YjHm6ifnXHUwMDE3Z6ePN1x1MDAxZp5cdTAwMWWdnrHI/3Ku3N2k7TrvXHUwMDFl3Hv88Oz06cn97e/cdVx1MDAwZtzdu9vfeXB4dPT1xfdN8p17Z6fn5+89Ori49+hO91f+9nLR3c+vvnt+qr3ffkt/9uGjk835+ctcdTAwMDNrPz19cnDv8IJccrJm+1x1MDAxY6zxyef32yH9Y7uys4Pjzeec0snTo6OrXHUwMDFmXHUwMDFmntzfcFx1MDAwMHfufv7w4sbfO7n/4u/d+P3zzVx1MDAwNlx1MDAxOTablHJcdTAwMGVXP9/qU6i1/+mfTk+abtlcdTAwMTCdMa647dFcdTAwMWOefySlumhCXHUwMDFmXHUwMDFjXHUwMDFjnW+2281cdTAwMTI+3ircjWU8fXL/4OLFSmL10dtSqtsu5+jw5HH/naPTe49f8XeaMqKUjzbvPjg9Oz64tlxyXHUwMDBmTk8uvj78gbW7cuOnn1x1MDAxY1x1MDAxY1x1MDAxZlx1MDAxZbHz8Yag31x1MDAxZFx1MDAxZD5kXHUwMDEz7lx1MDAxY21cdTAwMWVcXJOjfbg4lDldfXxx+mT76T3JOzg82Zytj+f07PDh4cnB0V9es8iDp1x1MDAxN6dfbc4vl3lx9nRzfVx1MDAxNzafvdR9u7jYPvjpN6803Sfpn38+Pf5cIj7+JP3hq5yePfzoTz+sTffBmVx1MDAxNOClXHUwMDE3aZ9cXJpvjGUxPrpYrFx1MDAwZsFsd+XSfI2MMyRcdTAwMWJzcqV6W7efX5mv9zLf4KqpsSbr8tp6XV2S86FcdTAwMDZcdTAwMWZNLSan7XmPrTnXaFx1MDAxZbhfjzU/+utcdTAwMGXWzJFZXHUwMDEzs7musS/tOfy8PcdcdTAwMThTrDVvbeM/b89PTlx1MDAwZq9HKP61/W/vbjWk/Y+r//6P34x/+z2zWF+qNClF77Vj0uddXHUwMDA1hFxcXHUwMDAzqqhcdTAwMTjlsvE7fl82XFyNrT55XHUwMDFitUPe+V2/X0JcdHLAttRcdTAwMTCyLXG373tcdTAwMTlxLTGFYm1O0dndvlx1MDAxZZeks3U1On3XXHUwMDA3XHUwMDFmdvzzebFGf7tIW3NcZk5uZLfvl0XfSpJhY8nB77j6ukgvvVx1MDAwYll7UE3wZce91+FcdTAwMDdtuzZQKEeKtOOftzjwmpK1tkjx4u5fLzaXKiOXr/a+puH3XHUwMDE3ryhvnc3BOaGuLXi6XHUwMDE06PR5soJi1Vx1MDAxYlOKXHUwMDFi78dSXHUwMDA1XHUwMDE5Sk1y8bakbFZcdTAwMDKlW9pgXHUwMDEzg+Cby8NcdTAwMTXapdlh1oaWKHVO+aZAr1x1MDAxMytaetSHXG5OYSTPLUbRSVx1MDAxYeKlW4ovvlx1MDAxN5c9m69ccjQ2u+Hy/FKr1faUqG85KU73vGFRPExcdTAwMDKr2pWUSlx1MDAxZJ5oXFzkdFLWUWiVgqfd+uJiQlx1MDAxNTKTR05CWWboXHUwMDFj4lwidYh6UmOSdMLVXp7zKet0LcLqSFqRdYbigp7XZNvJSlqbk9GxQFluXHUwMDFhKktdoo0h5Zh1XHUwMDE4MW6h5kt5Llfrq9JcdTAwMDZcdTAwMWSIXHUwMDFmOlx1MDAwMllf1cFa7Z9cdTAwMTVYSbZcdTAwMTcng5JGuix4oVx1MDAxZFx1MDAxZYpzSypywHJcItJcdTAwMDVvirspLy/y8FZbIT33aWxr2uuSnCNcYidcdTAwMTSs9uJCdfhcdTAwMGUnw41lqMj6QtJBSPV1JCmELeC+pTz5vlwivKXIXHUwMDEz0OWVuFx1MDAxY6LPgVx1MDAxZCzJjOU5Yb1cdTAwMTClVsKLbFL0+1xujIut0lYnXHUwMDA3X1x1MDAxNZtcXNhXYJJtRP2uXHUwMDBlT+E+7v3EZclyv1aOyjjnzb5cdTAwMGbsjVx1MDAxNCayiT4kWW6K+1xuXGZLzsIlPmZcdTAwMDFcdTAwMTP5mX3lJTnnLFlylJJZ9j1cdTAwMTBfZHGhJmwkZyvj2lNg0Fx1MDAwZeLnkyNcdTAwMWFJXHUwMDBi9z3iYFx1MDAxNznzSFx1MDAwMcI4hc3Sy6syOilAXCLHSWPsXHUwMDEyXHUwMDE0b0pWXFz1RYYqR7064l1cdTAwMDXGRSpcdTAwMWRcdTAwMTVcdTAwMGZ9kffsnUxZtLdJyMaA6etcdTAwMTibXHUwMDA22YhcXKq2z6XgfO9cdTAwMTV2l1dcdTAwMDVcdTAwMTBCXHUwMDE2ykrF6Gx6ccVoXHUwMDFiXHUwMDA0fGMh01x1MDAxY55H9ItcdTAwMTVQ1VkoW1S62Gm0XHUwMDAyjKBcXI5Fa1x1MDAxNyBccsPlRcVrcCNcYk6Zqu9Vuuq40Fx1MDAxN6v9dVr/UF5apH9BcYTQI4BwQ5wziytcdTAwMDI2qFJcdTAwMTbcXHUwMDE4L09cbit51lxuXHUwMDFkNIDfi1x1MDAxM1iT48k+aJXj+Fx1MDAxYqtihH41Klx1MDAxZFx1MDAwYkThm+JcdTAwMDRGXHUwMDA1RYv+ktPfSsOAmYQmjfJcdTAwMTXFXHUwMDFiPKBCUy8v8KxJpiigIGVcdTAwMWFcbrRyqFxuSlGGm1x1MDAwNJ96cVWnXHUwMDFhmyPLZVx1MDAxOH6TVMXJl1x1MDAxNuFcZv168jeP1rklKM7rjylg2jCG3kmq4nWq1ibQsbe9OD2nVLhoP1x1MDAxNEzHlpuEJ7ywpOB/NDj8m/LkKJSVetlgVn6axkA3XHSrXHRog6tKVTp201W5IOAnXHUwMDFiS1abWOw4S0t1UYaQpcxVzlRcbtOLk3qT8ERcInNcdTAwMWVcdTAwMDLJbPS0siNcdMu1eNtcdTAwMTmGYK6Al1Ytq/Fyf1x1MDAxM+KEcZVrszKhZtuLXHUwMDBiXHUwMDEx96XwbMJMTpqBktJ7XHUwMDFkbvGYZy8vy1x1MDAxOVx1MDAwYotogUKGY7eXmy1cdTAwMDVcdTAwMTOyYqW2u5OXdFSCzMpklK/bPHQrXHUwMDEyXHUwMDE3S3NEOkFF4NSLS15ZTFZu50xcdTAwMTJcdTAwMDSckCfzXHUwMDBm8uGmWFx1MDAxOa7v5dUsTcneRCf7XHUwMDE4qt5AnFJ8uT15MflQebM3IE/uWs+rXHUwMDA0lSw17vu4WVFIgFVRxWtcdTAwMDfNOMdcdTAwMWHKk70qV9DheqpcImNtfr28XCI3qyBcdTAwMWZcdTAwMTQrhezd0I1KnFx1MDAxMI7WoFNUslx1MDAxZmIvTr5Vy5M2yetUOyNPaZZcdTAwMTRZUVC5R8i9vGJk2CUq+XAmTFx1MDAxOIeMV7E7y79cYvxF18lTXmLlXHUwMDAz5Fx1MDAxNqNJgoXj45BvIT8tQSlq6CpcdTAwMTBcdTAwMTJcdTAwMTewaSMnSy167EeFWJTfg89cdTAwMTTXfJfGrOvVXHUwMDEz8rQz4L1Squ2T3lvJk0ezUpVglOf1vuA28oxyQMFNo3XqS/vKU1ZcdTAwMTS91leFsV2H+G4lTnpcXGu2cvMp9YjqNvKcNs1IV1x1MDAxM8a09/ZcdTAwMDHZs8J4qJYq5t7bl5s5ySOFQuT4hcWFXCLoI3wkL59yXHUwMDFjI7SBOFx1MDAwMTOhN3lcdTAwMTYl7DFcci13JM44hT2hUXkrX8f49vXiwEdan2BIXHUwMDE1YnEzXHUwMDAw7TXS8lx1MDAxMskzXHUwMDFjQaiavLc0Y6uOVTmcUYJap9RcdTAwMThQLbyiXHUwMDE0X9vUXHUwMDAzXHUwMDE26lx1MDAxYkrUk/CAMWM0Klx1MDAxZmpxUSYoqeMqYoVuvWt5gdSEcuREzDBJx2eF0Fx1MDAxNFQ7XHUwMDE33+Kdk1x1MDAwZUdPXCI5ToQkrlL39WSfipRcdTAwMWTeU9pfhFx1MDAxY4XRtD5cYlx1MDAwN0N5blHA93JcdTAwMDJSff1nladcdTAwMTlcdTAwMDVOeVhcbr0xjCOQX1Ih4kblJiV1j6ssUqBR26DD0kNP3DBloXXB9FwixG4jqLRPm2Vk1Vx1MDAwNWlcZlx1MDAxNjK+McpxkepcdTAwMDVcdTAwMDKqoHcyqzScsG5wY83FXHUwMDBlxUldneHCRUmp7UvgebmsXHUwMDBl+VY4mcDeslxmV7h1XHUwMDE0YOrRXHUwMDA1VVdcdTAwMTlZzESTKU1R3lwiUFOrpYxcdTAwMWPWNWasT7FO2lSM0rmRvGJcdTAwMDSXalx0VlahI1x0q1wiM8WhZKyMzGhrh0dRvLJiq9+snpjWZVx1MDAxYZKnj/RvaTrKOVaVQkXfXHUwMDE3XHUwMDAxs+qVn9SuYFx1MDAxM5dcdTAwMTbstDYv0DBxXHUwMDFhReBfiqzNU/LOhd6+8pqyOlRcdTAwMTU011x1MDAxN5TkqFx1MDAxY5fDucYsXHUwMDE3P04kq8CjXHUwMDAyXHUwMDAy7IRA0tOLU0qov1x1MDAxMlty6tNw+ypcdTAwMTQkakbyUVx1MDAwMn0x7ysvXGKdXHRcZlvsU4lk6OVxTlVgNctVxTA0tSpcdTAwMDXLVW7e5aTcwHRcdTAwMDXIsJQgn8hcdTAwMWHl5/NYnauOw6VcdTAwMTBNTFx1MDAwMlSxLzHvLE96L43wiTtoXHUwMDA1/Nrhs93vw5Th4lxcZMFcdTAwMTbQ7nLtXHUwMDA1Kjdw1E65M1OWMFx1MDAxNqhNT9xHoK8p+bS3QFx1MDAwMZNMPlsouypcdTAwMWS/KZDKjVeyVPBcZtKs8U2MVfCl/J3ZKuG62Fx1MDAwYuRGr2SnyJYo9o1cdTAwMDW6JVPlM9xrW99V0iTQVSt9J+tVXHUwMDBllsY3bdqlQNkmXGJMxNR7fG5+dGLNo1KBXHUwMDFhOkGBQ/lNXHUwMDBieoI/1sMhLV9BzVx1MDAxNiC/TYLpQ69giWExsvFcdTAwMDLiQa6kXHUwMDE3yEkpK+fuVpBp4omLsnhwLHXuXHUwMDFjOoAlgYZcdTAwMWKGwO1AcnZcZthku/IlQVHRUlx1MDAwYlF2eVMgXHUwMDE1XHUwMDFlm7zisFx1MDAwMHl046xcdTAwMTf2ZGmXXHSy1Eb+uClQdqm0TnpPPd9O1KysXHUwMDAzxWSKtUqMhLFWXHUwMDAyXHUwMDEzjkh/N1x1MDAwMVx1MDAxMcdlXHUwMDE369BcdTAwMWHqkbJkl71fXHR0clxigrFcdTAwMDVfM1x1MDAwZSXSVVxuzjVcdTAwMGKaRZyD7+VcdTAwMTmCk1B2piY58cCKZjIs4e6EX+jOuC7aV6e4pT+VXHUwMDAws2N5iZpyIYJcdTAwMGKvuK6kW1x1MDAxN/lTmbHiOnyhYTaDOGXSisZO6XmMXWyqUidcdTAwMTlcXKD8Llxcacc2QuHMU0BU4FH06a7zK0Ysi8zc9suZT2xfXlx1MDAxNJTgYSQvXbvpXHUwMDE0XHUwMDE0Z7hQXHUwMDEx1NGJlJnrciVoXHUwMDAxLlx1MDAwMfVpeVbbieNOKin14KarjIlcdTAwMWGWOlKR6UbIXHUwMDFmLnQ3O1x1MDAwMqk4XHUwMDAzOFx1MDAxMi6lOL4pQlx1MDAxZaVcXGo5VMG6XGZJqMlaiFx1MDAxYVxcwqc8JmEpui3yv1lcdTAwMWIkROA6601cdTAwMGLuu2Lf8mfFjVx1MDAwZlfiXHUwMDAyalx1MDAwMlx1MDAwM8zLXHUwMDBlOnFcXJnYXGJZXGJiyFj3vNXugfFzgv/ZwcCoXHUwMDAwKHBcdTAwMWG4qFx1MDAxNiYvXHUwMDEzy5N/i3rUrOih795UvaBoXHUwMDE1IDEpXGKDPcaWJnGQXHShXHUwMDA1JH3NdeJkXHUwMDExXjrHZVx1MDAwMoF6LFx1MDAwZXeubMtcdTAwMTF8XHUwMDA1VmInT1x1MDAxZjrjvUBcZkT2iadVvHGEfimCUTJ9c31cdTAwMDJcYk5equJcZrhcdTAwMGKeQFxiXHUwMDEy6CEtSF3lm30nzstgfDLQXHUwMDE4ZThj05A0eVx1MDAwMFx1MDAxOZtcdTAwMDKDl46lTp7Cbo0lNp5QXHUwMDFkU+qQXHUwMDA379BHMIC+1JVMXHUwMDE1iIB/lbVcdTAwMGJTTphcdTAwMDaRzaJdXHUwMDAyV9rDXlx1MDAxZaeqmOyVXHUwMDEy+nH5eigucGWrpFFcdTAwMDEqTz3uz7CaX4iT71x1MDAxN8LmL1x1MDAxNunMOCNcdTAwMWPJs1xutbArcfMhTcTx18qzpHeyXHUwMDBiU4pcdTAwMDJBmOB7sn9KyzFOfSOn/tp4oUriXHUwMDE0IZUmXHRcdTAwMWWOiy9Yb1x1MDAwMppcYu8otFmTen2Rh9XJerlbuYopb3A7ee90cu88Oducnz/VP25Ss89cdTAwMGaPn1x1MDAxZVx1MDAxZFxcbL588fGKlH9wfvHh6fHx4cXF5v6XkJVvcJVft8CfXd7l4n66/M1cdTAwMWZf/qVLYv9/f/jVt+dcdTAwMWY9/vM35UvzySdfXHUwMDFjfvqv+vHHcz05kPqp+Fx1MDAwMuHC9t70qiVHoUJJSFx1MDAwNOyDUq8+v+L0K1x1MDAxNZBccitdT63uXHUwMDEx923J+bWR+Fx1MDAwZnch8UM7sVx1MDAxNJivPtmS+K+ldT2Jn4BAXHUwMDE1523i8F/vyXl6vjl7yztybi7xzfTj3Hv+P3/7/vzkj4df3j8r6bz8+9E/P/jjfD+OsK9QJqlcdTAwMWW3ddtDeNGOXHUwMDEzlH3Ae1x1MDAwMqgoXHUwMDA2bT9/abrK6YQ86HAwXHUwMDE078qWlXDNdMNSuLr25I35esPd/zfXXZ7k1pL/tYslXHUwMDBikFx1MDAxYcf906sseUtcdTAwMDNcXLXjyMEq9l/r4vnPW/JcdTAwMWLtxqFcdTAwMDFH4cYpj1x1MDAxNH7kXG6sxDcqQJ+7XGYz0lLca4cwRChcdTAwMTMyXHUwMDFimlx1MDAwNe60jGFcZuGHXCKVINlsueWi9uUmuJVDkdzaKUo4hXpTYMqPe2tcdTAwMTZSMEtcdTAwMWJOgbTu+1RDSVx1MDAwM0zDXHUwMDFhbJ3IWyA0+yp/JTCRbOwp0iRWXrqdolCJkUJcdTAwMGWR3uKUPkLAMlx1MDAxMGR7Srh8V+KOJjeKt4xtLK9kbm91htW1+7QuzVx1MDAxNU5cIodcdTAwMGLQVfK4TqrUhOqj3GaGfmpKXySwUFuipVx1MDAwMVwicbc4XHUwMDEySLdcdTAwMGKltmhcdTAwMDRcdTAwMGVho/Rlglx1MDAxY0xU4mdLblx1MDAxNfYxX3jhVtbXqvw4lFpulrzyQlx1MDAxZlx1MDAxY/fUUVx1MDAxMFwij3MrdohKOC3AdND1NaBkQ2jdLVx08s2Ys7k0ojDFx5Iohd5cdTAwMTBXZciOOq73rfA5llx1MDAwNofaSE28ku3Ql7tcdTAwMTLkUEtBtjr9rZG0LPtMPG0s2rhqu7RcbstcdI2PXHUwMDE4dbphnLVoc1x1MDAxY1x1MDAxNfWWXHUwMDE2uJ6bb1x1MDAxN7lcdTAwMTbn6Vx1MDAwZYP9MO6bqqiCVMtcdTAwMTlffO6vPa0+pUBcdTAwMTApXHUwMDE1ZjO+v+d5ampkwlxisbArKFmlpbZcdTAwMWHoXGbKczHEXHSBJeI0ssKZkSr7XmColr9GsVx1MDAxZJ8xXHUwMDE0aJfAXHUwMDFkXHUwMDA2NyMlxLIq0KPgWX9KhqvMdOJOYqFqXHUwMDA0RUzmXHUwMDE0Y+q20C9CYLDUa8Hn24n6rTRGT1x1MDAwM2lcdTAwMGViq+3lKVx1MDAxMZRcdTAwMWZgLypcXNWhPKX1OaZEq0RR+tX3r/hFkvQxXHUwMDFiol+cqFfrXHUwMDFiUlx1MDAxOE9pi0bu9WVgot+Jeye5XHUwMDA148eFpbC0XHUwMDAyj3FVXHUwMDFl03Z8cHrZ5Fx1MDAwZVx1MDAxNGBcYkZ1fCCRXHUwMDEyZKZTjFtcdTAwMTbbX8HERefqYG9nXG6bXHUwMDEz12JZ/lx1MDAxN7+n50k2pa6Ea+UvqF5FuSBcdTAwMTY6UVx1MDAxM66Lpb1MT8plUFnRNKi2QzGXp8HrT8irlOQqt1xy+NTSXHUwMDBi1NaR1ukjXHUwMDFhXHUwMDE2hlx1MDAwYqRDoKSsOO5oMF05rbr4XHUwMDA2iYLghp3YQedgXHUwMDA2e2hAUmzzXG7eXGasXHUwMDAxk+ih46ptKI92XHUwMDE4UFx1MDAwZnXr1F98Slx1MDAxYz3FkmaK/MJEqc9cdFx1MDAwMaD5gJfsyop1ZIR4XHUwMDE1pcBcdTAwMWLUoofipGJcdTAwMDVcbj51ZEGUulx1MDAxMihcdTAwMWbp4VxcNzr3uNTHjVx1MDAwZd5cYt6mzMr2pUi5aVx1MDAwZmvc0I8nS1x1MDAxZa9QUUlu1Vx0xVBv9qWXXHUwMDA3Q0j6Llx1MDAxZi20P1x1MDAwNr6Qg+VNXHUwMDE1YVxyLVJcdTAwMWRe0/HTQ1x1MDAxMFx1MDAwMGFV6HBYOmxcdTAwMDTD1EpJXG5cItH1TVx08pAwa2rTwjDuOPFEJcRQ2k+u74FEPVx1MDAxNeO0hZTZXFxcdTAwMThfUbZrjlx1MDAxMumfUVx1MDAxMJOldFxu7SluKip47V5cZmPOoFdcXFR27lx1MDAxYlx1MDAwM0qKmHtxiVwic2x0ZVx1MDAxOdDwfCnlXG4jcy1cZlx1MDAwMu2p11x1MDAwMtCekVx1MDAwMVx1MDAxNmLXWFuU3OvckiyKNinbU4fDXHUwMDAy3ZKSOjz0cUDykUpgaOBcbo9cdTAwMWF7cVx0XHUwMDBlt0zYcHE2XHUwMDBlXHUwMDFmtFx1MDAxNzouYbKcm/N+JU9xjS5pK6lcdTAwMTArhvKK8iP6y3xcdTAwMDZ3dKxcdTAwMDV6TuiCXHUwMDE3lGNS0cTyXG7NeeSPRf9POH9PeTQrRrSuXHUwMDFh1Ksjuuwuzspb6pdcdTAwMDMxQlx1MDAxZXVfccplYiPEyNxKcL2y7CzPL8JcdTAwMTDFIUpBrifm7S5P6tUuIKlk5Vd07OwoTrqsNcGnjIWO6D3FKZOC/0jPIGSq/kpiZ3lKV2pS7HA0XHUwMDAxRbui++4mLjpcdTAwMTLpXHUwMDAyTS0pRXJ7Pm30RFxyyiCFe7RVXHUwMDE5Ymd5qeVcboYkWrm+79JyeYJYYXlANa9hXGZbYlkgeEV9XHUwMDA3klVY9bIpl4FcdTAwMTGafGtVXHUwMDFkOpbEVLLoXGL54M7O0rwyXHUwMDBm0Fx1MDAxMZg6xDF3KXlaeMlshWOBx704ryzMtlZBcokxRz8s3MSSXGI2tvRKXHUwMDFlnamCuYKtOrAxR1x1MDAxZkoqVKfoXHUwMDFiXHUwMDFjX7VRXG5V5tZcdTAwMDeqdHGig4Ce71x1MDAwNMVQz1x1MDAxYVxcryo3ieZcdTAwMTMk/dbSXG7nTFl87m/uafL01lx1MDAxYpZfqZ/P8NapgEnluIrzZVx1MDAwNVgsvPGSoDNBu1x1MDAxZsrTXHUwMDAzJW754ZOnXHUwMDFjX9HTmltB30n9JvLyrCjIeIpG4uFSvZdHmzX1O8fAk/Hy4lJpy5bysYu9rijFTo314KVGcaJGUlx1MDAxNlx1MDAxM9tcdTAwMDCp4JVC91x1MDAxMdxcdTAwMDKOWoddjZB5xk9b9bSKXHUwMDE5ynbI5Hu3XHUwMDA3I01ONjvtb6RaMZJXXHUwMDA0YPWsjvjIzJB19kK7LbcrXFzTXHUwMDBlXHUwMDFmtzA9RmlcdTAwMDZdL/Ly6+VcdTAwMTlaXGJcdTAwMDJFoVx1MDAxMMZur8RGl6vwRlx1MDAwM2NN+mQtXHUwMDFheMiVKonUZVxcYaJcdTAwMDCmiFFcdTAwMTjPXHUwMDE0Qyq9PFx1MDAwN1fIXHUwMDFibnrGtIdcIkvTmsBgXHUwMDAwMN+lkmWBLkT1ky7KieSqXHUwMDFhviG0lyz9XSb08lp3XHJtXHUwMDEzNFx1MDAxM4z7n6tsXHSSR4SSq0daTUKApKzcWZjLTLB8q/yuji43Y4LHsae4QO2Bq1x1MDAxN0pcdTAwMWamr93sLC4uXHUwMDBlkolcdTAwMWNcdTAwMWJoz60rXHUwMDBmu4lLi/xn0+Igu417SsvSXHUwMDE0mEPK6opdlVl2XHUwMDE2R1tZZNBcdTAwMTXzlnxPnN1VXHUwMDFjjHVcdTAwMDZEJFx1MDAxM5hcdTAwMWHm+6rX7vLkdLNyXHUwMDE2XHUwMDA1riCo1/NwUWSmQTS6zVx1MDAxNDNcdTAwMTBcdTAwMDY8IzY4WPmWurZb5SHKSlx1MDAxZCT+cWuXNYpcdTAwMTmKj+T0Qjixz1vkplx1MDAwMPpcdTAwMDF+pYLaeLKCtk17XHUwMDA08V1JuECBXVUx8DpcdTAwMTEyXHStXHUwMDBlXHUwMDEzlP8qT1x0Wc80unVcXI1WXGIgX4qbWYnDOIhDqFx1MDAxN7rxXHUwMDE2XG5bJtisIFDR4UuhlNmbifFcdTAwMDCW6i+loHhcdFx1MDAxMNMqXHUwMDBmj9yoaIOZKTGOQ5BoXHUwMDFh/pb7XHUwMDA1lvazKeA54/6i8ppcdTAwMTQn6JrM4uKarVx1MDAwNS5cdTAwMDbR3ZRcdTAwMDf/03HRkWkpK+OLXHUwMDE4S61Uvlmm4GFY9lx1MDAwZpy0fCil9Fx1MDAxZuu3Jlx1MDAxNkhcdTAwMWIvbT5cdTAwMDDc0GPczNi51IZcdTAwMDTP8MBlXHUwMDAywehhMrNPejIprOTGXFyF9G6kNVx1MDAxM9ReQ+BttzFcdTAwMTCjOmI0vGQmnFCnXHUwMDE0bJmRZ1x1MDAxObOnmFx1MDAxNSiFdj1KtLXKW1C2KYp7dYIvXHUwMDA3THSteUpcdTAwMDbHoKReIFx1MDAxNGyaXHUwMDBilVjP3HpiUa1toDJ60vfPW2lcdTAwMDKSm4CVrCDjp1pcdTAwMTG4Jipcclx1MDAxNZt+cotvvaUmcCVcdTAwMWHaZKFcdTAwMTmBgIM2ralBtV6gPFx1MDAxNlV1OYtcdTAwMTTczGy3VrlWQsn1SHdcdTAwMTUjcZd+gkHVSpvG/Z6vXHUwMDE3p9OnTUfuO0unZUb7istYr4KIXHUwMDFlNM2whV8rzi1cbny53aBm6fNEgFx1MDAxYohTVqjVZdrzw9zRZklcdTAwMTHuk54km10vr9KJ6mnKknLOdIVAT41yQ9kqevTFXHUwMDAyL8/NLSXBXHUwMDE5bsXU7lkjLeXiMDIls5eXXHUwMDFj/Mk21ChP3CtS1dezOppISdp8L68hYK/FM1xubUJZlJU5hTe5jsZyv1x1MDAxOdy8sn9cdTAwMDNOMvT4hYmGuzb3iV4hhZwgqbmXXHUwMDE3aOHTb9CUMc5cdTAwMDHxptXJgzCaytNcdTAwMWLVy2vUXHIuXHQtd9wz8lx1MDAxYas82VL45mp9N1x0azPyXHUwMDE4+MpgQZLBulrf7vJcZuxcdTAwMTGGnLre9e0sjTtAXHUwMDA1XHUwMDBmYDh4qLuBuZU86q00XHUwMDFjSGw/IO5W8lx1MDAxMjdcdTAwMTiZir/cgful5XFPy607Y5wmeFx1MDAwYkN5XGaAdFx0ykJWeHlcdTAwMDNcdTAwMDI9k0xcdTAwMGK99Vxcruwvz1x1MDAxOUpcbvwjKMfbe4F+KZeekXwmm4lcdLxDgXDpmKBCejJD1Fx1MDAxOMlj7l7j4cjzT7Q6XHLluVx1MDAxMnFm9H6NO7ZcdTAwMTGnLLVcctg1icRwXHUwMDFkkDhcdFdcdTAwMWEpZIL2IbhcdTAwMTNpM3Fttlvvr1x1MDAwNJVA6SXR8j41llx1MDAxNnSqcJlcdTAwMTg8S19JL0+nwHwohlx1MDAwNE/12Vk23LjMkMlrhNRLcVx1MDAwMi8utsFcdTAwMTLks1x1MDAxM6lcdTAwMDeXwlx1MDAxNv4h0/ZcXF/za9iKeM5s7lx1MDAxNGfUr6VntNL5XHUwMDA2XHUwMDEz3FxuTFJmip5cdTAwMTlWRvs8sYFBXHQpXHUwMDE3nvpcdTAwMDet6is8br1SXHUwMDE4XHUwMDA1KoHUmXhcdTAwMTSAKO2WlDueXHUwMDBlwJAtXHUwMDE0Vq5UlogwkZ/TNspcZs8sPOuZXtLnM3JcdTAwMDOG5lx1MDAxZmV4XHUwMDEwOsdcdTAwMDJrmy8n/SpK7LtKXHUwMDFkw+z01+RcdTAwMTM8vJ9cdNKltlxcmVx1MDAxMVx1MDAxZMY0Jea0vpdlXHUwMDA0c8mQp1x1MDAwNP4nXHUwMDFhnuzC8Fx1MDAwMWFGrmdDf1x1MDAwN1x1MDAxMCB2ybfUXHUwMDE37ZFcdTAwMTNEJkBcdTAwMTn0w0wy0NNcXNpk4kxjNTyxMFx1MDAxZdJuXHUwMDE5nIs6WFruXHUwMDE4iNlcdTAwMTckmDDC5Sj3NVx1MDAxM1xi2sP8NIzpXHUwMDBmgSbD9S2AU3bJpGjfqlxcY4F5iUwlTUz4zqshZUJ0Nkd6M3VkNk6M0vZcdTAwMTWyUoGZXHUwMDA3YbjXXHUwMDE53vRcdTAwMDHCSYlRYW5cdTAwMDJFXHUwMDA3eUFcdTAwMWSy/FXVLq4yfmGwRJHLUciSKY+VJshxWpAy1OjYjUKFyVx1MDAxNGWQyt9cdTAwMWRz4MZcdTAwMTczllG8wkNGbppCwppcdTAwMWFlmfaps2fI+YRcdTAwMGVcdTAwMDagXHUwMDBmqYZlKJPvp4eXRaehOMdlXHUwMDBmXHUwMDEzbyd2MCpyQ9rmZvJcdTAwMTXkMjhcdTAwMTc4QpLCMEEuo/vjckJ34CbK9/JcdTAwMWO1bog1XHUwMDE00CfYdCEzolRBXHRNU1x1MDAwMFjVUVx1MDAwNYiUjTHdN6c64Vx1MDAwN4P2SP6A21x1MDAwNVx1MDAxY926bryzQFx1MDAxZKLkyckpXHUwMDE017o6kVvIq8znYkKP4uNqWnVRfplbklx1MDAxN2a6XGb0d8lsuO+EJtiz/W4hXHUwMDBmx1x1MDAxNKVcdTAwMGaG66P9xXlmjFF6XGJcXJatxuHvLE/YXHUwMDE2PmxlSIMgxv5cdTAwMGJknE4szCilqtS/KuJcdTAwMTZcdTAwMDLlp1x1MDAwNWRaI7DhZPZcdTAwMTao4F3bu1x1MDAxZFx1MDAxNNkldP9cdTAwMTWWxTPROFx1MDAwMlx1MDAxNGw/vvA2XHUwMDAyqVx1MDAxZVbqXHUwMDFkspG+lnpcdTAwMGJ5ySpjU0wk3Fx0ga9nxO8sMCxcZjOVXHUwMDFmjG1cYvveT5xcImxcdTAwMTDeWSH3Sa/r3lx1MDAwMtNcIrxCm39iJvp6bP/OXHUwMDAys1x1MDAxZVlcdTAwMDHX0vpcdTAwMTn7XHUwMDExXFy3kFdcdTAwMDQwXHUwMDE4seAp5vf3NbdcdTAwMTFYmS9cdTAwMDSkh/VcdTAwMWF7Lv/uXHUwMDAyXHUwMDE5WSu8XHUwMDA1NVZH4/b2XGZZnpBkTipj97fiTFxyLFx1MDAxOK54XCJcdTAwMTXuva04p1x1MDAwNZDAJZlk9s1cdTAwMWG3kMe7QVx1MDAxOLBlqUi+XHUwMDAx15+Fp3grXHUwMDE1bUfQXHUwMDFk95VXhPiFzyzDuFx1MDAwYlx1MDAxM973XHUwMDE2XGLvKCbP91rZdG+BYSGut5dgJZ/2d6yFr9DQXHUwMDFm6O/I+ytNXHUwMDEx+mi9dFnwUv5wpTWRkZA4NOha4zJKpS5N3lZdY26sXHUwMDE2XGLzXaHQ8Vt5ojBdPdcyjVZcdTAwMTbp+Vot0JDm8O5cdTAwMDYjLDDOwuBcdTAwMTEws8Nw6cZcctxNeUzGo+zPXHUwMDBiM8jRxvKK1sedtVx1MDAxZWlFXHUwMDE121mcM5DBXHUwMDA0XHUwMDFlqVwiXHUwMDBiva1cdTAwMDZNMuamtElcdTAwMWKg5LG4QPOgwCDl+/VcdTAwMThHZj1RsWkvN1x1MDAxOWNVZ3gxXGJXhszc6ccwM35NzrG9SIt3To2zXHUwMDA3PWGjXHUwMDAxVnBR7XlxtOpcdTAwMTjKO8qqeDnC+NZGYGO5nLHNwLi6XHUwMDAy07tcdTAwMGJs88mY3c/9I72SvUDUjktcdTAwMWRGYExcdTAwMWMvV/6xXHJcdTAwMTR37UVj3Vx1MDAwMlx1MDAxYlx1MDAwZo/5u1x1MDAwNGo/riM7S1x1MDAxNUqZjaUwmFZcdTAwMGa8u7zERUomM3SBQk8vj3Zb6FAxXHUwMDE1JjSOXHUwMDA1Ym5FXHRTslx1MDAxNIjWXHUwMDBm3C6sPXNcclx1MDAxNVxmJ1bIWF5a3HxoXHJh/Vxu/Vx1MDAwMls4XHUwMDA15kGViV5ccsfrgnUszL61XGaIdL083uhjY6NcdTAwMDWAj8dcdTAwMDKZQuh5OVhOUZh/JVx1MDAxMO5XoeBcdTAwMDDImejGgXtcdTAwMTDa6+Qoj/cqSKH0ktqKJU90RzHumzHAIWdwZe/xbeOQXGJx0+NTxlxydYoyreHaUVx1MDAwNDUrXpFduHFX0Fc8XCJcdTAwMWVMyHNMnLDkzjim3kGbhZdZyVdASohuzIR23C3E9lx1MDAwMp9Mb+lKXHUwMDFlLyhmcoHzOuNcdCfI9Vx1MDAxMS9cdTAwMDawmbvXkPtcdTAwMDF/lNCk1bXyQp7xVamj4yVcdTAwMDSmXGbS/tQj88I1OVx1MDAxNSjqMcZP+Fx1MDAxOJ+WQmjjrlx1MDAxNI6r6+TpsOTLiPhy4mM2r7Zlye1mXHUwMDFkg9d/bq5PkKOYRpGkubFOOH1/+YKmzOtcdTAwMTi1itqJXHUwMDAzuOi8MGI5jIntqzSEp1Rw+KmjebXX4TBhtM1anrhodpRcdTAwMTgzPClaXHUwMDBmjO0yQ7lHYZtWkeNlYOPVXHUwMDA1u7TWctfGZJvUj7xLVPZ8m0Wvx51YXptRx1x1MDAxYkqsgnbq6LdcbnA6ctNGzypJXHUwMDE5+5bQaFx1MDAwZe0llm3ASz/zXHUwMDBlTmAprceaXHUwMDBlllx1MDAxOXltRre0VVx1MDAxMMCtRjPkXHUwMDAycKFwTklprHvBa/sql15cXK3b7poqQFx1MDAxZZabp6iSXHUwMDE5gTwjXHUwMDBmXHUwMDFhZqWxzejB+iF1lbmclUlVzEifOI5cdTAwMDBcdTAwMGJcdE+aklJD28vTrjbCjvyLXHJcdTAwMTMz2lx1MDAxMUg+zeVv62Ds7vmY50onp8JcdTAwMWZz/Sf0LzCbPNuSLFx1MDAxZO1deVxmNMJbPVx1MDAwYv+nPzr2zoG5x8yGdbxcdTAwMDegt15cdTAwMGZhsObK4Fx1MDAwNqg4Y2cgeZCEc5VGMMfshjhcdTAwMDdrQXrMpIz2jrOJXl1eUpDrJee0t17HaFGdL22evKZySlx1MDAxY69ilHcuvCqnu+ST44l0RMTLkaRjXHUwMDEyXHUwMDE18si1YK4pjvRXNtyHMDrZM29p6jTSYrllJkmjJN1P+buFPJeYdFx1MDAxY1x1MDAxOOxg+9bkXHUwMDA1/nW7PaM0P56VgTyGvmOivOG0f1xyjNL3JFx1MDAwN0PKyuXG+EZpKJA30Fx1MDAxNtiWXGZcdTAwMGVcdTAwMTnP6Fx1MDAxY8hbT3L5XHUwMDA1XHUwMDA1vtNcdP6Fxlx1MDAwNr5uhT+/vtfNXHL8+Fx1MDAwZl+bw/TFX7+7+20+OD4sz/Kn5WRubmCxtFx1MDAwN1x1MDAxYnpPqFx1MDAxNV+p27XpYzSmtC4rt2VcdTAwMTldXHJcdTAwMWaTnkJEUjQ2XHUwMDA0m2vDIW45N1Bhf1N/RdPGzueHjbVXX7prTMhrs8auz/zqZo1cdTAwMTXTXp7pbzNr7Nqj/Fx1MDAwMjNcdTAwMDOXXHUwMDA3erDlyen5xfv+6Ifv3b9cdTAwMGXDyfG5++EtniE4XvKbmSn43edcdTAwMWadbj5//uFcdTAwMDdcdTAwMDd/OD9+/p359r2PPrg/Z9KhusauS46miqt4fs2iM02sXHUwMDAynTj+9SjQRM1cdTAwMDJ2NmynuP8k0F+bRV/sMD5cdTAwMTB6tdCLfbVJu/6nVybtXHUwMDE5QXR9WOSbMekqXGazTc9vY9JcdTAwMDdcdTAwMTe/ff/9t9h+u/W9XHUwMDE5Y33874vTe+HTp399ePr3h/eehU+/MF9Nzu1Nhel2dE+0XrJ1/OWdTHSh0b1cXLa1se3g3rjwjpvq6N1k5vx2m//fXFynzPWLXHUwMDFk7JXeXHUwMDE1Zlx1MDAxZr7KXpVcdTAwMDf8bFxmZk5cdTAwMTSjXG7yXHUwMDFit9hcdTAwMWOu3cjcxmLfZmt945Z6/t2Tu1x1MDAxZv7w+0fp6I/Pvn20+X165I9/P2mplTeYwixjRN42jb9cdTAwMTZXmftfSqR5crtP1yds0/VcdTAwMTSrkjfGLmyN4HaG+muby/t0XHUwMDA3O6VqwfviXlx1MDAxOVfNauz21kxpXHUwMDBlJ5l567DywZMny93zx9+/xda6XuKbMdrPvnz4w7fxk1x1MDAwZr/57NnHZ2fus5Pj51x1MDAxZv5hdiw+L/Ss2nxXmMXxXG6jrbzD6LLrfj1bm5fh1MYmRMJcdTAwMWJcdTAwMDDDv7ax+N/tYLSRXl2T/aum4jM+8eeslpehhfJcdTAwMTba7NlT/rXc37zFRvuKNe5ste+8KJrhXHUwMDAxvr7QLurTS1x1MDAxYr7z3eHm2Vx1MDAwN6/Q8Vx1MDAwN+1fd955YfPoeCuz/fjTOz/9XHUwMDFmkVx1MDAwZobHIn0=<!-- payload-end --></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><g transform="translate(269.845485107915 13.779591306684324) rotate(0 75.01197052001953 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#228be6" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">the format</text></g><g transform="translate(75.37369566187408 85.60488354179506) rotate(0 66.51214547980635 -14.811747175490382)" stroke="none"><path fill="#7950f2" d="M -2.59,0 Q -2.59,0 -2.55,-1.63 -2.50,-3.27 -2.53,-4.37 -2.57,-5.47 -2.65,-6.96 -2.73,-8.45 -2.82,-9.89 -2.91,-11.33 -2.35,-12.80 -1.79,-14.28 -0.99,-15.07 -0.20,-15.87 1.50,-16.71 3.20,-17.56 4.49,-18.07 5.78,-18.58 7.35,-19.05 8.91,-19.52 11.19,-19.90 13.48,-20.29 14.64,-20.39 15.81,-20.49 18.30,-20.59 20.79,-20.69 22.05,-20.69 23.31,-20.69 25.31,-20.72 27.30,-20.75 29.89,-20.68 32.47,-20.62 33.64,-20.62 34.80,-20.62 35.92,-20.62 37.03,-20.63 38.12,-20.63 39.21,-20.64 40.78,-20.76 42.35,-20.89 44.00,-21.04 45.66,-21.19 48.05,-21.57 50.45,-21.96 52.34,-22.39 54.24,-22.82 55.82,-23.31 57.40,-23.80 58.61,-24.20 59.81,-24.60 61.29,-25.04 62.78,-25.49 64.32,-26.11 65.86,-26.73 66.92,-27.21 67.97,-27.69 69.21,-27.49 70.45,-27.29 71.38,-26.63 72.30,-25.96 73.09,-24.95 73.87,-23.93 74.70,-23.07 75.52,-22.21 76.54,-21.55 77.57,-20.89 78.95,-20.55 80.33,-20.20 81.38,-19.98 82.43,-19.77 84.15,-19.54 85.87,-19.31 87.75,-19.07 89.63,-18.84 91.45,-18.68 93.28,-18.52 94.34,-18.38 95.40,-18.24 97.49,-18.07 99.58,-17.89 100.68,-17.76 101.78,-17.62 104.42,-17.31 107.05,-16.99 109.22,-16.72 111.38,-16.44 113.17,-16.21 114.97,-15.98 116.09,-15.75 117.20,-15.52 118.70,-15.19 120.20,-14.85 121.85,-14.41 123.51,-13.97 124.54,-13.66 125.58,-13.34 126.85,-12.86 128.13,-12.38 129.08,-11.79 130.02,-11.20 130.85,-10.47 131.67,-9.74 132.50,-8.87 133.33,-8.01 134.04,-7.15 134.75,-6.30 134.75,-5.40 134.75,-4.50 134.90,-4.10 135.06,-3.70 135.11,-3.28 135.17,-2.86 135.12,-2.44 135.07,-2.01 134.92,-1.61 134.78,-1.21 134.54,-0.86 134.30,-0.51 133.98,-0.22 133.66,0.05 133.29,0.25 132.91,0.45 132.50,0.56 132.09,0.66 131.66,0.67 131.24,0.67 130.82,0.57 130.41,0.47 130.03,0.28 129.65,0.09 129.33,-0.18 129.01,-0.46 128.76,-0.81 128.52,-1.16 128.80,-0.77 129.09,-0.37 128.77,-0.84 128.44,-1.31 128.28,-1.86 128.12,-2.41 128.15,-2.98 128.17,-3.55 128.38,-4.09 128.58,-4.62 128.95,-5.06 129.31,-5.50 129.80,-5.80 130.29,-6.10 130.85,-6.23 131.40,-6.36 131.97,-6.30 132.54,-6.25 133.06,-6.01 133.58,-5.78 134.00,-5.39 134.42,-5.00 134.70,-4.50 134.97,-3.99 135.07,-3.43 135.17,-2.87 135.08,-2.30 134.99,-1.74 134.73,-1.23 134.47,-0.72 134.06,-0.32 133.65,0.06 133.13,0.31 132.61,0.56 132.05,0.62 131.48,0.69 130.92,0.57 130.36,0.46 129.87,0.17 129.37,-0.11 129.00,-0.54 128.62,-0.98 128.41,-1.51 128.19,-2.03 128.15,-2.60 128.11,-3.18 128.26,-3.73 128.41,-4.28 128.72,-4.76 129.04,-5.23 129.49,-5.58 129.94,-5.94 130.48,-6.12 131.02,-6.31 131.60,-6.32 132.17,-6.32 132.71,-6.15 133.25,-5.97 134.00,-5.23 134.75,-4.50 134.90,-4.10 135.06,-3.71 135.11,-3.28 135.17,-2.86 135.12,-2.44 135.07,-2.01 134.92,-1.61 134.78,-1.21 134.54,-0.86 134.30,-0.51 133.98,-0.22 133.66,0.05 133.29,0.25 132.91,0.45 132.50,0.56 132.09,0.66 131.66,0.67 131.24,0.67 130.82,0.57 130.41,0.48 130.03,0.28 129.65,0.09 129.33,-0.18 129.01,-0.46 128.76,-0.81 128.52,-1.16 128.45,-2.18 128.39,-3.19 127.45,-4.25 126.51,-5.30 125.72,-6.04 124.92,-6.77 123.78,-7.19 122.63,-7.60 121.59,-7.97 120.55,-8.33 118.92,-8.80 117.30,-9.28 115.66,-9.64 114.02,-10.01 112.34,-10.28 110.67,-10.55 108.54,-10.82 106.42,-11.09 104.93,-11.17 103.43,-11.24 102.26,-11.29 101.09,-11.34 99.04,-11.52 97.00,-11.70 94.82,-11.84 92.64,-11.98 90.71,-12.11 88.78,-12.24 87.09,-12.43 85.39,-12.62 83.32,-12.83 81.26,-13.05 79.35,-13.36 77.45,-13.68 75.89,-14.21 74.34,-14.74 72.89,-15.65 71.44,-16.57 70.52,-17.47 69.60,-18.37 68.54,-19.57 67.49,-20.78 66.86,-21.64 66.23,-22.51 65.38,-24.04 64.52,-25.57 64.39,-27.53 64.25,-29.49 64.76,-30.64 65.28,-31.79 67.38,-32.45 69.49,-33.12 70.77,-32.73 72.06,-32.34 73.27,-31.26 74.48,-30.18 74.77,-28.46 75.06,-26.74 74.47,-25.46 73.88,-24.19 72.73,-23.24 71.59,-22.30 70.53,-21.82 69.47,-21.34 68.07,-20.81 66.68,-20.28 65.45,-19.80 64.22,-19.32 62.94,-19.00 61.66,-18.67 60.37,-18.33 59.07,-18.00 57.35,-17.54 55.63,-17.08 53.63,-16.67 51.63,-16.26 50.15,-15.91 48.66,-15.55 47.30,-15.33 45.93,-15.10 44.22,-15.01 42.51,-14.93 40.87,-14.89 39.22,-14.84 38.12,-14.85 37.03,-14.86 35.92,-14.86 34.81,-14.86 33.64,-14.86 32.47,-14.86 29.90,-14.79 27.33,-14.72 25.37,-14.73 23.41,-14.73 22.20,-14.69 20.99,-14.65 19.66,-14.55 18.34,-14.46 16.43,-14.26 14.52,-14.06 12.65,-13.69 10.79,-13.33 9.41,-12.96 8.03,-12.59 6.80,-12.15 5.56,-11.71 4.24,-11.47 2.91,-11.23 2.82,-9.84 2.73,-8.45 2.65,-6.96 2.57,-5.47 2.53,-4.37 2.50,-3.27 2.55,-1.63 2.59,0 2.55,0.31 2.52,0.62 2.40,0.91 2.29,1.20 2.12,1.46 1.94,1.72 1.70,1.92 1.47,2.13 1.19,2.28 0.92,2.42 0.61,2.50 0.31,2.57 -0.00,2.57 -0.31,2.57 -0.61,2.50 -0.92,2.42 -1.19,2.28 -1.47,2.13 -1.70,1.92 -1.94,1.72 -2.12,1.46 -2.29,1.20 -2.40,0.91 -2.52,0.62 -2.55,0.31 -2.59,-0.00 -2.59,-0.00 L -2.59,0 Z"></path></g><g transform="translate(75.85317516227951 10) rotate(0 57.35797882080078 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">the user</text></g><g transform="translate(220.25062721840732 89.60220950083522) rotate(0 123.21464997310977 -17.40024157983953)" stroke="none"><path fill="#228be6" d="M -2.65,-0.19 Q -2.65,-0.19 -2.61,-1.20 -2.56,-2.20 -2.11,-3.73 -1.67,-5.26 -0.76,-6.50 0.14,-7.74 0.93,-8.58 1.72,-9.42 2.72,-10.31 3.72,-11.20 4.87,-12.16 6.02,-13.12 6.95,-13.79 7.87,-14.45 8.76,-15.10 9.65,-15.75 10.52,-16.38 11.38,-17.02 12.73,-18.01 14.08,-19.01 14.99,-19.61 15.90,-20.22 17.01,-20.94 18.11,-21.67 19.19,-22.19 20.27,-22.71 21.47,-23.28 22.66,-23.84 23.73,-24.25 24.79,-24.65 26.29,-25.18 27.79,-25.70 29.12,-26.23 30.45,-26.76 32.14,-27.29 33.82,-27.81 35.18,-28.09 36.54,-28.38 38.08,-28.47 39.61,-28.55 40.76,-28.57 41.91,-28.59 43.43,-28.61 44.95,-28.62 46.68,-28.59 48.41,-28.56 49.51,-28.53 50.62,-28.49 52.96,-28.28 55.30,-28.07 56.40,-27.84 57.51,-27.61 59.72,-27.19 61.94,-26.78 63.64,-26.51 65.34,-26.24 66.39,-26.09 67.44,-25.94 68.99,-25.72 70.54,-25.50 72.20,-25.27 73.86,-25.03 75.49,-24.80 77.13,-24.56 79.21,-24.11 81.29,-23.67 83.12,-23.29 84.94,-22.91 86.69,-22.56 88.44,-22.22 89.97,-21.96 91.50,-21.70 92.99,-21.68 94.49,-21.66 95.55,-21.68 96.61,-21.70 98.07,-21.71 99.52,-21.71 100.97,-22.21 102.43,-22.70 104.13,-23.59 105.84,-24.47 107.96,-25.50 110.09,-26.52 111.80,-27.38 113.50,-28.23 114.80,-28.96 116.11,-29.68 117.10,-30.25 118.10,-30.82 119.12,-31.55 120.15,-32.29 121.35,-32.44 122.54,-32.60 122.71,-33.82 122.87,-35.04 123.85,-34.20 124.82,-33.36 125.71,-32.59 126.60,-31.82 128.02,-30.75 129.45,-29.67 130.77,-28.77 132.10,-27.86 133.72,-26.90 135.34,-25.94 136.64,-25.35 137.94,-24.76 139.18,-24.25 140.42,-23.74 141.99,-23.20 143.57,-22.65 144.74,-22.31 145.90,-21.96 147.10,-21.75 148.30,-21.54 149.56,-21.45 150.81,-21.36 152.19,-21.36 153.56,-21.36 155.04,-21.38 156.51,-21.39 157.59,-21.39 158.67,-21.40 160.62,-21.35 162.56,-21.30 163.90,-21.30 165.24,-21.30 166.53,-21.31 167.83,-21.31 169.11,-21.31 170.40,-21.32 171.80,-21.31 173.19,-21.31 174.84,-21.29 176.48,-21.28 178.18,-21.26 179.87,-21.25 181.52,-21.24 183.17,-21.23 184.52,-21.17 185.86,-21.11 187.15,-21.00 188.44,-20.89 189.52,-20.77 190.59,-20.65 193.08,-20.26 195.57,-19.86 197.97,-19.55 200.38,-19.24 201.72,-19.05 203.07,-18.86 204.98,-18.63 206.88,-18.40 208.50,-18.30 210.12,-18.19 211.71,-18.06 213.31,-17.93 214.37,-17.82 215.43,-17.72 217.41,-17.41 219.39,-17.11 221.70,-16.73 224.00,-16.35 225.36,-15.98 226.71,-15.62 227.82,-15.29 228.92,-14.96 230.70,-14.35 232.47,-13.75 233.83,-13.18 235.18,-12.60 236.22,-12.20 237.25,-11.79 238.33,-11.26 239.42,-10.72 241.01,-9.80 242.61,-8.87 243.71,-8.19 244.80,-7.51 247.33,-5.21 249.87,-2.90 249.95,-2.35 250.03,-1.80 249.92,-1.25 249.82,-0.70 249.55,-0.21 249.28,0.27 248.86,0.65 248.45,1.02 247.94,1.25 247.42,1.47 246.87,1.52 246.31,1.57 245.77,1.44 245.22,1.30 244.75,1.01 244.28,0.71 243.92,0.27 243.57,-0.15 243.38,-0.67 243.18,-1.20 243.16,-1.76 243.14,-2.32 243.30,-2.85 243.46,-3.39 243.79,-3.84 244.11,-4.30 244.56,-4.63 245.02,-4.96 245.55,-5.12 246.08,-5.29 246.64,-5.28 247.20,-5.27 247.73,-5.08 248.25,-4.89 248.69,-4.54 249.13,-4.19 249.43,-3.72 249.74,-3.25 249.88,-2.71 250.01,-2.17 249.97,-1.61 249.93,-1.05 249.71,-0.54 249.49,-0.02 249.12,0.38 248.75,0.80 248.27,1.08 247.78,1.36 247.23,1.47 246.68,1.58 246.13,1.50 245.57,1.43 245.07,1.19 244.57,0.94 244.17,0.55 243.77,0.15 243.52,-0.34 243.27,-0.84 243.27,-0.83 243.27,-0.83 241.97,-1.47 240.67,-2.10 239.64,-2.84 238.61,-3.59 237.63,-4.24 236.64,-4.89 235.13,-5.69 233.62,-6.49 232.54,-7.05 231.45,-7.62 230.17,-8.20 228.89,-8.77 227.05,-9.36 225.21,-9.94 224.17,-10.21 223.13,-10.48 220.82,-10.79 218.52,-11.10 216.66,-11.37 214.81,-11.64 213.11,-11.82 211.41,-11.99 209.84,-12.22 208.28,-12.44 207.22,-12.52 206.16,-12.60 204.28,-12.88 202.40,-13.17 201.03,-13.30 199.66,-13.43 197.23,-13.65 194.79,-13.87 193.27,-13.99 191.74,-14.11 189.89,-14.31 188.03,-14.51 186.95,-14.55 185.86,-14.60 184.52,-14.54 183.17,-14.48 181.52,-14.47 179.87,-14.46 178.18,-14.45 176.48,-14.43 174.84,-14.42 173.19,-14.40 171.80,-14.40 170.40,-14.39 169.11,-14.39 167.83,-14.40 166.53,-14.40 165.24,-14.40 163.90,-14.40 162.56,-14.41 160.62,-14.36 158.67,-14.31 157.59,-14.31 156.51,-14.32 155.02,-14.34 153.53,-14.35 152.02,-14.39 150.50,-14.43 149.03,-14.58 147.56,-14.72 146.35,-14.95 145.14,-15.17 143.96,-15.51 142.78,-15.84 141.41,-16.28 140.05,-16.73 138.39,-17.32 136.74,-17.92 135.22,-18.60 133.71,-19.28 132.13,-20.08 130.54,-20.88 129.51,-21.42 128.47,-21.97 127.11,-22.84 125.76,-23.72 123.87,-24.95 121.99,-26.19 120.68,-27.28 119.37,-28.37 118.47,-29.22 117.56,-30.06 116.96,-30.96 116.35,-31.86 116.21,-33.31 116.07,-34.75 116.45,-35.79 116.84,-36.83 118.80,-37.57 120.76,-38.30 121.94,-37.86 123.12,-37.41 124.07,-36.48 125.02,-35.56 125.62,-34.48 126.22,-33.40 126.24,-31.91 126.26,-30.43 125.82,-29.37 125.38,-28.31 124.30,-27.33 123.22,-26.36 122.26,-25.76 121.30,-25.16 120.18,-24.59 119.05,-24.01 117.69,-23.31 116.32,-22.61 114.62,-21.75 112.92,-20.90 110.93,-19.82 108.94,-18.73 107.89,-18.12 106.83,-17.50 105.01,-16.63 103.19,-15.76 102.16,-15.45 101.12,-15.15 99.60,-15.08 98.08,-15.02 96.78,-15.04 95.49,-15.06 94.35,-15.10 93.21,-15.14 91.76,-15.27 90.32,-15.40 88.79,-15.69 87.27,-15.99 85.36,-16.35 83.45,-16.72 81.77,-17.08 80.08,-17.44 78.17,-17.82 76.25,-18.19 74.63,-18.45 73.00,-18.70 71.29,-18.95 69.59,-19.21 68.12,-19.47 66.66,-19.72 65.51,-19.85 64.36,-19.98 62.68,-20.28 61.01,-20.57 59.57,-20.72 58.13,-20.87 56.36,-21.19 54.59,-21.51 52.61,-21.58 50.62,-21.65 49.51,-21.61 48.41,-21.58 46.68,-21.55 44.96,-21.52 43.46,-21.53 41.96,-21.53 40.88,-21.53 39.80,-21.52 38.22,-21.38 36.63,-21.25 35.19,-20.88 33.75,-20.52 32.38,-20.01 31.00,-19.50 29.48,-18.96 27.95,-18.43 26.68,-18.02 25.40,-17.60 24.29,-17.10 23.17,-16.59 22.00,-15.93 20.82,-15.27 19.51,-14.38 18.20,-13.48 16.81,-12.47 15.41,-11.46 14.13,-10.62 12.84,-9.79 11.84,-9.16 10.84,-8.53 9.56,-7.61 8.28,-6.70 7.14,-5.87 6.01,-5.04 5.14,-4.25 4.28,-3.47 3.57,-2.61 2.86,-1.75 2.75,-0.77 2.65,0.19 2.59,0.51 2.53,0.82 2.39,1.12 2.26,1.41 2.06,1.66 1.85,1.91 1.60,2.10 1.34,2.30 1.05,2.42 0.75,2.55 0.44,2.61 0.12,2.66 -0.19,2.63 -0.51,2.61 -0.82,2.51 -1.12,2.41 -1.40,2.24 -1.67,2.07 -1.89,1.84 -2.12,1.61 -2.28,1.33 -2.44,1.05 -2.53,0.75 -2.62,0.44 -2.64,0.12 -2.65,-0.19 -2.65,-0.19 L -2.65,-0.19 Z"></path></g><g transform="translate(331.58938407869573 89.07176612885542) rotate(0 172.39590454101562 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">.feed.post/3lzy2ji4nms2z</text></g><g transform="translate(10 89.36299530115593) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(202.20795004433603 89.03669325972533) rotate(0 7.853996276855469 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/</text></g><g transform="translate(216.6579406465171 89.18857126772946) rotate(0 57.189979553222656 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#228be6" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">app.bsky</text></g><g transform="translate(74.9183298167045 89.16372756630062) rotate(0 63.95197296142578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">ruuuuu.de</text></g></svg>
+4
public/where-its-at/3.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 403.78274038483505 146.0923676561706" width="807.5654807696701" height="292.1847353123412"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1ZS3PTSFx1MDAxML7zK1xc3itcdTAwMTHzfuSGXHIkZiFcdTAwMDTCQsJcdTAwMTZcdTAwMDdZXHUwMDFl28Ky5Ejj2EDlv2+PsDXyXHUwMDAzcLLe2lBEh5TcPSP1dPf3dbfy9UGj0bSfJ6Z52GiaeVx1MDAxNCZxL1x1MDAwZmfNh05+ZfJcIs5SUJHyd5FN86hcXDm0dlJcdTAwMWM+euR3XHUwMDA0UTb+tsskZmxSW8C6v+F3o/G1/Fx1MDAwYpq45/ZedZ5kpjNvt8JcdTAwMTfFeH6FPlx1MDAxYzxp9cqt5aKlMdbMrZfOnVx1MDAxMZRcdTAwMGJcdTAwMTRgzalcdTAwMTKCYLip1J9BfUBcdTAwMTHFXHUwMDAxlZxowlx1MDAxMKXKq2dxz1x1MDAwZWGJYIHCWlx1MDAwYsyZVFx1MDAxY3NRrVx1MDAxOJp4MLSwhPpdYTpInCmoklx1MDAxNDbPRqadJVnuTPxDXHRltPBWdsNoNMizadrza7qkT7pdv6ZcdTAwMWYnyZn9XFw+uVx1MDAxOeVZUVx1MDAxY1xmQ1x1MDAxYlxym2tveb+wXHUwMDE5r8mrvUVcdTAwMDae97vgtYNhaopiXHUwMDE5rlKaTcIots49XHUwMDE4+XM4XHUwMDFiJ51eXHUwMDE5oo/esjxcdTAwMWObjotROk2SSlx1MDAxY6c949zf7HbsyuvS3uJ1K8tcdTAwMGJj3COwJERcdTAwMTHMMas0Pp0wIVx1MDAxYuKTLC1zXHUwMDBiU1wiMVx1MDAxM1xu+YPHxVx1MDAxM0gqWz63XHUwMDFmJoXxXHUwMDBld1Y8rSWcP8100lx1MDAwYu3CXHUwMDE0rlx1MDAxOVwiiFOtVaVP4nS0bnuSRaMtbylTXHUwMDExXHUwMDFjXHUwMDEwWkj5WiSz1J7FX5zRRK1In4XjOHFO5yvPeJzEXHUwMDAzd/xmYvo1V4JcdTAwMDNsXGY4qtQ2m3htXHUwMDA0z1x1MDAwYuPU5JuRyfJ4XHUwMDEwp2Hydrt94dRmb0zxzUKbT0397OZ4mfE4ILxUXFw/3Fx1MDAwNtfo/IilfXZydcHbo37/9buzzunFbnBlmlx1MDAwNZpcdTAwMGKAJNZcYlO5XG5Xolx1MDAxOVx1MDAwZbRQnFLEMWHyXHUwMDFlr3vHq7hcdTAwMDFglaRcblx1MDAxY430NsBcIiHWxVx1MDAxNWAxVpxIrug9YFx1MDAxYndcdTAwMDCw796e/3n28uVcYtNP+fN2NjyWycXpjvWVy1x1MDAwMGmp4OJcdTAwMWNcdTAwMTFcdTAwMWbQJWBRICVHnDAsXGLfXHUwMDAyWKD8XHUwMDAw5FCfhdZcdTAwMDRSyi+5XHUwMDFkYqXmqE9+I8S2d0csQ1x1MDAxY1x0RGu4q1x1MDAwMVx1MDAxNtdcXL9cdTAwMDZYSl2TRG9VX1x1MDAxN1xun3S1xLtcdTAwMWN1u73HSdF6S8Lz1skn+clGL6vjrCRfmOfZrFlprlx1MDAxN3c/IFx1MDAwM8w5qZ30XHUwMDE2ZNCLe4cz0z2Mw3GQT91cdTAwMTX0zFx1MDAxZCaHn9i7XHUwMDFmslx1MDAxOF3aLGJH03eD7HxcdTAwMTDN2NEr9ObpbmRcdTAwMDE5XHUwMDE0uD6bXHUwMDAy2Dlf44qyXHUwMDE3V0IygpAkrObRJVVQXHUwMDE1SEK1olIwwVx1MDAxNPNccuF9bd+JKV7doLYjJZlcdTAwMTJUb23GmfouVWBcIimXXG7iu+/aXHUwMDBlRUKTesreXHUwMDE0zo+CILjD6F01bz9g1W/mJ9Ow3Urs+9fW/lx1MDAxNV+q50eD3cCqoHQjmMYoVlx1MDAxONHa9LWs7CRAgEKslHbz9T1a943W4e5ohZaHcqFcdTAwMTSuZ+VcdTAwMTKsXHUwMDE0f7dcdTAwMTGH9k1cdTAwMTClmG/g94ZVqL/3WL1cdTAwMTFWj09cdTAwMDdfPvBn7Yvj2dM8J8fpeN5+sVx1MDAxYlahtVx1MDAwZTCMy1gwKn0wfV3lknFcdTAwMDFcdTAwMDO1XCLQXHUwMDAybkBcdTAwMTU4O9BcYriea0L2MTX/bj341VxyXG5r2ZVcIklRPSurwso2xH5oXHUwMDE2jDEogf6jyK/Qhf/7kfxX6Lz/s277x7FZi8s6K0hcdTAwMTE4PsCaXHUwMDAy9rVcdTAwMDf1ooIr7tQwXHJcIvdcdTAwMTXHq5e0gFx1MDAwMo2Z5oq7hlx1MDAxY1x1MDAwYr2VXHUwMDE2RFx1MDAwMDVeXHUwMDEyTlx1MDAwNNKccY/Ku8FcdTAwMTJkTf5/ssR8O0t4RC7DSVx1MDAxNpLrdfJgXHUwMDAyOFx1MDAxZVwiKetcdTAwMTm84Fx1MDAwZbn5XHUwMDE5bklcdTAwMWRcdTAwMWPiq6Qmt5nfV1x1MDAwZbOBbklcdTAwMDSltyr0kyxepyZ/1/BZU/6o7j8+3L76u5nqro1cdTAwMWP1j9vgryQsbDtcdTAwMWKPY1x1MDAwYuc8dTZu8LhccnPbgrDG6WA1eot/Y3V2qOYlW0XTokRcdTAwMTm0S1x1MDAwNKYtXHUwMDA3SEREXHKJLtfCScn9gavuUlwiLoRmlMiNXGYxae/nRv34Q1/NqFx1MDAwM1x1MDAxNGBcdTAwMDbMXHUwMDAwY7iihFwihLZYpVx1MDAwMoi+QJwhRinDfNOo0lePXHUwMDFkN1xyTbhcdTAwMDFccjC5rlsnMZN0s5nPnG8k+WBcdTAwMTGsZjiZnFlIxuq0zavYzFpbKKRfXq5olYY5XG4xJe6uXHUwMDFmXFz/XHUwMDAz3MIo3iJ9<!-- payload-end --></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><g transform="translate(75.23901143105832 10.49212766904293) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(10 99.8988870211806) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(72.14151366488659 101.09236765617061) rotate(0 141.28793334960938 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did:web:iam.ruuuuu.de</text></g><g transform="translate(267.44696147539435 10) rotate(0 19.36199188232422 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/...</text></g><g transform="translate(355.0587566201866 99.80260117289026) rotate(0 19.36199188232422 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/...</text></g><g transform="translate(140.15734124776282 10.292859934187618) rotate(0 63.95197296142578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">ruuuuu.de</text></g><g stroke-linecap="round"><g transform="translate(191.48154417005935 56.430036990881945) rotate(0 0.4574792503608478 18.199362630477253)"><path d="M0.88 -0.25 C1.16 6.15, 1.06 31.28, 1.14 37.24 M-0.12 -1.43 C0.06 4.74, 0.15 29.27, 0.31 35.57" stroke="#7950f2" stroke-width="2" fill="none"></path></g><g transform="translate(191.48154417005935 56.430036990881945) rotate(0 0.4574792503608478 18.199362630477253)"><path d="M-6.53 18.89 C-3.36 23.6, -4.34 26.7, 1.07 35.16 M-6.36 18.02 C-4.57 25.62, -1.37 31.41, 0.32 35.04" stroke="#7950f2" stroke-width="2" fill="none"></path></g><g transform="translate(191.48154417005935 56.430036990881945) rotate(0 0.4574792503608478 18.199362630477253)"><path d="M5.92 18.72 C5.88 23.56, 1.7 26.71, 1.07 35.16 M6.09 17.85 C3.29 25.41, 1.91 31.25, 0.32 35.04" stroke="#7950f2" stroke-width="2" fill="none"></path></g></g><mask></mask></svg>
+4
public/where-its-at/4.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 604.3916306925712 152.29345326784187" width="1208.7832613851424" height="304.58690653568374"><!-- svg-source:excalidraw --><metadata></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAABFIAA4AAAAAHbgAABD0AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiIbhGocgWoGYACBDBEICqp8n0ALOgABNgIkA3AEIAWDGAcgG8sWo6KsEh4h+2dCNmRKb5iNaEiiWE2tuVtnnWXds/qL53wnMRUanq71783M7qzZIvra8oLRP+Jqg89Q/2TGP99f7Lz7N5BmAlyywPIkCNqZLMBAdfsfoH/6fG3WB29vsOtVRaRnMhJpGbGM++1DGDn1aS9XuMK1BIm0FMS7R0h6xARZudrvVznMH6GqhErohEQp9+/rvv3mmL7f8IuIV6JHIkM180yTROmUSuqkSMmU0FlV1VN66agAPTZBz8mcuVobQADQQDWAQEE/MwYnAkkJfCoAKPzEwO6muDQgeXXYa4HkbS+rAZKjqLkeSGAAAN+QWp8yez0g9UWAhMwWxISQgKRFB+bvAn8wDBkT/MS+G0GvfW9GCe07lqzvUQ54Ol5WbtSgczDqftoGh4docCQUVHRMHFw8QqJTGCmFoTFdDEgAdASKoQ4hsBN0EZEVUk9AAIBOo1weAp8qCQAUcHMBADYSbR3OFBCIhChEUejEkRJ4QeymYOw75ytngIhhFEBXQOG5ELktcGwBIdJhLIQrRKwV4Sjb2k7NLctugDjlveKPqooiwPvaS6tzGUBPBADg1G4VwAklg3A1TOWKSUYrxHEqa0M5WIjrBjUDUIf46Tj9sCxTDHsWVSG+Xj36Lzp5b0qDDIpQCzEgYSNsjJUGJyEgNaYsAEAsjhk+QYBf96EpnNPwMCeAQ3y0r9Z/FsJqha1KYCEAAPz3IyrPUH3CyJshQwHT/jwSLCgKXwGChQhTqlyVBnat2k8BPtWkSKhU4938FZuLzjvnjBWWW2apJRZbFAQ6eFa5AMgcovFyCcRvNtJO6zGeOvBBEGqlVkZqrdz1l4fXPaueny4DsB9Ulfrxv/6hL8xcr1w/l988/7f67WuXbu8rSagEL2m8GpY6yvDY+zxxCXQP9zQ8T4vr/z/3SaAz31eKMw5r3Z1uDOgono3h70l17ozKGp8OlEF2yX4MdfZ8eL+0+uqoxSPeyzkAY4wtTDEF/FFh7dTzz9fey/YcIX133Fqece786M2OYpDASVIJPubIrBVJVjhFaaqWn2jVKZoepRJdQ979ig0JfDipB++HCwuP05ODsMoP8AORZFUH29f17SZjio/MoqmIbIEpdgUmsn2otjEXtHJGmYPzPH2HhIblAKzO4JyA4cah94fsydPxznEyE6vxIv+EXu1f3V/l79AjxXxcC/SivptlIgstJFmxZFWfLbCNSc3WdeWQkm0lI+DEORXIduVhzrnM1Vgz+kJLowN0r+Q/rsjBVtIDuCkYvVt0oKwcY7K9CTcBdjAG2NJ4Ok+pRz1T6WTPjzHluWxrJCAABpBoJIFjCDEG/CCl1EPIa8Rxw659PvW6ZHYQj8v8y2hHb2d/8U0jbaQl3c+2M8aU/rlmnIu14aH3MXdlksy4d3qzrSixhv+qGkrWdYwhJgaB9wPf/sWKrClKz67pndennmf17sP+nOvmHGBxitqITkH0Y4fRdKF7JUhqOukLxuFIs+Yp8tKimGXaJMkrJb3EVv2O4meiXyMPobtVNVQnzgn5ve44gS7A27a3xotyfPlNo/lUtpVMx6GUno+1Zcm0MxWpucuSN6ywidJo650/hsfYhqJNICEuNFwHOFU6QOEBi6NprMZbbwr/cY70yliy0jz9231ZyAnNJG9gvmhV19JCWuVfejEYHa7d8y4qmp7JlE62khZW0+3bKWOMEHnxu0hUI9mS6V3ZnnuabE1mDAwb3MbhJxS2i/7jCmEEVoTkBGmCG3x+gGiaIq9m3jrJGy13GRpNR2iR8XFWY49ZV7ZNY5B7Kxx8AQR/uZxopNaFfbV3MG3QATIlCce5ZjkqrJWY74usLf6pnWYFVunuNZIk7woa0SxE19reirialVaybb5szxkEakRYimPBTVqtPAFYiwrUE0Vnx32Yd7a+Lg0LjyvBTmGYFddrk7m7msOpDt0c5ij0uTZdET0J96AIgOMA18KAh4iKokRBrOLFwapQuo220T2QrPw7jnNO9aToZ4pZoKI5hHbeFYymwNbXFTYcKsNDLBiTfCJoRktwAebau+mjtc06NZXH+mPZTs4ke2IVR/ydbmdZiXoeHaBUsgAHuAce5LDTEsgYBjcxppSitGGmZtbJiqu/r23fK0i0n2ozvhilJyUjFmLNzfVu27uzxuDd+Qeqe+RhPbh9/9jzS++P9AUD9CQrRFI0OzsCeASW8sTFDr5xRc8KjXawYtGvsQASIhja4nSe3ssVpdv9zY6HAytC7+rMtKMc27ikiMN6P08SWOm+79brk5ke4FEMrAPg8jcydpKgxgAU/h79CiP5/BIY9b5cxtGiFYaIenSTjbGa7GlAnyFvxctEJop0OxsqbCJPdC0wEpZ2koDI9kKmlzqP1Siz7P5hf8YATSPZ1JrFmkXR4NWudLN3tl0c6YzDGnuaT7S86/YhUdhzts6GvuKX7i+wCpGfpDhzXmIvkMutTO/J0nB1rSiyY+way4rUG6BXBVnkZJsQOEeSDy8rLKuxIetCVI2RFxUMF4NeOVYx1EiFBAS6wChTHqIwRdSSeO8JwD8A/3foFfAD8EGyfonmwrBb627IvmiKC38ARzykClPytIwd4veIarSA01Oj6kDxrzHZviCTDUgmAeznnMM4nCIJR9Ftg/Rlu3Zte3z7Tbov1c3vsj2WP54gLThWwShu6tHtrGDeLM8XVYAEygKZ6F8DAxAYqoe3a926uDQM5pbLvCqJyr/bWNoE7aT7fqOWzfXvHLhoWSAsc8muGG0WrlMLYmFHyTOXqYnUgoKjwHvF1gOi/HUjsYX/TUYkBjDuQLY8mG2xf5q6gZr849po50yPsBbTmNojmpwLfgEkMJ40410mf30/0w5k2qSlo8C0sX+04599kC5eMf/klDxHKIDOMFCZoo0J2uvsW2QPv5ShDH2w/eXxb2XZPUURezze3/qg1bTVEybPyHXgPmMG1E/CphJYJyZMMfFK6HGIPRm8VXFXlIgMO9Bl10JLFxWx+77i1X/JZUZjaqqMlOHYvv+D0fajvU0o7r2cHsHU/sREHkgJlRaPYch/m6+zaTOXJFAsusqa78PFKam8127qXclWgisLK6MTbrpGMLUNLBtZXNw8O3aL6k7xkFQ+KH7qm5WuSJEDsZeu/J188IkClmrxUQ8Zk5k9uII4zTk+KzI6leXi5Jzk5jOQwnGl/crZY8ACHNKRNPTw6sN28ma5r3BHWPlp55QJ3HBnf9vn6fwsplstaoIj0W7QRRoyS5z/ZlA4i/6qBPeExg6EMdwhYJNHnNRhJdNlG3kdGQ423xgUIwBHiI+1Ipfx1TcS0/ke0MO426tUPdnCTMTZVXM0Z/JyJwgThOpc0XhGIvYbEDyLV5mOOgWtFmhd9Y1punz2ddhdZkHb8SAbFjg1a2sDViUBFGqp2d3mQ86DqT3ENCn/HZ1ffYc9kyL7QGCOlDHHoUmDnF44WOYofJLUmobRM+OC/LMzWL2d//emsc1fXi3wckokHzTdSz/NzgF59EOS5E8lrNu/iKb6iUMEBa4vYnpV5RejnXsPoZe+hl6a/nLyR7egvYgufNnngN/xcEsNwR6Ei9rzwQqO/8lbG34SkyNxcuLQzgTaQ5tbFIqjHTjRT1CawGH9h0Ts+pAinRkEkpxpNU6KeKeYIt9MmTTG5vA6GOfVvRPX1zPN1qO9QcsatquE3zO9IqfzFyNmclpZINhMmfxj5BD3Tj5HP6hzZTZ/oKOCNSlsK6wRiJPHiIK2Nk21JyFdK4au+c4h1PpvfvC+6JjTolTY6hcWbKcnkCyXTli7ud8vz4zyBJ+hVM9f5CIUt5IIJI1sIBW/Ew8+QpLLFU+7kKl/sde9JshaSaddowpHqQGK0X0D/ZBYDpqLCBh/3LkEjW5y4SYI+s4RJAOxz9F4kSmnISVQIFBZeIqshbIsDxjCMTaCiYcf/ZQrKSuGFHHZSSnTY0YFdDp2wFB2Jnuk6MwyQJdL8SQ8C5fIX7ocP8+v63/Zm6T2hYivezVYKb1ZYhyn9Xu2gsYmVqBE048jzUlHh4I3vZGFOVb5kfUXBxgZultIq2tRqdpjF0RIRCTf/5qXsXfkP7TGjUPqwOXBaC00USb47XAiIInEKudcR7jEAgZzhb33YI+IjlOo/0sQ6jmvDoFMgQwkoGYK3Zu+G59uhahRuXEpvbMXdIJlhU4LkM9hAtrk3ILgeb8fZ7nv+soLaeJGpj68sXGa7RApG2YelM91obUdSeurH43x7kZKyP6phnb2/MT5gXFvpyJ3ojOpTYg/6BcXWDAWbEUoYk55BUeb57LBIlQTSSptSHAK1yehcbFhn8JKnS4nxKNklv6D8mi1JyME6rdcdczo73grRrW0elLNFgdzOicZPQwlr5SV4A7mUXXA+hurqvao2qB1mxVtGxXdG2haH13XI/ta1uCnjHXLEFg1Jqp2UQVUp/l4KY7vYadNNpBcPamQUvdhSw2YZ29ouJWxn+1qpqwkgXIJTMkjGwZ+OMk10vGpLQ5+lr+0Ae/qo9S3UoLmY2RkhAqvDiudMrvfjAMRUr7VYkrb4PyNGstc2MPoBBMdpzIMPMJmNU0YqKbDpYuEy8flDingEciS8RYebZW5ZzSF6zwG2AX0caHhyx9ejGSgVlSAGABPczHX+dIOZ2qXC8+SoT9ygyzfXHz7gT6Um0U7pKnYVSlY5uOKNpQ7+9/arL24fC0YXPTdxNoszDcxNZTEjZJ/ZUF+n5O2aocZ26q6zVgLb8Ohi3eTYwq7QuKTGpUDwC0aEQ/Tjn4xsEyUgkR1dSiUy7aMCA5cNVkEjdR0OtHnG1ftP9iMKn0QJ+cw35TzxW5HyfJ9tnKZzU1zdj2OYoVLGSpfTxcNFkk3f/rDyQb3cPbR3Sc1+eKkjW1C+WCghirglPQVBKB6cc3IkbsWfH78Ry3KXDRfci33yrNU4qzNSuxQHBN/oNG2G9KM+eNC7mpXsEtVej8tEQeWyMWJc2qwqtghieU6zbTsj2UZD6Z6C9oySFjjVNaotRGpRTHoouYUXah++owe/lUWO0Vp5PSDKOJhWF6eGxlj5AssGnaHsvhK4C8vyc9DyUOmTczWvWJREMHxMfp00W1foN+e8sO5KajxMt95hLu6TT93iOqudAjiP/Ygixsck1XMvk/G19Lrm5eEHs2koiX0Vsp7imDcraXsOWvkQstnIe87YdLzS4WLlhQfzX5YPmiKoNB10v8GyCN66bjSLGLxODa4v21bSShKzYwdjp9ncoaxRziBBvZ7WTbf3ayduLiTS2V8UPrGjnT1snmRTHxKEpPPtw5cd7sfdQrYf1ee9Crx+4xxUhklTNC0bTKsFb7Rjrc/bMKd6IK3//EbhxCNYJL9RGkSMfW95sJmbvKkB1u/SAAgNHsDi7/czypgRnwlUdEXAADwsMPDgEd4tOR1V9+wf11zsVdYKABIkBaKnxTGC4cI/B9WaA4fR7aKgXE4dPnD2kdQNVDmAxkS0BYDXPIUPD9Slg/c56OfhQj2BSs+bw8FNVNEDBBaFvBLLGTm8c8dzIl/dEAvETBnBZnJnXH+Gz1a5jqiAfllZI8Bm+FyoOBGBgDUupTEQjwHYhEMm2JRHkbEYpwUxxJEcQKium8AjNqVKFKrSrn+6jXzlqJMhRa1ithlKGPXpIqqreTPh1/gCmempQ4NKh1aSQIhAMFNk4C+Q7j3noWyeW/jN0hlliC8VXPVOgl6dDXoIKCKhZUjgRsD3NEVwM9AUATFZHdlWyhlS0Xa+CjQFqAWm7THpgyUOaQMWlWtUj5QkFPfPwQ=); }</style></defs><g transform="translate(127.65595739800483 10.199267734855312) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(10 107.22598213647916) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(77.18849687948023 107.29345326784187) rotate(0 238.27987670898438 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#be4bdb" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did:plc:fpruhuo22xkm5o7ttr2ktxdo</text></g><g transform="translate(555.6676469279228 103.79446148765874) rotate(0 19.36199188232422 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/...</text></g><g transform="translate(192.07819346470933 10) rotate(0 81.87196350097656 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#be4bdb" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">danabra.mov</text></g><g stroke-linecap="round"><g transform="translate(257.80954609590844 52.94637102860088) rotate(0 -0.4996832868473575 20.03146835490611)"><path d="M-0.74 -0.6 C-1.13 6.09, -1.97 33.36, -2.05 40.16 M1.07 1.71 C1.02 8.6, 0.64 35.1, 0.11 41.75" stroke="#be4bdb" stroke-width="1" fill="none"></path></g><g transform="translate(257.80954609590844 52.94637102860088) rotate(0 -0.4996832868473575 20.03146835490611)"><path d="M-6.94 20.91 C-3.81 28.51, -1.71 36.77, 1.96 40.07 M-5.33 23.53 C-4.91 27.88, -2.14 32.79, 1.06 42.03" stroke="#be4bdb" stroke-width="1" fill="none"></path></g><g transform="translate(257.80954609590844 52.94637102860088) rotate(0 -0.4996832868473575 20.03146835490611)"><path d="M6.75 21.48 C5.45 28.81, 3.12 36.88, 1.96 40.07 M8.37 24.1 C5.38 28.43, 4.75 33.19, 1.06 42.03" stroke="#be4bdb" stroke-width="1" fill="none"></path></g></g><mask></mask><g transform="translate(355.99007346395956 10.18969538392048) rotate(0 19.36199188232422 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/...</text></g></svg>
+4
public/where-its-at/7.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 572.1144746598547 151.27917129494972" width="1144.2289493197095" height="302.55834258989944"><!-- svg-source:excalidraw --><metadata></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><g transform="translate(65.21993399498024 10.544292024565493) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(10 106.27917129494972) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(76.05731956727686 106.17546758490653) rotate(0 223.3979034423828 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#c2255c" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did:plc:5c6cw3veuqruljoy5ahzerfx</text></g><g transform="translate(523.3904908952063 106.24975061908344) rotate(0 19.36199188232422 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/...</text></g><g transform="translate(451.8358033952063 10.343500619083443) rotate(0 19.36199188232422 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/...</text></g><g transform="translate(129.9670889702793 10) rotate(0 160.81793212890625 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#c2255c" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">barackobama.bsky.social</text></g><g stroke-linecap="round"><g transform="translate(260.0240310104273 52.94637102860088) rotate(0 -0.33916207493894035 20.978279196436006)"><path d="M-0.88 -0.84 C-1.3 6.22, -1.48 35.53, -1.52 42.8 M0.85 1.34 C0.74 8.02, 1.25 34.53, 0.74 41.11" stroke="#c2255c" stroke-width="1" fill="none"></path></g><g transform="translate(260.0240310104273 52.94637102860088) rotate(0 -0.33916207493894035 20.978279196436006)"><path d="M-7.39 19.83 C-1.97 27.99, 0.53 37.09, 2.52 42.12 M-5.1 21.5 C-3.6 29.26, -0.15 35.99, 1.38 40.54" stroke="#c2255c" stroke-width="1" fill="none"></path></g><g transform="translate(260.0240310104273 52.94637102860088) rotate(0 -0.33916207493894035 20.978279196436006)"><path d="M6.95 20.15 C6.94 28.08, 4.01 37.06, 2.52 42.12 M9.25 21.82 C5.09 29.43, 2.88 36.03, 1.38 40.54" stroke="#c2255c" stroke-width="1" fill="none"></path></g></g><mask></mask></svg>
+4
public/where-its-at/8-full.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 840.853849410727 140.8998102444716" width="1681.707698821454" height="281.7996204889432"><!-- svg-source:excalidraw --><metadata></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><g transform="translate(430.958766357915 10) rotate(0 75.01197052001953 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#228be6" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">the format</text></g><g transform="translate(156.1891126622795 11.919627443315676) rotate(0 57.35797882080078 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">the user</text></g><g transform="translate(381.3639084684073 85.82261819415089) rotate(0 123.21464997310977 -17.40024157983953)" stroke="none"><path fill="#228be6" d="M -2.65,-0.19 Q -2.65,-0.19 -2.61,-1.20 -2.56,-2.20 -2.11,-3.73 -1.67,-5.26 -0.76,-6.50 0.14,-7.74 0.93,-8.58 1.72,-9.42 2.72,-10.31 3.72,-11.20 4.87,-12.16 6.02,-13.12 6.95,-13.79 7.87,-14.45 8.76,-15.10 9.65,-15.75 10.52,-16.38 11.38,-17.02 12.73,-18.01 14.08,-19.01 14.99,-19.61 15.90,-20.22 17.01,-20.94 18.11,-21.67 19.19,-22.19 20.27,-22.71 21.47,-23.28 22.66,-23.84 23.73,-24.25 24.79,-24.65 26.29,-25.18 27.79,-25.70 29.12,-26.23 30.45,-26.76 32.14,-27.29 33.82,-27.81 35.18,-28.09 36.54,-28.38 38.08,-28.47 39.61,-28.55 40.76,-28.57 41.91,-28.59 43.43,-28.61 44.95,-28.62 46.68,-28.59 48.41,-28.56 49.51,-28.53 50.62,-28.49 52.96,-28.28 55.30,-28.07 56.40,-27.84 57.51,-27.61 59.72,-27.19 61.94,-26.78 63.64,-26.51 65.34,-26.24 66.39,-26.09 67.44,-25.94 68.99,-25.72 70.54,-25.50 72.20,-25.27 73.86,-25.03 75.49,-24.80 77.13,-24.56 79.21,-24.11 81.29,-23.67 83.12,-23.29 84.94,-22.91 86.69,-22.56 88.44,-22.22 89.97,-21.96 91.50,-21.70 92.99,-21.68 94.49,-21.66 95.55,-21.68 96.61,-21.70 98.07,-21.71 99.52,-21.71 100.97,-22.21 102.43,-22.70 104.13,-23.59 105.84,-24.47 107.96,-25.50 110.09,-26.52 111.80,-27.38 113.50,-28.23 114.80,-28.96 116.11,-29.68 117.10,-30.25 118.10,-30.82 119.12,-31.55 120.15,-32.29 121.35,-32.44 122.54,-32.60 122.71,-33.82 122.87,-35.04 123.85,-34.20 124.82,-33.36 125.71,-32.59 126.60,-31.82 128.02,-30.75 129.45,-29.67 130.77,-28.77 132.10,-27.86 133.72,-26.90 135.34,-25.94 136.64,-25.35 137.94,-24.76 139.18,-24.25 140.42,-23.74 141.99,-23.20 143.57,-22.65 144.74,-22.31 145.90,-21.96 147.10,-21.75 148.30,-21.54 149.56,-21.45 150.81,-21.36 152.19,-21.36 153.56,-21.36 155.04,-21.38 156.51,-21.39 157.59,-21.39 158.67,-21.40 160.62,-21.35 162.56,-21.30 163.90,-21.30 165.24,-21.30 166.53,-21.31 167.83,-21.31 169.11,-21.31 170.40,-21.32 171.80,-21.31 173.19,-21.31 174.84,-21.29 176.48,-21.28 178.18,-21.26 179.87,-21.25 181.52,-21.24 183.17,-21.23 184.52,-21.17 185.86,-21.11 187.15,-21.00 188.44,-20.89 189.52,-20.77 190.59,-20.65 193.08,-20.26 195.57,-19.86 197.97,-19.55 200.38,-19.24 201.72,-19.05 203.07,-18.86 204.98,-18.63 206.88,-18.40 208.50,-18.30 210.12,-18.19 211.71,-18.06 213.31,-17.93 214.37,-17.82 215.43,-17.72 217.41,-17.41 219.39,-17.11 221.70,-16.73 224.00,-16.35 225.36,-15.98 226.71,-15.62 227.82,-15.29 228.92,-14.96 230.70,-14.35 232.47,-13.75 233.83,-13.18 235.18,-12.60 236.22,-12.20 237.25,-11.79 238.33,-11.26 239.42,-10.72 241.01,-9.80 242.61,-8.87 243.71,-8.19 244.80,-7.51 247.33,-5.23 249.85,-2.95 249.93,-2.39 250.01,-1.84 249.91,-1.29 249.81,-0.74 249.53,-0.25 249.26,0.23 248.85,0.60 248.43,0.98 247.92,1.20 247.41,1.42 246.85,1.47 246.30,1.52 245.75,1.39 245.21,1.26 244.74,0.96 244.26,0.66 243.91,0.23 243.56,-0.19 243.36,-0.72 243.17,-1.24 243.15,-1.80 243.13,-2.36 243.29,-2.89 243.45,-3.43 243.78,-3.89 244.10,-4.34 244.55,-4.67 245.00,-5.00 245.54,-5.17 246.07,-5.33 246.63,-5.32 247.19,-5.31 247.71,-5.12 248.24,-4.93 248.68,-4.58 249.11,-4.23 249.42,-3.77 249.72,-3.30 249.86,-2.75 250.00,-2.21 249.96,-1.66 249.91,-1.10 249.70,-0.58 249.48,-0.07 249.11,0.34 248.74,0.76 248.25,1.03 247.77,1.31 247.22,1.42 246.67,1.53 246.12,1.46 245.56,1.39 245.06,1.14 244.56,0.89 244.16,0.50 243.76,0.11 243.51,-0.38 243.26,-0.88 243.26,-0.88 243.26,-0.88 241.97,-1.49 240.67,-2.10 239.64,-2.84 238.61,-3.59 237.63,-4.24 236.64,-4.89 235.13,-5.69 233.62,-6.49 232.54,-7.05 231.45,-7.62 230.17,-8.20 228.89,-8.77 227.05,-9.36 225.21,-9.94 224.17,-10.21 223.13,-10.48 220.82,-10.79 218.52,-11.10 216.66,-11.37 214.81,-11.64 213.11,-11.82 211.41,-11.99 209.84,-12.22 208.28,-12.44 207.22,-12.52 206.16,-12.60 204.28,-12.88 202.40,-13.17 201.03,-13.30 199.66,-13.43 197.23,-13.65 194.79,-13.87 193.27,-13.99 191.74,-14.11 189.89,-14.31 188.03,-14.51 186.95,-14.55 185.86,-14.60 184.52,-14.54 183.17,-14.48 181.52,-14.47 179.87,-14.46 178.18,-14.45 176.48,-14.43 174.84,-14.42 173.19,-14.40 171.80,-14.40 170.40,-14.39 169.11,-14.39 167.83,-14.40 166.53,-14.40 165.24,-14.40 163.90,-14.40 162.56,-14.41 160.62,-14.36 158.67,-14.31 157.59,-14.31 156.51,-14.32 155.02,-14.34 153.53,-14.35 152.02,-14.39 150.50,-14.43 149.03,-14.58 147.56,-14.72 146.35,-14.95 145.14,-15.17 143.96,-15.51 142.78,-15.84 141.41,-16.28 140.05,-16.73 138.39,-17.32 136.74,-17.92 135.22,-18.60 133.71,-19.28 132.13,-20.08 130.54,-20.88 129.51,-21.42 128.47,-21.97 127.11,-22.84 125.76,-23.72 123.87,-24.95 121.99,-26.19 120.68,-27.28 119.37,-28.37 118.47,-29.22 117.56,-30.06 116.96,-30.96 116.35,-31.86 116.21,-33.31 116.07,-34.75 116.45,-35.79 116.84,-36.83 118.80,-37.57 120.76,-38.30 121.94,-37.86 123.12,-37.41 124.07,-36.48 125.02,-35.56 125.62,-34.48 126.22,-33.40 126.24,-31.91 126.26,-30.43 125.82,-29.37 125.38,-28.31 124.30,-27.33 123.22,-26.36 122.26,-25.76 121.30,-25.16 120.18,-24.59 119.05,-24.01 117.69,-23.31 116.32,-22.61 114.62,-21.75 112.92,-20.90 110.93,-19.82 108.94,-18.73 107.89,-18.12 106.83,-17.50 105.01,-16.63 103.19,-15.76 102.16,-15.45 101.12,-15.15 99.60,-15.08 98.08,-15.02 96.78,-15.04 95.49,-15.06 94.35,-15.10 93.21,-15.14 91.76,-15.27 90.32,-15.40 88.79,-15.69 87.27,-15.99 85.36,-16.35 83.45,-16.72 81.77,-17.08 80.08,-17.44 78.17,-17.82 76.25,-18.19 74.63,-18.45 73.00,-18.70 71.29,-18.95 69.59,-19.21 68.12,-19.47 66.66,-19.72 65.51,-19.85 64.36,-19.98 62.68,-20.28 61.01,-20.57 59.57,-20.72 58.13,-20.87 56.36,-21.19 54.59,-21.51 52.61,-21.58 50.62,-21.65 49.51,-21.61 48.41,-21.58 46.68,-21.55 44.96,-21.52 43.46,-21.53 41.96,-21.53 40.88,-21.53 39.80,-21.52 38.22,-21.38 36.63,-21.25 35.19,-20.88 33.75,-20.52 32.38,-20.01 31.00,-19.50 29.48,-18.96 27.95,-18.43 26.68,-18.02 25.40,-17.60 24.29,-17.10 23.17,-16.59 22.00,-15.93 20.82,-15.27 19.51,-14.38 18.20,-13.48 16.81,-12.47 15.41,-11.46 14.13,-10.62 12.84,-9.79 11.84,-9.16 10.84,-8.53 9.56,-7.61 8.28,-6.70 7.14,-5.87 6.01,-5.04 5.14,-4.25 4.28,-3.47 3.57,-2.61 2.86,-1.75 2.75,-0.77 2.65,0.19 2.59,0.51 2.53,0.82 2.39,1.12 2.26,1.41 2.06,1.66 1.85,1.91 1.60,2.10 1.34,2.30 1.05,2.42 0.75,2.55 0.44,2.61 0.12,2.66 -0.19,2.63 -0.51,2.61 -0.82,2.51 -1.12,2.41 -1.40,2.24 -1.67,2.07 -1.89,1.84 -2.12,1.61 -2.28,1.33 -2.44,1.05 -2.53,0.75 -2.62,0.44 -2.64,0.12 -2.65,-0.19 -2.65,-0.19 L -2.65,-0.19 Z"></path></g><g transform="translate(486.06204032869573 94.03826857217109) rotate(0 172.39590454101562 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">.feed.post/3lzy2ji4nms2z</text></g><g transform="translate(10 95.89981024447161) rotate(0 32.40998077392578 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">at://</text></g><g transform="translate(356.68060629433603 94.003195703041) rotate(0 7.853996276855469 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#868e96" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">/</text></g><g transform="translate(371.1305968965171 94.15507371104513) rotate(0 57.189979553222656 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#228be6" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">app.bsky</text></g><g transform="translate(74.6448923167045 95.1575737596163) rotate(0 141.28793334960938 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did:web:iam.ruuuuu.de</text></g><g transform="translate(85.46012783036349 90.90971884389364) rotate(0 133.9296875 -11.7109375)" stroke="none"><path fill="#6741d9" d="M -2.62,0 Q -2.62,0 -2.64,-1.14 -2.66,-2.29 -2.74,-3.64 -2.81,-5.00 -2.76,-6.21 -2.70,-7.43 -1.73,-8.90 -0.75,-10.37 0.07,-11.12 0.89,-11.87 2.22,-12.58 3.54,-13.28 4.57,-13.61 5.61,-13.94 6.95,-14.35 8.29,-14.76 10.01,-15.24 11.73,-15.71 13.20,-15.95 14.67,-16.19 16.08,-16.48 17.48,-16.76 18.63,-16.95 19.78,-17.14 21.04,-17.22 22.30,-17.29 23.68,-17.29 25.07,-17.29 26.51,-17.26 27.95,-17.23 30.06,-17.26 32.17,-17.28 34.01,-17.16 35.85,-17.05 37.32,-17.02 38.78,-17.00 39.93,-17.02 41.09,-17.04 42.26,-17.05 43.44,-17.07 45.59,-17.11 47.74,-17.15 49.25,-17.10 50.77,-17.06 51.94,-16.98 53.10,-16.90 55.21,-16.77 57.31,-16.63 58.82,-16.40 60.33,-16.17 62.36,-16.10 64.39,-16.03 65.52,-16.02 66.66,-16.01 67.83,-16.01 69.00,-16.01 70.99,-16.04 72.98,-16.07 74.08,-16.08 75.17,-16.08 76.56,-16.05 77.94,-16.02 79.06,-16.03 80.18,-16.04 82.15,-16.07 84.11,-16.10 85.84,-16.14 87.57,-16.18 88.98,-16.14 90.39,-16.09 91.52,-16.09 92.65,-16.10 94.48,-16.13 96.31,-16.16 97.59,-16.21 98.86,-16.26 100.32,-16.58 101.78,-16.91 102.65,-17.70 103.52,-18.49 104.57,-19.31 105.62,-20.13 106.62,-21.09 107.62,-22.04 106.84,-21.14 106.05,-20.24 105.99,-21.35 105.94,-22.45 106.53,-21.36 107.12,-20.26 108.20,-20.31 109.28,-20.35 110.44,-21.34 111.60,-22.33 112.35,-21.27 113.09,-20.21 113.94,-19.53 114.80,-18.85 115.90,-18.14 117.00,-17.43 118.15,-16.96 119.29,-16.48 120.55,-16.13 121.81,-15.78 123.01,-15.63 124.21,-15.48 125.56,-15.43 126.90,-15.38 127.98,-15.38 129.05,-15.37 130.54,-15.37 132.03,-15.37 133.10,-15.37 134.17,-15.37 135.57,-15.35 136.97,-15.33 138.29,-15.32 139.60,-15.31 140.95,-15.31 142.30,-15.30 144.45,-15.82 146.61,-16.35 148.12,-16.80 149.62,-17.24 151.41,-17.72 153.21,-18.19 154.95,-18.70 156.70,-19.22 158.64,-19.70 160.58,-20.18 162.35,-20.38 164.12,-20.58 165.78,-20.82 167.43,-21.07 168.82,-21.23 170.20,-21.40 172.64,-21.53 175.09,-21.67 176.71,-21.79 178.32,-21.91 179.62,-22.10 180.92,-22.28 183.15,-22.22 185.38,-22.16 187.07,-22.29 188.77,-22.41 190.61,-22.46 192.45,-22.50 194.09,-22.55 195.72,-22.59 197.27,-22.62 198.82,-22.66 200.33,-22.68 201.85,-22.71 203.34,-22.73 204.84,-22.75 206.33,-22.77 207.83,-22.79 209.31,-22.80 210.80,-22.82 212.48,-22.80 214.15,-22.78 216.11,-22.70 218.07,-22.62 219.75,-22.62 221.44,-22.62 222.84,-22.66 224.24,-22.70 225.69,-22.73 227.14,-22.76 228.97,-22.71 230.80,-22.67 232.11,-22.72 233.42,-22.76 234.50,-22.82 235.59,-22.88 237.07,-22.79 238.54,-22.70 239.72,-22.47 240.90,-22.25 242.48,-21.81 244.05,-21.38 245.28,-20.91 246.50,-20.45 247.52,-20.02 248.55,-19.58 249.57,-19.11 250.60,-18.63 251.75,-18.12 252.89,-17.61 254.30,-16.80 255.71,-15.99 257.15,-15.17 258.58,-14.35 260.23,-13.21 261.88,-12.07 263.35,-10.91 264.82,-9.76 265.97,-8.83 267.13,-7.91 268.11,-7.03 269.09,-6.15 270.14,-3.56 271.18,-0.97 271.09,-0.44 271.01,0.09 270.76,0.56 270.51,1.04 270.12,1.41 269.73,1.78 269.25,2.01 268.76,2.24 268.22,2.30 267.69,2.36 267.16,2.25 266.64,2.13 266.17,1.86 265.71,1.58 265.36,1.17 265.01,0.76 264.81,0.27 264.61,-0.22 264.58,-0.76 264.55,-1.30 264.69,-1.82 264.83,-2.34 265.13,-2.78 265.43,-3.23 265.86,-3.56 266.28,-3.88 266.79,-4.06 267.30,-4.23 267.84,-4.23 268.38,-4.24 268.89,-4.07 269.40,-3.90 269.83,-3.57 270.26,-3.25 270.56,-2.80 270.87,-2.36 271.01,-1.84 271.16,-1.33 271.13,-0.79 271.10,-0.25 270.91,0.24 270.71,0.74 270.36,1.15 270.02,1.56 269.56,1.84 269.10,2.12 268.57,2.24 268.05,2.36 267.51,2.30 266.98,2.25 266.49,2.02 266.00,1.80 265.61,1.43 265.22,1.06 264.96,0.59 264.71,0.11 264.62,-0.41 264.53,-0.94 264.53,-0.94 264.53,-0.94 264.49,-1.18 264.46,-1.41 263.67,-2.22 262.89,-3.02 261.77,-3.97 260.65,-4.92 259.41,-5.91 258.17,-6.90 256.76,-7.88 255.36,-8.87 253.79,-9.79 252.22,-10.71 251.05,-11.43 249.88,-12.16 248.87,-12.69 247.86,-13.23 246.77,-13.84 245.68,-14.46 244.45,-15.19 243.22,-15.93 242.11,-16.46 240.99,-16.99 239.58,-17.48 238.17,-17.97 236.88,-18.09 235.59,-18.21 234.50,-18.26 233.42,-18.32 232.11,-18.37 230.80,-18.41 228.97,-18.37 227.14,-18.33 225.69,-18.35 224.24,-18.38 222.84,-18.42 221.44,-18.46 219.75,-18.46 218.07,-18.46 216.11,-18.38 214.15,-18.31 212.48,-18.29 210.80,-18.27 209.32,-18.28 207.83,-18.29 206.34,-18.31 204.85,-18.32 203.35,-18.34 201.86,-18.36 200.36,-18.38 198.85,-18.39 197.32,-18.40 195.79,-18.41 194.20,-18.40 192.60,-18.39 190.87,-18.30 189.14,-18.22 187.33,-18.04 185.53,-17.86 183.53,-17.65 181.54,-17.44 180.14,-17.28 178.74,-17.12 177.23,-16.97 175.72,-16.83 173.31,-16.32 170.89,-15.81 169.76,-15.67 168.63,-15.52 166.86,-15.03 165.09,-14.53 163.71,-14.23 162.32,-13.92 160.35,-13.34 158.38,-12.77 156.64,-12.22 154.90,-11.68 153.46,-11.29 152.01,-10.91 150.15,-10.31 148.29,-9.71 147.24,-9.39 146.20,-9.08 144.25,-8.63 142.31,-8.18 140.95,-8.18 139.60,-8.17 138.29,-8.16 136.97,-8.15 135.57,-8.13 134.17,-8.11 133.10,-8.11 132.03,-8.11 130.53,-8.11 129.03,-8.12 127.91,-8.12 126.79,-8.13 125.51,-8.16 124.23,-8.19 123.16,-8.34 122.10,-8.50 121.03,-8.59 119.96,-8.69 118.80,-8.97 117.65,-9.24 116.44,-9.68 115.22,-10.12 113.86,-10.83 112.50,-11.53 111.51,-12.18 110.52,-12.82 109.45,-13.63 108.38,-14.45 107.46,-15.41 106.54,-16.36 105.74,-17.47 104.95,-18.58 104.65,-19.64 104.34,-20.70 104.36,-21.91 104.39,-23.13 104.77,-24.25 105.15,-25.37 106.12,-25.92 107.09,-26.48 108.70,-26.57 110.32,-26.66 111.29,-25.82 112.25,-24.98 112.66,-23.58 113.08,-22.19 113.04,-21.09 113.00,-19.99 112.79,-18.95 112.58,-17.90 111.55,-16.79 110.52,-15.68 109.21,-14.63 107.90,-13.57 106.91,-12.79 105.92,-12.02 104.95,-11.44 103.97,-10.86 102.50,-10.46 101.03,-10.05 99.94,-10.09 98.86,-10.13 97.59,-10.17 96.31,-10.22 94.48,-10.25 92.65,-10.28 91.52,-10.29 90.39,-10.29 88.98,-10.24 87.57,-10.20 85.84,-10.24 84.11,-10.28 82.15,-10.31 80.18,-10.35 79.06,-10.35 77.94,-10.36 76.56,-10.33 75.17,-10.30 74.07,-10.31 72.97,-10.31 70.97,-10.35 68.97,-10.39 67.78,-10.40 66.59,-10.41 65.41,-10.43 64.22,-10.45 63.03,-10.50 61.85,-10.55 60.76,-10.65 59.66,-10.76 57.16,-11.03 54.67,-11.31 52.72,-11.50 50.77,-11.68 49.25,-11.64 47.74,-11.60 45.59,-11.64 43.44,-11.68 42.26,-11.69 41.09,-11.71 39.94,-11.73 38.78,-11.75 37.32,-11.72 35.85,-11.70 34.02,-11.58 32.18,-11.46 30.09,-11.47 28.00,-11.48 26.60,-11.43 25.20,-11.37 23.93,-11.32 22.66,-11.26 20.82,-11.07 18.98,-10.89 17.48,-10.43 15.98,-9.98 14.63,-9.62 13.28,-9.26 11.96,-8.92 10.65,-8.57 9.09,-8.10 7.54,-7.64 6.08,-7.17 4.61,-6.71 3.71,-5.85 2.81,-5.00 2.74,-3.64 2.66,-2.29 2.64,-1.14 2.62,0 2.58,0.31 2.54,0.62 2.43,0.92 2.32,1.22 2.14,1.48 1.96,1.74 1.72,1.95 1.49,2.16 1.21,2.30 0.93,2.45 0.62,2.53 0.31,2.60 -0.00,2.60 -0.31,2.60 -0.62,2.53 -0.93,2.45 -1.21,2.30 -1.49,2.16 -1.72,1.95 -1.96,1.74 -2.14,1.48 -2.32,1.22 -2.43,0.92 -2.54,0.62 -2.58,0.31 -2.62,-0.00 -2.62,-0.00 L -2.62,0 Z"></path></g></svg>
+4
public/where-its-at/9.svg
··· 1 + <?xml version="1.0" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 457.61706642137506 503.22839841437144" width="915.2341328427501" height="1006.4567968287429"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1a21LbSlx1MDAxNn3nKyjPy0xV6NP3S55cdTAwMDZcYlx1MDAwNFx1MDAxMpIhQDjA1KmUbMm2YllyJFx1MDAxOVx1MDAwM6fy77PbgFuWZGODXHUwMDE5cmYwVZTdarW2utda+9L959r6eiO/XHUwMDFlXHUwMDA0jbfrjeCq5UWhn3qjxlx1MDAxYtt+XHUwMDE5pFmYxHCJjn9nyTBtjXt283yQvf3tN3dcdTAwMDdqJf3bu4Io6Fx1MDAwN3GeQb9/w+/19T/H/+FK6Nt7T0/OPlx1MDAxZX/61CPse/phO+nuqej8cHzruNO9MXlwlbvWK2tcdTAwMDTlXFwhSZlkXGYrIdnk6jVcXN2g2lAkuWRGXHUwMDEzgrHgcnJ9XHUwMDE0+nnXjqApXHUwMDEySmgppTGUaCUmXbpB2Onm0Ie5Ni/uRNZcdTAwMTY8acnyNOlcdTAwMDXbSZSk1sa/KSNwmzozm16r10mTYey7Pk3aps2m69NcdTAwMGWj6Di/XHUwMDFlj9xopUmWbXS9vNVtlJ7y+53RpNQ+uTdLYOrdXfDYTjdcdTAwMGWy7H69xq3JwGuFuZ0gmJNJq7VxsO+P1+hcdTAwMGZnWer1g327SPEwiibNYexcdTAwMDd2/lx1MDAxYs39fHvqebF/97yp/llcdTAwMTDYMThcdTAwMTZYYqbdMjk8XHUwMDExJmi5+XNcdTAwMTKPwcU44Vx1MDAxYf5cXIcwe1x1MDAwN6DKx6O2vShcdTAwMGLcfFtcdTAwMWJ2yoArgq5cdTAwMDC8kfdlzzS/XHUwMDFk/L57eXGlg3ZOvfNk8j5T4PPSNFx1MDAxOTUmV36+mTdu+7RcdTAwMTfeXHUwMDFjjr5e3Vxc+cOL9/nRWffz98XGvfvmlmA48L3bXHUwMDE3JUpcdTAwMThOlVGMXHUwMDE3kFx1MDAxY4VxrzzfUdLq1czNmEDwTD/0346C5tvQ66N0aD/IXHUwMDBmXG5wTOL8OLxcdMb0mGrd9fphZJEjpsbcjMKOXcNGXHUwMDA0U9gormJcdTAwMWWCXHUwMDFhTC7nycBdbcF4Xlx1MDAxOFx1MDAwN2lcdTAwMTVeSVx1MDAxYXbC2ItOXHUwMDE2s9dcdTAwMWLmyVGQ3Vqcp8OgODfB3j2NXHSiYq2wdiVcdTAwMTFcbnP1/f2R1/2sdne3XHUwMDBmm+z63cbX88VESFCGlMGGgtbQwpTdipDiXHUwMDEyUc4wlVRgxlVVhIhcdTAwMTRIc2ZcdTAwMTgjVqSeLkIksH//R1witLu4XGJcdTAwMDGFjCRGVtTG2lJ0ISVcdTAwMTFcdTAwMDJcdTAwMWaBXHUwMDE5XHUwMDA1V7FqXHUwMDExel6xMEIz/lx1MDAxNLFoRlx1MDAwMJ6sd428weBcdTAwMTfWiHozVyNccnuHnZtcdTAwMGKxu32+N9pJU7pcdTAwMTf3r7ZcdTAwMGZcdTAwMTaVXHUwMDA2gqQ2mlx1MDAxYqWFpmpaXHUwMDFiXHUwMDE4llxmUYOpIFRcdTAwMThcdTAwMDVerqpcclx1MDAwMiONQWFcdTAwMTTIXHUwMDAypVx1MDAwNfVw0sCRoVx1MDAxYTODJVx1MDAxN1hp/Vx1MDAxYa/MlorLxZWCXGJBNcSVXGZcdTAwMTfhelx1MDAxZv9cdTAwMTKiys33Ulx1MDAwMbEkXHUwMDEzXHUwMDA0XHUwMDFix7uXj1dcdTAwMTaQXG5ccqGFKlx1MDAxMmRZqfjnXHUwMDAzwYRC8Fx1MDAxY85cdTAwMDDrRFx1MDAwMvLJVKdcdTAwMTfSjTqbV6NcdTAwMWHzXHUwMDE3q7RQZdkwXHUwMDAyKVx1MDAwMvCDoFx1MDAwMnKXsmpcdTAwMTCNXGLFXHUwMDA2IFx1MDAwNrKgXHUwMDFkzCaigbSSituPzYHArVVVg8AgXHUwMDAwbfBqSkt4knZ9XHUwMDFlVlxyXHUwMDE4nPjmeVWDltpfUDU+0HrVcFx1MDAxNL1fTnrX8tNcdTAwMTl/JyaUXHUwMDE5KTisV42YXGJsZmlcdIE1ZERo8pi4Y+p1amJcdTAwMDNIeXFcdTAwMTHbi1x1MDAxMn6QhGW1ct/WXHUwMDFkbsY/Jt//eFPbe2M2Wu2nilM3YEXUXCIvy7eTfj/M4U1cdTAwMGatlVx1MDAxNW3PvTTfgqVccuPO9Fxu3lUp9lx1MDAxN3D5Y8lqXHLtXGZghFx05YRcdTAwMTCGsdBgI1G80K3jXHLGXHUwMDEwhFxmXHUwMDAweoDyKWaLXHUwMDE2ulx1MDAwMpMg9lx1MDAxZrZqfqFk2ipGNFx1MDAxM8pIjaVcdTAwMTBS0YpRXHUwMDEyKc5gRrG1XGarXHUwMDFh5Nqp2rTy1FxyPL9Gq8DmuZejZjJy6JkjlV9cdTAwMDb9TfzNw73r3uFFckjM6ObqZrFcdTAwMDBcdTAwMGKyJlx1MDAwNIG5fU1gXHR3WngrlYRIpIlQklx1MDAxOYivIOyvaqUmIKbwXHUwMDA3cTKsI61LvqhAWClFlDRcdTAwMDT8pHReciW5WJ56cTbwUljnZ5BL38u6wX9ZL9VcdTAwMTJRlsZcXFx1MDAwMlx1MDAxME1dlFx1MDAwNXo5U1x1MDAxOJmm0sjHJWRcdTAwMGZcYuOTI6GuXHUwMDE3+1Gw/vdsXHUwMDA06YjXjIJ/1EdEwFNJQTGIYJxpUyjrvGBINM/41YRGPlx1MDAwZnZIe1x1MDAxNPBItEJcdTAwMTGfXHUwMDFl5Dv7n1x1MDAxNuO7hCjS8lRcdTAwMTHQ0UrBVyqFrCdcdTAwMTHMXHUwMDE2ZbSDd5HtXHUwMDEy9Fx1MDAwMmRcdTAwMTiUg1x1MDAxOOpGcGxnSGEhJThcdTAwMWQmMfhs1+eV7vV0P12C79JoiF1ZoSrh+K7VzFwiMKRcZlx1MDAxYZZcdTAwMTXTx1SB5/JcdTAwMWT8JLi/p/E9yXJw3Vx1MDAwZlx1MDAxMlx1MDAxZWJcdTAwMTlcdTAwMDNJOlx1MDAwM8mTXHUwMDEwuDP+S9RO5lq/XHUwMDFhxlx1MDAxZiTvf1xmN/HxxX56MvLTo6/koD+oMj5cclr5LcfKbl5TRLRmXHUwMDEw0Fxiq1x1MDAwMWU3XHUwMDBmqmBcdTAwMGIo4OdhKXnBh99cdTAwMTNcdTAwMWZYjFxmXHUwMDAzYVx1MDAxN7BcdTAwMDBMSeLg51JcIsGRYFZabG5cdTAwMDWOwEHtlfi1xNdL8F5RXG5cdTAwMTEy1bWF12Lpq0x8XG4pgSDU+cYlqimrrHfMgffHXHUwMDBmp63e4Ozrj/Bfx+Het2wjPj67XFxcbt5cZkF0yiw8wTGV4E01JFx1MDAxMEJBmC61UlJW3Vx1MDAxYcRPSEKKJCXXIJSiNuGXXHUwMDEygXxCXG5lNFe2mv2K7vnoNkugmzNQXHUwMDBlkKE6r0Z4YT0qblxyboUlU48qXHUwMDE2zoa3xlx1MDAxOLJcdTAwMWS9XHUwMDFheM/foniolKWQzc5cdTAwMDDiXHUwMDFjy/LmmOZcdTAwMWExwVxyXHUwMDAxKtrdd1aBNkXMSjJoulx1MDAwMFxuKKZrhNtcdTAwMDCywTNcdTAwMTjrXHUwMDA3hCyyeiWlrOdF9lx1MDAwYpSzNlx1MDAxN1x1MDAwNzaTlFx1MDAxMaZwXHUwMDA1wPZatTJ+XHUwMDBmaz3WIfmoXHUwMDEy+ENVq8fuaK20ajVcdTAwMWKW9lNcdTAwMDGkXHUwMDFibq007OpqVlx1MDAwYleHNiDttIdeIFx1MDAxNMJcZjMplDuSsX5fXHUwMDFlMohcdTAwMWF4Pc2YJuAoJ2W3JUtW87fVS0YpyLcgKqa2IOUy/4lJXHUwMDAyaVx1MDAwNkFcdTAwMDSGzM1cYsUlr5hUKVlNzePK6lVcdTAwMDctXHUwMDFjtnvB1oHhXHUwMDE3nY2b/eyTPlxiXHUwMDE33Fx1MDAxMFx1MDAxNJC/2uNI9tRcdTAwMTI15Vx1MDAwNNZwjow2XHUwMDEwJVx1MDAxOVxmUK96ekogOdXEYFuJqj8qXHUwMDAwSVx1MDAwNohcdTAwMWEjglx1MDAxMCaloVx1MDAwNVx1MDAxMv5cdTAwMDVcdTAwMWP9XHUwMDBiyOHWXHUwMDEy6atgQjFiRF1cdTAwMTm/uKVVdvPYYIiAiXDWrq5axVx0dlx1MDAwZn7ceaD6bFx1MDAxNbDIXGZcdTAwMTeQXHUwMDBlQcIueEF2X/Y00Kqz063L64ujkzBukzPqt+LBNSM3vUXrUbbWXHUwMDBlqVx1MDAwNKZYq0Jyc3f4R1x1MDAxM1x1MDAxMC2YO4qBk6Zmf58wxKUyVEJobrfw3Vxir4R+NKGjJVwid8j6sTZM10U4kI7NZLSARbWnRp9jY+6ph3ayIL1cZlvBTuyPg56/XHUwMDEwu2davlx1MDAxYaZfNbObXHUwMDFmwflJZN533iXf440vKlx1MDAxOC7IdEJcdTAwMTDTXHUwMDEwK2H4xsopOuBcdTAwMDBhq/JcdTAwMDYzZki1XHUwMDAwZYiNb1xiwE1CtFxiIU49z8Gtw8cwqYgh/8s810+n+TJHjyWHmInQuuJcdTAwMTM1otzq3DZcdTAwMDV1XHUwMDEwoLrP4LafuslcdTAwMDSNycc4XHUwMDE5xZvZTIJcdTAwMGLGKaWMXHUwMDFiXHUwMDA2ufUvsbtUa/XS5F67y6BcdTAwMWHeYHCcw8xOUpDGZVx1MDAxOIy2as6jtMdcdTAwMWZ7JGosXHJcdTAwMTb4wfhcdTAwMTDHz7Wf/1x1MDAwMfVcdDecIn0=<!-- payload-end --></metadata><defs><style class="style-fonts"> 4 + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAABLIAA4AAAAAH7QAABJ1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbhjwcNAZgAIEMEQgKrliiQAtAAAE2AiQDfAQgBYMYByAbbRijooaxWvRkf3FgG0sf6gGK42BxiCVMUIPQ8QiJJcCby6ViWNYxqUdlthNb7je8Hw9QGsI5/y9Jm9SMqoAXtYrhBYqIF9GZyt+YOrx9nk6UzZmY0vOxuP3v7kpXomrGCEVXjBEoWeyymFX+V2dlt9Ydf0mGAE6yBGxYqOjKq65oJu1Maw4NxOEFZtl6RzG8Nw6EA5IItrvUWfFcl2uZp3/AogXKt191ciu1u1pT2HiAzgGhLFu0369+8fbBo3iCUC00QqOk93z+IuZRNL3D629IaVQRSy4hvk6J9ERqZE+FRd2mkmJloiVzOWkPIABQAGBixoVgpKCHAoDEjw7ueEp0AOG1u6UeCO8tFXVA+CxpawRCOACA26PN49bSCAi1ACSa4UZCAyOhhQCeQeTrhffzDQUb+D0UtPp92BmDuiNQ64V7SgpeBDjmyMHIe+pMJCDMEA16Xn/Ej5biqJ5788RTIZNBaN7JAAJ/WggdioCIhIyKhomNi4dPTLKLgHjNVBiASYf2WTELngCxm1noWLyRuDAeioSBhozJgYJ1owQE2gGiAOAJHy1BgRp5VWhdBCL1QtlzLkHxQKMoMGLyzqRD58AIlXOhCR5KEaDPRgLDi30hDJ0JnRv/AAGJSY1APjtCUJNjZtcb3NSy0xLopKGLAN96p4V1TWdrMjgJ5LqXjh+Hf7eZjh0Aq36iVz2Z8jPJ9x6cN0HUjdN1eiEGBImcB60IemYJKtRw6tT9V5rcs8262CWq1Gl5A5zrmpJ268KpA4lYJBRkzK/jN4E3y4wxBjcHdqCU8ANoBYKIKdlT3tZRshjxBbXL/gK48LeB/zNWwQRds/m3WTB3qanD5cO7lMzC399sP3do37ED57svyEtS1FZuvXKEUQAAVwv+p2+gcr+Z0Rs8DwbOAC82U9A3uDsUDdPzKD38qi1zU5tOQgCu+kcQ1jSz10Ot73G8Iot1jy1Whj8RZqt0ftOlgoGD7fWxIYdkLes+LLcHeewDNATWPyuHtLGJkEhDA8C3bYCp89zU5klSzK3oY4/T8ziOWc4bP7s3kSYFa9zv3II0yZic7n5dky9BX5oMFIYHHcEaIEEP+UptclZVuopP2PffRzE+t4gjURaOL8Bqn61Oz0ZPtaZGVEqyY3dA5azL/3Iil3a588RUpdTVYSVf4qQIjx4BfAIuVpaVXvJlxK/evKmZbTJf6Z4eyL/tGR/qM0xAJCLHcTDaAOsBIBawGfgAF8Td3t1ej3YXxb8DDBYYgGOEBMBfKNWS5XHNkC2xkqVej7YocpGPlN7eRbTmDa7roAW7Q+QT0Dqwe9DBlAi1R5xXMBvdxtuyXYI70gXTfQbf04Kwyad70zTiKDcwAICUleDVK4Bb5L/twtGJY3517wHkahBPbBRNnm1apZsVWhfNypCyUh65FYz40Jo/lR+bp8F9JVn/3n706Bk8Lf4ebcHnjzHexzi5Rbp1DD9ZKN2QP0mNVvnTTUQPkTAwcqAzwmhgjBdx50ZqWitdNxSWDk3FTFG53XaaNduTO8+n8eJ4SXL+zNRi72ypzovkeXQb3IjHZGXMr7tE4kArTtnX+lVl5kX3xUQo8cNgY3AmHfik4fNf+a2pO0TvXLKqL7cd8jXR/cECO5At4B2XVIi8A+m3xawRYPtAEFdKzkBg61ODL5TESTQj50YrVrqzVtabr7epy0W3gvexnf89qVtMLij+weUY2wASnzgwj0pq/5npPUaC5hZ0Jb1bWQ2+K7e6e/g8ztfZbl/iB6d8oR7vax2a7u2LlMKj1RNsrTK1FSB0oulpFD3V3INTU/f61Z5/Pphhd6hWKvNIYHw+KAsa3eTU5dqZxPfuJJkm37SLxA4GTOJE7g3vjasAYA/7aQMk+FctX9ceLOkkTSYr3bu2eKEE5/nEUqLqCf9Tn8HCepD4UimaYc2jVT6XqM2kOO56fInjoBoOkOkszuVWhMSC/TifpZjNb3MOuh9I2NLch2X0HDglxi3YgH+dhXNEJglxNHcNKqwZzGLW8IUScXd5hR56PSRyLsPm+FDhDtD+tD8zsBLhRI/XalMOiSkVgcleKVWXslSvi9QS2yoJV6kcrWBxrNjCHzQ4kVda4ZzY5NkGL7hILGAO/uMINcYEO242FQQwUlWGouEiMcC9fv40O6/uloMlhRHXliutBauG1WxiW4YOCVUZttw50ZGg3+9Dx2aw+zb4XSJFoyhB6XC+sSaiC7JFS4YSj+uHydbEV9Hl1FSlaI6wUGyBNQS6epXSaETn11F02VEy4Q9rCjZAx7i48+BKpuTW6K61i0S8I1uudLjVeWMJzrMyDN0h0RIHH6APWzL4TQEv4+AMnMvFiTbNpMbbXGv+PDR9esqMZ91je9Nka5ExQRm2HJ8Ty3Jz7s3uLd3esL0mOLe+6XDf5YZEYBBLvEuKaXVmDDCGmoKB7dsAEn+i/m2ZFc2tUITREZhR8ur0fAvevh29RaJCdRImZEVWCEll0zduiWh6qW+uXOWv7FudDoCPt+MFmcD0s2TjxvmNhfJGS9NFN+MQH/gtcq5PLNv009193eL8mkTBGE/23vbWvLmdisG/xYCv7jh8XdgSjCoEoPhDumVylN4ZWOh/2g+3zvNul8jQXaQspQeHGrpPiDMXcpFEQxZpROk2GuhmHWOhtgQjB4lFuTXcvixOh8iJcJJi4E3G6XIVDO6SuRdLdcbZWdFaOuF8hR4rmdEbBAOMlJ56V71RnbrZ9UVURnQleFLr7ekTHwC7ZWrK4J1Kuq905ubUvloomqVuzVt0vQxwQViSRkSjq7S7fpO6+4QTBAFeVFCZHr3kGq2ibeRFNEkpXelwT3ow1f9TZEk0li2XZ4+YxBfumE5tnmtVJHAiQ7Vm2I/L1BvVr1KXa1T3PHok/wci2f7b9gVg4LHlYNu+bDkB6uQBw7VHtoWfMkgJzImxR9OSF+k1Gmbh2GgkmmPLNLUV1R+KMU8jGqjFpg0fdzL6Ev9lLDw9fgplDGgawD6ibAJknQqpV9YObRatwQzMZbLM5sypx0w/AJNx55AAoI01UNP1wAKJqV5RJlhIzJb4btvlqALTNKNXNii5CVB32SPXeSnkoqJ+MIwwdrGg4MXoSAb1WRnmBZm7YdoEacBOz0SRdBPddS9xGxEaYx0creOCx/8Zkzhj5A1jg1jzX0qWX5hCVn5KoDT3lhp1uW6NoOnRbD7+yxAUZPljo/N+TmLLh+jEZC+vhqeynU4rekI7SLe+rUPxYShK9KWQP8fSN3qH5jZytOWSuU0ORRzXwZ5L7hc7/kykNY+eR31rorLcvpCKvOAZBWOHC/3P7Ama/Xb6lL4VX1zQ9NNz/02N91o97AIQBFL3PW+Lo5wXkxZmQPNNk7lLOGTKfTTgcHn5JsZmFdt+sTDrU/uaDyt9zMavnHxoLNTk3Mk5ArAaukqKQhBcKlxCg1lz+idpIDK05ZFfYBIzDk9wsJf+tQH9iDrlhtare8v9tCBL+1VhvOmX41wBjGA1hfPb0OvRGdFYDsFIwHIyigLv4XUmV+J+hvW8y9PNe+f19U4UrtaELFy0+Aw+NnRgrb9mVxPOwltv5O79WYFVOSs9vJFCsYm+k7TzpLH/v6qQ62kjcVfbeSkiTZrq4Bgb7lJJsw6W2VFGeTuKT0Yc9EtPYno2zZ5JP1/8V8660Q2pYIWrve/q5ltdZ5j79ClROrfJ6KBh1YW0VRJYtA1eT1qCT4RiTR3j7JNjeY48W++UV0FjXvmatwgSMrEnffZajiu/ddzL3vtK9T84Y3ZmesJ3xmzFVu9VLh0X5zBAH3pfz3edUTuYkuHiCXmaD3mXK+fY6CkYs2Zp2L/5eTN5yTxlHn8GLQX3HeC9Sjdb+tVBW7h6N2Ozw1DAvAoNr7AhXVhQPC5wXvYeJ65GCEjkcqtHvJaYD6WPQB0il1dUl9qbzEUk8Rs8fZKYPh2xj1Y/kdqWyrR2ZZhzyqLEIP+cTMZAz58DDqb1w7OV3uoU4gnL7Yx/mLkgn3pSmPqujHHjG2ppnDWWW+T2RDegqDwfqxk4iVz4GH5hwdM5b92DjsCGyPXvA74nQbvr8C1BGL+rAGxk+f91vfcrmhqNEVPG9SRThuLdYxAM6cZQP255MotRCEcdfJMmWhQE7BpKnVqWpNaV+GaJRbp46fNgjNPwStDYSLfG9Q8ErXfuU/A+Z3lHL3BZA1uJjopA4Jb5h7qyNiY1kf+u7GRzaqwrLjUH4boE1t/+i7apNHEce8zADWOLRsm5N9HOTU3BGTxMqtxnnfjWj3qSBE5/EDiz2YXzT0DVGsDpYFtCcxuW6QTJJTzWEdPcz0oEXB9LTCpk4VLhmSH+kZMibKTg7OrPyaFWgCf7kfGd+832n+/qLEpTZGTc1srDzBtquYFTJCN0rEA2TVQDUdjyFFkNrVNevuym9xPwFhNcL3m18OjGJ0c86oLiqqmUK2TeZCVAcFTfQD84gYXkwVzaDw82Pswwp3gnBH6fwQtH4d7HYiWWXGdaIJersHFk2avE2Z5QKMvcDGaduvdVIidtHFvCZtrTFugmB/SouqFwZhZzEv/f9YAqEWF2LBsTSp66nj7r0tB00Yeg9IVgX49asEl0rcw8Xe/3aCOFiVYhqOVLX5u9fxx4MRBdnBsn6dt+fqSZZrgOd7iVlCs9D0IwAYUL/K94mwcm/ULq3FmEbkwSjNRDFtJMv/1qPJyC1mjyVNBaGxjD5g3chkbwqRiJ/KcQJp/x7uaKZfAoPGIlUX2oh7AFcRBilu9YR+0ZAD1gfbF6Jfw+gkuZk1cUvPz7/WyPgx85oa3s6PShwR3z408ScqCsE5JlrpTOPsfvxoWpPsPhMqJ/uqmLuSJlRWDiy3nwzdgscivsD0ISA4umgT0wScCqrGLp8117bTwlSlDoQ4PT2Nrk5jWmo7I48gIJPgkhMoxv5P21XrRQyLj7siqzSXVdp1hXO7tut9SawUpFTkHCZ/JqcBPnWXM87juuptazplfvvjg2fnLsQKBle2zDCPHHCqefPME9kxsXZiHrV1dBSofWW3b6MNMxx0Rw8yJDpIY3u+vA8han83rmMaablbSJACqFUFo+0TTqzV9sMxWb1y71s/2kjHzVGKO8nha0AkeEJyqw2ojyuUtCFh6PErnE2SyOXs0ncgJ91QhaD5ilmkczcfC7lBReoJIKrVvN2zA9b2wRB08UzrBxKJutI6aQ2JqpoIVLnR4euWHofDQNiUO4sAnozezLmrCA55L1u3Rk8d/zgjt8ViYZMoT6YEvpBRHa8DhfnY6HFchb/z2mDptL2gR86pu97d+vXprw+fWejLfZoYbCexizWTFyJOCEnc/TXNivIQ9z5dgyjX2DRMmu0ht3jeHsbMrJsKqD1dz1WjfEWanxv75Lf37D/8CYks8Wxi5egYUeRkrZIfxVEeT33r5HP97cWTPcimvn9J48fytVVzwsNMneLB8JrlNQLEI/5cmoCn4aHDOsWyZfv3ticODmOXzITM6gotpPbKX/GCsi18JqTYRv2tlS936i5Gh8pTjePey/7RiCK15HU/h6uYbhoqnWdz9YOeA2xuw/9FdYgcC+o5MnGQOUkAKo7R9BAGIU1E2adHDl+/s/lPys1SuEV/IuPUpHF++S404m0rG7Yfouk8NcMD30ln4js1xh9NOjGPg/DZGTLxqGqhdNaAqMhJbDqCuV6aVkZo3GOpiOAskIC36BpzErQd9Sec8ViTMqElAvluS0tmK5f/ru8AfQqoy3JbrZvdbmoHilIcZyZsilU5QWUQ1ut8oXaBSUwvCZ26LnXhi0hCsEZZdFefX6EpKNN9qd+Xm0FfV6gxTTvK3nepbcvZr1IzylCmeKmpE+nC9WJvXENAe9giM0Tv7OmO0BoRHEGFVnVQIAAAAgAECA4JDELaJHfQkIeQIAMNQ9JgAAuLf2+TD7h+GereEAIIBLiH+JsjYWw/F+8AkW9C5bSplnNgC9ec9c7sNiGALQL9H8Wg/AHwFV0Kbjmpfg+UJeAY8ERHnxKlSwDxi58DEEglokKhoIL5tfAqGznH8y7nmAUvOPCqjt4xNLAKj1FlcBinfACEoW6PA7YXIhoJUIAKh3IYiAcByPgNHsjEB4mhiBo1YagRdDDVS5bQDMupQpUa9GpSaN2vhIU6FKu3olWmSq0KJVDautnD8tP1Icycpu6uZULXCZxkAEULirxoCJQ3nUnoCcrW3qJumskkWWYpX+mTDSYqduBmpQVN0S3BHwoMUB/J4IslHK9JIri3J0c4lOWgX6BOp5TXlqjaCCGivQobQpp4WA1H7/ggc=); }</style></defs><g transform="translate(75.435253423002 244.9507781971929) rotate(0 141.28793334960938 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did:web:iam.ruuuuu.de</text></g><g transform="translate(151.60254680965227 391.3541500568999) rotate(0 82.92196655273438 17.5)"><text x="0" y="24.668" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">blacksky.app</text></g><g transform="translate(149.5014701332002 74.30692504753279) rotate(0 75.40261893761453 17.464019532254042)"><text x="0" y="24.617281932665158" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="27.94243125160631px" fill="#7950f2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">@ruuuuu.de</text></g><g stroke-linecap="round"><g transform="translate(223.5269012754834 119.47626536936332) rotate(0 -0.9383722221446078 59.36543039326443)"><path d="M0.63 0.09 C-0.29 19.77, -2.71 96.39, -3.5 116.55 M-2.46 -2.34 C-2.66 17.77, 1.35 99.14, 2.03 118.99" stroke="#6741d9" stroke-width="2" fill="none"></path></g><g transform="translate(223.5269012754834 119.47626536936332) rotate(0 -0.9383722221446078 59.36543039326443)"><path d="M8.41 22.71 C4.03 13.42, 4.37 7.94, 0.33 1.32 M8.06 24.86 C6.63 19.65, 4.91 14.28, 0.48 0.33" stroke="#6741d9" stroke-width="2" fill="none"></path></g><g transform="translate(223.5269012754834 119.47626536936332) rotate(0 -0.9383722221446078 59.36543039326443)"><path d="M-8.69 22.1 C-6.67 13, 0.07 7.75, 0.33 1.32 M-9.03 24.25 C-6.88 19.18, -5.02 13.94, 0.48 0.33" stroke="#6741d9" stroke-width="2" fill="none"></path></g><g transform="translate(223.5269012754834 119.47626536936332) rotate(0 -0.9383722221446078 59.36543039326443)"><path d="M-7.44 94.72 C-5.62 102.95, 1.18 115.02, 1.72 120.23 M-7.78 96.88 C-5.89 101.49, -4 105.95, 1.88 119.24" stroke="#6741d9" stroke-width="2" fill="none"></path></g><g transform="translate(223.5269012754834 119.47626536936332) rotate(0 -0.9383722221446078 59.36543039326443)"><path d="M9.65 94.01 C5.07 102.54, 5.46 114.88, 1.72 120.23 M9.3 96.16 C7.62 100.92, 5.93 105.52, 1.88 119.24" stroke="#6741d9" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(21.415300963217305 20.781412310127962) rotate(0 90.5606091720565 12.538858845899313)"><text x="0" y="17.67477542917971" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20.062174153438946px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">handle (swappable)</text></g><g transform="translate(255.25279157200202 459.8527230548657) rotate(0 90.8301866196598 11.852831541802743)"><text x="0" y="16.70775134132511" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="18.964530466884348px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">hosting (swappable)</text></g><g stroke-linecap="round" transform="translate(10 10) rotate(0 216.96939798218773 77.26585604552133)"><path d="M2.04 0.04 C115.75 -1.87, 228.03 -2.52, 433.13 -0.73 M432.49 3.78 C433.72 63.25, 431.68 126.15, 434.96 156.71 M434.47 153.73 C288.21 156.74, 146.69 156.07, -1.32 154.08 M1.57 158.14 C-0.96 95.21, -4.33 38.92, -0.87 1.88" stroke="#1e1e1e" stroke-width="2.5" fill="none" stroke-dasharray="8 10"></path></g><g stroke-linecap="round" transform="translate(10.930999939770118 327.0258694295853) rotate(0 218.34303324080247 83.10126449239306)"><path d="M1.31 -0.99 C162.07 -2.36, 321.56 -2.05, 437.21 -0.17 M433 -1.97 C435.87 50.23, 435.82 99.44, 438.42 163.33 M437.04 167.15 C276.07 165.13, 115.79 165.11, -1.31 164.35 M-0.88 162.58 C-2.85 106.16, -4.93 54.73, -1.98 3.92" stroke="#1e1e1e" stroke-width="2.5" fill="none" stroke-dasharray="8 10"></path></g><g stroke-linecap="round"><g transform="translate(225.49225868805297 289.24225653537223) rotate(0 1.1658573777858692 48.139846563784886)"><path d="M-2.48 -3.96 C3.05 20.12, 0.61 43.53, 4.21 96.04 M1.68 -0.78 C0.77 25.88, -1 51.27, 3.11 96.31" stroke="#6741d9" stroke-width="2" fill="none"></path></g><g transform="translate(225.49225868805297 289.24225653537223) rotate(0 1.1658573777858692 48.139846563784886)"><path d="M-8.17 71.43 C-3.66 78.1, -2.99 84.43, 4.04 96.19 M-6.09 73.02 C-3.91 79.7, -2.43 85.69, 3.49 96.32" stroke="#6741d9" stroke-width="2" fill="none"></path></g><g transform="translate(225.49225868805297 289.24225653537223) rotate(0 1.1658573777858692 48.139846563784886)"><path d="M8.9 70.33 C9.78 77.23, 6.81 83.79, 4.04 96.19 M10.98 71.93 C8.61 78.74, 5.55 85.02, 3.49 96.32" stroke="#6741d9" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(185.29272658366608 192.60789339925532) rotate(0 10.854095458984375 9.337157556834654)"><text x="0" y="13.161657292114118" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="14.939452090935434px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">did</text></g><g transform="translate(247.8504105561551 355.76363423974954) rotate(0 56.73396301269531 9.337157556834654)"><text x="0" y="13.161657292114118" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="14.939452090935434px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">serviceEndpoint</text></g><g stroke-opacity="0.8" fill-opacity="0.8" transform="translate(239.19740095874295 139.5862672733324) rotate(0 45.536555783690346 9.345888896835959)"><text x="0" y="13.17396498897997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="14.953422234937536px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">alsoKnownAs</text></g></svg>
+428
public/where-its-at/index.md
··· 1 + --- 2 + title: Where It's at:// 3 + date: '2025-10-02' 4 + spoiler: From handles to hosting. 5 + --- 6 + 7 + You might have heard about the AT protocol (if not, [read this!](/open-social/)) 8 + 9 + Together, all servers speaking the AT protocol comprise *the atmosphere*--a web of hyperlinked JSON. Each piece of JSON on the atmosphere has its own `at://` URI: 10 + 11 + - <span style={{wordBreak: 'break-word'}}>[`at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z)</span> 12 + - <span style={{wordBreak: 'break-word'}}>[`at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22)</span> 13 + - <span style={{wordBreak: 'break-word'}}>[`at://tessa.germnetwork.com/pub.leaflet.publication/3lzz6juivnc2d`](https://pdsls.dev/at://tessa.germnetwork.com/pub.leaflet.publication/3lzz6juivnc2d)</span> 14 + 15 + But where do they point, exactly? 16 + 17 + Given an `at://` URI, how do you locate the corresponding JSON? 18 + 19 + In this post, I'll show you the exact process of resolving an `at://` URI step by step. Turns out, this is also a great way to learn the details of how `at://` works. 20 + 21 + Let's start with the structure of a URI itself. 22 + 23 + --- 24 + 25 + ### The User as the Authority 26 + 27 + As you might know, a URI often contains a scheme (for example, `https://`), an *authority* (like `wikipedia.com`), a path (like `/Main_Page`), and maybe a query. 28 + 29 + In most protocols, including `https://`, the authority part points at whoever's *hosting* the data. Whoever *created* this data is either not present, or is in the path: 30 + 31 + ![A URI to a Bluesky post; "bsky.app" domain is highlighted as "the app" while "ruuuuu.de" username is highlighted in the path](./1-full.svg) 32 + 33 + **The `at://` protocol flips that around.** 34 + 35 + In `at://` URIs, whoever *created* the data is the authority, in the most literal sense: 36 + 37 + ![An at:// URI, with the "ruuuuu.de" username where you'd usually see a domain, followed by "app.bsky.feed.post" in the path](./2-full.svg) 38 + 39 + **The user is the authority for their own data.** Whoever's *hosting* the data could change over time, and is *not* directly included in an `at://` URI. To find out the actual physical server hosting that JSON, you're gonna need to take a few steps. 40 + 41 + --- 42 + 43 + ### A Post in the Atmosphere 44 + 45 + Let's try to resolve this `at://` URI to the piece of JSON it represents: 46 + 47 + ![at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z](./2-full.svg) 48 + 49 + An easy way to resolve an `at://` URI is to use an [SDK](https://sdk.blue/) or a client app. Let's try an online client, for example, [pdsls](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) or [Taproot](https://atproto.at/viewer?uri=at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) or [atproto-browser](https://atproto-browser.vercel.app/at/ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z). They'll figure out the physical server where its JSON is currently hosted, and show that JSON for you. 50 + 51 + **The above `at://` URI points at this JSON, wherever it is currently being hosted:** 52 + 53 + ```js 54 + { 55 + "uri": "at://did:web:iam.ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z", 56 + "cid": "bafyreiae4ehmkk4rtajs5ncagjhrsv6rj3v6fggphlbpyfco4dzddp42nu", 57 + "value": { 58 + "text": "posting from did:web, like a boss", 59 + "$type": "app.bsky.feed.post", 60 + "langs": ["en"], 61 + "createdAt": "2025-09-29T12:53:23.048Z" 62 + } 63 + } 64 + ``` 65 + 66 + You can guess by the `$type` field being `"app.bsky.feed.post"` that this is some kind of a post (which might explain why it has fields like `text` and `langs`). 67 + 68 + However, note that this piece of JSON represents a certain social media post *itself*, not a web page or a piece of some app. **It's pure data as a piece of JSON**, not a piece of UI. You may think of the `$type` stating the data *format*; the `app.bsky.*` prefix tells us that the `bsky.app` application might know something about what to do with it. Other applications [may also](https://bsky.app/profile/o.simardcasanova.net/post/3luujudlr5c2j) consume and produce data in this format. 69 + 70 + A careful reader might notice that the `uri` in the JSON block is *also* an `at://` URI but it's slightly different from the original `at://` URI we requested: 71 + 72 + ```js 73 + // What's at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z ? 74 + { 75 + "uri": "at://did:web:iam.ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z", 76 + // ... 77 + } 78 + ``` 79 + 80 + In particular, the short `ruuuuu.de` authority has expanded into a longer `did:web:iam.ruuuuu.de` authority. Maybe that's the physical host? 81 + 82 + **Actually, no, that's not the physical host either**--it's something called an *identity*. Turns out, resolving an `at://` URI is done in three distinct steps: 83 + 84 + 1. Resolve the handle to an identity *("who are you?”)* 85 + 2. Resolve that identity to a hosting *("who holds your data?”)* 86 + 3. Request the JSON from that hosting *("what is the data?”)* 87 + 88 + Let's go through each of these steps and see how they work. 89 + 90 + --- 91 + 92 + ### From Handles to Identities 93 + 94 + The `at://` URIs you've seen earlier are fragile because they use handles. 95 + 96 + Here, `ruuuuu.de`, `danabra.mov`, and `tessa.germnetwork.com` are handles: 97 + 98 + - <span style={{wordBreak: 'break-word'}}>[`at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) </span> 99 + - <span style={{wordBreak: 'break-word'}}>[`at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22)</span> 100 + - <span style={{wordBreak: 'break-word'}}>[`at://tessa.germnetwork.com/pub.leaflet.publication/3lzz6juivnc2d`](https://pdsls.dev/at://tessa.germnetwork.com/pub.leaflet.publication/3lzz6juivnc2d)</span> 101 + 102 + *(Read more about [domains as "internet handles" here.](/open-social/#open-social))* 103 + 104 + The user may choose to change their `at://` handle later, and it is important for that not to break any links between pieces of JSON already on the network. 105 + 106 + This is why, before you *store* an `at://` URI, you should turn it into a canonical form by resolving the handle to something that never changes--an *identity*. An identity is like an account ID, but global and meant for the entire web. There are two mechanisms to resolve a handle to an identity (also known as a “[DID](https://en.wikipedia.org/wiki/Decentralized_identifier)”): 107 + 108 + 1. Query the DNS TXT record at `_atproto.<handle>` looking for `did=???` 109 + 2. Make an HTTPS GET to `https://<handle>/.well-known/atproto-did` 110 + 111 + The thing you're looking for, the DID, is going to have a shape like `did:something:whatever`. (We'll revisit what that means later.) 112 + 113 + --- 114 + 115 + For example, let's try to resolve `ruuuuu.de` via the DNS mechanism: 116 + 117 + ```sh {6} 118 + $ nslookup -type=TXT _atproto.ruuuuu.de 119 + Server: 192.168.1.254 120 + Address: 192.168.1.254#53 121 + 122 + Non-authoritative answer: 123 + _atproto.ruuuuu.de text = "did=did:web:iam.ruuuuu.de" 124 + ``` 125 + 126 + Found it! 127 + 128 + The `ruuuuu.de` handle *claims* to be owned by `did:web:iam.ruuuuu.de`, whoever that may be. That's all that we wanted to know at this point: 129 + 130 + ![ruuuuu.de resolves to did:web:iam.ruuuuu.de](./3.svg) 131 + 132 + **Note this doesn't *prove* their association yet.** We'll need to verify that whoever controls the `did:web:iam.ruuuuu.de` identity "agrees" with `ruuuuu.de` being their handle. The mapping is bidirectional. But we'll confirm that in a later step. 133 + 134 + --- 135 + 136 + Now let's try to resolve `danabra.mov` using the DNS route: 137 + 138 + ```sh {6} 139 + $ nslookup -type=TXT _atproto.danabra.mov 140 + Server: 192.168.1.254 141 + Address: 192.168.1.254#53 142 + 143 + Non-authoritative answer: 144 + _atproto.danabra.mov text = "did=did:plc:fpruhuo22xkm5o7ttr2ktxdo" 145 + ``` 146 + 147 + That also worked! The `danabra.mov` handle claims to be owned by the `did:plc:fpruhuo22xkm5o7ttr2ktxdo` identity, whoever that may be: 148 + 149 + ![danabra.mov resolves to did:plc:fpruhuo22xkm5o7ttr2ktxdo](./4.svg) 150 + 151 + This DID looks a bit different than what you saw earlier but it's also a valid DID. Again, it's important to emphasize we've not confirmed the association yet. 152 + 153 + --- 154 + 155 + Subdomains like `barackobama.bsky.social` can also be handles. 156 + 157 + Let's try to resolve it: 158 + 159 + ```sh {6} 160 + $ nslookup -type=TXT _atproto.barackobama.bsky.social 161 + Server: 192.168.1.254 162 + Address: 192.168.1.254#53 163 + 164 + Non-authoritative answer: 165 + *** Can't find _atproto.barackobama.bsky.social: No answer 166 + ``` 167 + 168 + The DNS mechanism didn't work, so let's try with HTTPS: 169 + 170 + ```sh {2} 171 + $ curl https://barackobama.bsky.social/.well-known/atproto-did 172 + did:plc:5c6cw3veuqruljoy5ahzerfx 173 + ``` 174 + 175 + That worked! This means that `barackobama.bsky.social` handle claims to be owned by the `did:plc:5c6cw3veuqruljoy5ahzerfx` identity, whoever that is: 176 + 177 + ![barackobama.bsky.social resolves to did:plc:5c6cw3veuqruljoy5ahzerfx](./7.svg) 178 + 179 + So you get the idea. When you see a handle, you can probe it with DNS and HTTPS to see if it claims to be owned by some identity (a DID). If you found a DID, you'll then be able to (1) verify it actually owns that handle, and (2) locate the server that hosts the data for that DID. And that will be the server you'll ask for the JSON. 180 + 181 + [In practice](https://docs.bsky.app/docs/advanced-guides/resolving-identities), if you're building with AT, you'll likely want to either deploy your own handle/did resolution cache or hit an existing one. (Here's [one implementation.](https://ngerakines.leaflet.pub/3lyea5xnhhc2w)) 182 + 183 + --- 184 + 185 + ### AT Permalinks 186 + 187 + Now you know how handles resolve to identities, also known as DIDs. Unlike handles, which change over time, DIDs never change--they're immutable. 188 + 189 + These `at://` links, which use handles, are human-readable but fragile: 190 + 191 + - <span style={{wordBreak: 'break-word'}}>[`at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) </span> 192 + - <span style={{wordBreak: 'break-word'}}>[`at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22)</span> 193 + - <span style={{wordBreak: 'break-word'}}>[`at://tessa.germnetwork.com/pub.leaflet.publication/3lzz6juivnc2d`](https://pdsls.dev/at://tessa.germnetwork.com/pub.leaflet.publication/3lzz6juivnc2d)</span> 194 + 195 + They will break if one of us changes a handle again. 196 + 197 + In contrast, the `at://` links below, which use DIDs, will not break until we either delete our accounts, delete these records, or permanently take down our hosting: 198 + 199 + - <span style={{wordBreak: 'break-word'}}>[`at://did:web:iam.ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://did:web:iam.ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) </span> 200 + - <span style={{wordBreak: 'break-word'}}>[`at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/sh.tangled.feed.star/3m23ddgjpgn22)</span> 201 + - <span style={{wordBreak: 'break-word'}}>[`at://did:plc:ad4m72ykh2evfdqen3qowxmg/pub.leaflet.publication/3lzz6juivnc2d`](https://pdsls.dev/at://did:plc:ad4m72ykh2evfdqen3qowxmg/pub.leaflet.publication/3lzz6juivnc2d)</span> 202 + 203 + So, really, this is the "true form" of an `at://` URI: 204 + 205 + ![at://did:web:iam.ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z](./8-full.svg) 206 + 207 + **Think of `at://` links with DIDs as "permalinks".** Any application *storing* `at://` URIs should store them in this canonical form so that logical links between our pieces of JSON don't break when we change our handles or change our hosting. 208 + 209 + Now that you know how to resolve a handle to a DID, you want to do two things: 210 + 211 + 1. Verify that whoever owns this DID actually goes by that handle. 212 + 2. Find the server that hosts all the data for this DID. 213 + 214 + You can do both of these things by fetching a piece of JSON called the *DID Document*. You can think of it as sort of a "passport" for a given DID. 215 + 216 + How you do that depends on what kind of DID it is. 217 + 218 + --- 219 + 220 + ### From Identities to Hosting 221 + 222 + Currently, there are two kinds of DIDs, known as *DID methods*, supported by the AT protocol: `did:web` (a [W3C draft](https://w3c-ccg.github.io/did-method-web/)) and `did:plc` ([specified](https://github.com/did-method-plc/did-method-plc) by Bluesky). 223 + 224 + Let's compare them. 225 + 226 + #### `did:web` 227 + 228 + The `ruuuuu.de` handle claims to be owned by `did:web:iam.ruuuuu.de`: 229 + 230 + ![ruuuuu.de points at did:web:iam.ruuuuu.de](./3.svg) 231 + 232 + To check this claim, let's find the DID Document for `did:web:iam.ruuuuu.de`. The [`did:web` method](https://w3c-ccg.github.io/did-method-web/) is a specification that specifies an [algorithm](https://w3c-ccg.github.io/did-method-web/#read-resolve) for that. 233 + 234 + In short, you cut off the `did:web:` from the DID, append `/.well-known/did.json` to the end, and run an HTTPS GET request: 235 + 236 + ```sh 237 + $ curl https://iam.ruuuuu.de/.well-known/did.json | jq 238 + { 239 + "@context": [ 240 + "https://www.w3.org/ns/did/v1", 241 + "https://w3id.org/security/multikey/v1", 242 + "https://w3id.org/security/suites/secp256k1-2019/v1" 243 + ], 244 + "id": "did:web:iam.ruuuuu.de", 245 + "alsoKnownAs": [ 246 + "at://ruuuuu.de" 247 + ], 248 + "verificationMethod": [ 249 + { 250 + "id": "did:web:iam.ruuuuu.de#atproto", 251 + "type": "Multikey", 252 + "controller": "did:web:iam.ruuuuu.de", 253 + "publicKeyMultibase": "zQ3shWHtz9QMJevcGBcffZBBqBfPo55jJQaVDuEG7ZwerALGk" 254 + } 255 + ], 256 + "service": [ 257 + { 258 + "id": "#atproto_pds", 259 + "type": "AtprotoPersonalDataServer", 260 + "serviceEndpoint": "https://blacksky.app" 261 + } 262 + ] 263 + } 264 + ``` 265 + 266 + This DID Document looks sleep-inducing but it tells us three important things: 267 + 268 + - **How to refer to them.** The `alsoKnownAs` field confirms that whoever controls `did:web:iam.ruuuuu.de` indeed wants to use `@ruuuuu.de` as a handle. ✅ 269 + - **How to verify the integrity of their data.** The `publicKeyMultibase` field tells us the public key with which all changes to their data are signed. 270 + - **Where their data is stored.** The `serviceEndpoint` field tells us the actual server with their data. Rudy's data is currently hosted at `https://blacksky.app`. 271 + 272 + A DID Document really *is* like an internet passport for an identity: here's their handle, here's their signature, and here's their location. It connects a handle to a hosting while letting the identity owner change *either* the handle *or* the hosting. 273 + 274 + ![did:web:iam.ruuuuu.de is bidirectionally connected to the swappable @ruuuuu.de handle, and points to swappable blacksky.app hosting](./9.svg) 275 + 276 + Users who interact with `@ruuuuu.de` on different apps in the atmosphere don't need to know or care about his DID *or* about his current hosting (and whether it moves). From their perspective, his current handle is the only relevant identifier. As for developers, they'll refer to him by DID, which conveniently never changes. 277 + 278 + All of this sounds great, but there is one big downside to the `did:web` identity. If `did:web:iam.ruuuuu.de` ever loses control of the `iam.ruuuuu.de` domain, he will lose control over his DID Document, and thus over his entire identity. 279 + 280 + Let's have a look at an alternative to `did:web` that avoids this problem. 281 + 282 + #### `did:plc` 283 + 284 + We already know the `danabra.mov` handle claims to be owned by the `did:plc:fpruhuo22xkm5o7ttr2ktxdo` identity (actually, that's me!) 285 + 286 + ![](./4.svg) 287 + 288 + To check this claim, let's find the DID Document for `did:plc:fpruhuo22xkm5o7ttr2ktxdo`. 289 + 290 + The [`did:plc` method](https://github.com/did-method-plc/did-method-plc) is a specification that specifies an [algorithm](https://github.com/did-method-plc/did-method-plc?tab=readme-ov-file#did-resolution) for that. 291 + 292 + Essentially, you need to hit the [`https://plc.directory`](https://plc.directory) service with a `GET`: 293 + 294 + ```sh 295 + $ curl https://plc.directory/did:plc:fpruhuo22xkm5o7ttr2ktxdo | jq 296 + 297 + { 298 + "@context": [ 299 + "https://www.w3.org/ns/did/v1", 300 + "https://w3id.org/security/multikey/v1", 301 + "https://w3id.org/security/suites/secp256k1-2019/v1" 302 + ], 303 + "id": "did:plc:fpruhuo22xkm5o7ttr2ktxdo", 304 + "alsoKnownAs": ["at://danabra.mov"], 305 + "verificationMethod": [ 306 + { 307 + "id": "did:plc:fpruhuo22xkm5o7ttr2ktxdo#atproto", 308 + "type": "Multikey", 309 + "controller": "did:plc:fpruhuo22xkm5o7ttr2ktxdo", 310 + "publicKeyMultibase": "zQ3shopLMtAvvVrSsmWPE2pstFWY4xhGFBjkdRuETieUBozgo" 311 + } 312 + ], 313 + "service": [ 314 + { 315 + "id": "#atproto_pds", 316 + "type": "AtprotoPersonalDataServer", 317 + "serviceEndpoint": "https://morel.us-east.host.bsky.network" 318 + } 319 + ] 320 + } 321 + ``` 322 + 323 + The DID Document itself works exactly the same way. It specifies: 324 + 325 + - **How to refer to me.** The `alsoKnownAs` field confirms that whoever controls `did:plc:fpruhuo22xkm5o7ttr2ktxdo` uses `@danabra.mov` as a handle. ✅ 326 + - **How to verify the integrity of my data.** The `publicKeyMultibase` field tells us the public key with which all changes to my data are signed. 327 + - **Where my data is stored.** The `serviceEndpoint` field tells us the actual server with my data. It's currently at `https://morel.us-east.host.bsky.network`. 328 + 329 + Let's visualize this: 330 + 331 + ![did:plc:fpruhuo22xkm5o7ttr2ktxdo is bidirectionally connected with the swappable @danabra.mov handle, and points at swappable morel.us-east.host.bsky.network hosting](./10.svg) 332 + 333 + Although my handle is `@danabra.mov`, the actual server storing my data is currently `https://morel.us-east.host.bsky.network`. I'm happy to keep hosting it there but I'm thinking of moving it to a host I control in the future. I can change both my handle and my hosting without disruption to my social apps. 334 + 335 + Unlike Rudy, who has a `did:web` identity, I stuck with `did:plc` (which is the default one when you create an account on Bluesky) so that I'm not irrecovably tying myself to any web domain. "PLC" officially stands for a "Public Ledger of Credentials"--essentially, it is like an npm registry but for DID Documents. (Fun fact: originally PLC meant "placeholder" but they've decided [it's a good tradeoff.](https://www.youtube.com/watch?v=m9AVUAUDC2A)) 336 + 337 + The upside of a `did:plc` identity is that I can't lose my identity if I forget to renew a domain, or if something bad happens at the top level to my TLD. 338 + 339 + The downside of a `did:plc` identity is that whoever operates the PLC registry has some degree of control over my identity. They can't outright *change* it because every version is recursively signed with the hash of the previous version, every past version is queryable, and the hash of the initial version *is* the DID itself. 340 + 341 + However, in theory, whoever operates the PLC registry [could](https://github.com/did-method-plc/did-method-plc?tab=readme-ov-file#plc-server-trust-model) deny my requests to update the DID Document, or refuse to serve some information about it. Bluesky is currently moving PLC to [an independent legal entity in Switzerland](https://docs.bsky.app/blog/plc-directory-org) to address some of these concerns. The AT community is also [thinking](https://updates.microcosm.blue/3lz7nwvh4zc2u) and [experimenting](https://plc.wtf/). 342 + 343 + --- 344 + 345 + ### From Hosting to JSON 346 + 347 + So far, you've learned how to: 348 + 349 + * Resolve a handle to a DID. 350 + * Grab the DID Document for that DID. 351 + 352 + That actually tells you enough to get the JSON by its `at://` URI! 353 + 354 + Each DID Document includes the `serviceEndpoint` which is the actual hosting. *That's* the service you can hit by HTTPS to grab any JSON record it stores. 355 + 356 + For example, the `@ruuuuu.de` handle resolves to `did:web:iam.ruuuuu.de`, and its DID Document has a `serviceEndpoint` pointing at `https://blacksky.app`. 357 + 358 + To get the <span style={{wordBreak: 'break-word'}}>[`at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z)</span> record, hit the `https://blacksky.app` server with the [`com.atproto.repo.getRecord`](https://docs.bsky.app/docs/api/com-atproto-repo-get-record) endpoint, passing different parts of the `at://` URI as parameters: 359 + 360 + ```sh 361 + $ curl "https://blacksky.app/xrpc/com.atproto.repo.getRecord?\ 362 + repo=ruuuuu.de&collection=app.bsky.feed.post&rkey=3lzy2ji4nms2z" | jq 363 + ``` 364 + 365 + And there it is: 366 + 367 + ```json 368 + { 369 + "uri": "at://did:web:iam.ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z", 370 + "cid": "bafyreiae4ehmkk4rtajs5ncagjhrsv6rj3v6fggphlbpyfco4dzddp42nu", 371 + "value": { 372 + "text": "posting from did:web, like a boss", 373 + "$type": "app.bsky.feed.post", 374 + "langs": [ 375 + "en" 376 + ], 377 + "createdAt": "2025-09-29T12:53:23.048Z" 378 + } 379 + } 380 + ``` 381 + 382 + Now let's get <span style={{wordBreak: 'break-word'}}>[`at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22)</span>: 383 + 384 + - The `@danabra.mov` handle resolves to `did:plc:fpruhuo22xkm5o7ttr2ktxdo`. 385 + - The DID Document for `did:plc:fpruhuo22xkm5o7ttr2ktxdo` points at `https://morel.us-east.host.bsky.network` as the current hosting. 386 + 387 + Let's hit it: 388 + 389 + ```sh 390 + $ curl "https://morel.us-east.host.bsky.network/xrpc/com.atproto.repo.getRecord?\ 391 + repo=danabra.mov&collection=sh.tangled.feed.star&rkey=3m23ddgjpgn22" | jq 392 + ``` 393 + 394 + And there you have it: 395 + 396 + ```json 397 + { 398 + "uri": "at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/sh.tangled.feed.star/3m23ddgjpgn22", 399 + "cid": "bafyreiaghm4ep5eeqx6yf55z43ge65qswwis7aiwc67rt7ni54jj6pg6fa", 400 + "value": { 401 + "$type": "sh.tangled.feed.star", 402 + "subject": "at://did:plc:dzmqinfp7efnofbqg5npjmth/sh.tangled.repo/3m232u6xrq222", 403 + "createdAt": "2025-09-30T20:09:02Z" 404 + } 405 + } 406 + ``` 407 + 408 + And that's how you resolve an `at://` URI. 409 + 410 + *Exercise: In the record above, the `subject` is a link to another record. Figure out the handle of its owner and the contents of that record. Use [pdsls](https://pdsls.dev/) to check your answer.* 411 + 412 + --- 413 + 414 + ### In Conclusion 415 + 416 + To resolve an arbitrary `at://` URI, you need to follow three steps: 417 + 418 + 1. Resolve the handle to an identity (using DNS and/or HTTPS). 419 + 2. Resolve that identity to a hosting (using the DID Document). 420 + 3. Request the JSON from that hosting (by hitting it with `getRecord`). 421 + 422 + If you're building a client app or a small project, an [SDK](https://sdk.blue/) will handle all of this for you. However, for good performance, you'll want to hit a resolution cache instead of doing DNS/HTTPS lookups on every request. [QuickDID](https://quickdid.smokesignal.tools/) is one such cache. You can also check out the [pdsls source](https://tangled.org/@pdsls.dev/pdsls/blob/main/src/utils/api.ts) to see how exactly it handles resolution. 423 + 424 + In practice, a lot of apps don't end up needing to resolve `at://` URIs or load JSON records because they *receive* data from the network [via a websocket](https://pdsls.dev/jetstream?instance=wss%3A%2F%2Fjetstream1.us-east.bsky.network%2Fsubscribe) and aggregate it in a local database. If that's your approach, you'll still use the `at://` URIs as unique identifiers for user-created data, but the data itself will get pushed to you rather than pulled by you. Still, it's useful to know that you *can* fetch it on demand. 425 + 426 + The AT protocol is fundamentally an abstraction over HTTP, DNS, and JSON. But by standardizing how these pieces fit together—putting the user in the authority position, separating identity from hosting, and making data portable—it turns the web into a place where [your content belongs to you](/open-social/), not to the apps that display it. 427 + 428 + There's more to explore in the atmosphere, but now you know where it's `at://`.