feat(website): rebuild marketing pages on new brand chassis

This commit is contained in:
Ogulcan Celik
2026-08-03 21:39:25 +03:00
parent eacea2daf0
commit 22bb476d64
48 changed files with 2618 additions and 5402 deletions
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

-145
View File
@@ -1,145 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 539.99603 66.958542"
version="1.1"
id="svg36"
sodipodi:docname="atlassian-logo-gradient-horizontal-blue.svg"
width="539.99603"
height="66.958542"
inkscape:version="0.92.1 r15371">
<metadata
id="metadata40">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>atlassian-logo-gradient-horizontal-blue</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="780"
inkscape:window-height="480"
id="namedview38"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="0.81666667"
inkscape:cx="358.15928"
inkscape:cy="25.92"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg36" />
<defs
id="defs9">
<style
id="style2">.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#2684ff;}.cls-3{fill:#0052cc;}</style>
<linearGradient
id="linear-gradient"
x1="28.540001"
y1="69.489998"
x2="11.41"
y2="99.169998"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-0.0039818,-33.961454)">
<stop
offset="0"
stop-color="#0052cc"
id="stop4" />
<stop
offset="0.92"
stop-color="#2684ff"
id="stop6" />
</linearGradient>
</defs>
<title
id="title11">atlassian-logo-gradient-horizontal-blue</title>
<path
style="fill:url(#linear-gradient)"
inkscape:connector-curvature="0"
id="path13"
d="m 19.636018,30.518546 a 1.88,1.88 0 0 0 -3.2,0.35 l -16.2299998,32.46 a 1.94,1.94 0 0 0 1.73,2.81 H 24.536018 a 1.87,1.87 0 0 0 1.74,-1.1 c 4.87,-10 1.92,-25.37 -6.64,-34.52 z"
class="cls-1" />
<path
style="fill:#2684ff"
inkscape:connector-curvature="0"
id="path15"
d="m 31.546018,1.038546 a 42.81,42.81 0 0 0 -2.5,42.27 l 10.95,21.73 a 1.94,1.94 0 0 0 1.73,1.08 h 22.6 a 2,2 0 0 0 1.67,-2.79 l -31.15,-62.29 a 1.83,1.83 0 0 0 -3.3,0 z"
class="cls-2" />
<polygon
style="fill:#0052cc"
id="polygon17"
points="509.01,69.6 525.42,100.1 540,100.1 540,45.66 528.4,45.66 528.4,80.79 524.01,70.6 510.83,45.66 492.52,45.66 492.52,100.1 504.12,100.1 504.12,58.58 "
class="cls-3"
transform="translate(-0.0039818,-33.961454)" />
<rect
style="fill:#0052cc"
id="rect19"
height="54.439999"
width="12.68"
y="11.698545"
x="406.05603"
class="cls-3" />
<path
style="fill:#0052cc"
inkscape:connector-curvature="0"
id="path21"
d="m 391.42602,50.038546 c 0,-9.53 -5.05,-14 -19.31,-17.07 -7.87,-1.74 -9.77,-3.48 -9.77,-6 0,-3.15 2.81,-4.48 8,-4.48 6.3,0 12.52,1.91 18.4,4.56 v -12.45 a 41.57,41.57 0 0 0 -18,-3.73 c -14.09,0 -21.38,6.13 -21.38,16.16 0,8 3.73,14.41 18.31,17.23 8.7,1.82 10.53,3.23 10.53,6.13 0,2.9 -1.83,4.64 -8,4.64 a 45,45 0 0 1 -21.13,-5.71 v 13.09 c 4.39,2.15 10.19,4.55 21,4.55 15.24,0 21.29,-6.79 21.29,-16.9"
class="cls-3" />
<path
style="fill:#0052cc"
inkscape:connector-curvature="0"
id="path23"
d="m 338.64602,50.038546 c 0,-9.53 -5,-14 -19.3,-17.07 -7.87,-1.74 -9.78,-3.48 -9.78,-6 0,-3.15 2.82,-4.48 8,-4.48 6.3,0 12.51,1.91 18.39,4.56 v -12.45 a 41.57,41.57 0 0 0 -18,-3.73 c -14.08,0 -21.38,6.13 -21.38,16.16 0,8 3.73,14.41 18.32,17.23 8.7,1.82 10.52,3.23 10.52,6.13 0,2.9 -1.82,4.64 -8,4.64 a 45,45 0 0 1 -21.13,-5.71 v 13.09 c 4.4,2.15 10.2,4.55 21,4.55 15.25,0 21.29,-6.79 21.29,-16.9"
class="cls-3" />
<polygon
style="fill:#0052cc"
id="polygon25"
points="210.92,88.33 210.92,45.66 198.16,45.66 198.16,100.1 224.22,100.1 228.32,88.33 "
class="cls-3"
transform="translate(-0.0039818,-33.961454)" />
<polygon
style="fill:#0052cc"
id="polygon27"
points="173.52,100.1 173.52,57.42 188.6,57.42 188.6,45.66 146.67,45.66 146.67,57.42 160.76,57.42 160.76,100.1 "
class="cls-3"
transform="translate(-0.0039818,-33.961454)" />
<path
style="fill:#0052cc"
inkscape:connector-curvature="0"
id="path29"
d="m 128.16602,11.698546 h -16.72 l -19.000002,54.44 h 14.550002 l 2.7,-9.17 a 36,36 0 0 0 20.3,0 l 2.69,9.17 h 14.5 z m -8.36,35.46 a 24.75,24.75 0 0 1 -7,-1 l 7,-23.76 7,23.76 a 24.8,24.8 0 0 1 -7,1 z"
class="cls-3" />
<path
style="fill:#0052cc"
inkscape:connector-curvature="0"
id="path31"
d="m 269.13602,11.698546 h -16.72 l -19,54.44 h 14.5 l 2.7,-9.17 a 36,36 0 0 0 20.3,0 l 2.69,9.17 h 14.5 z m -8.36,35.46 a 24.69,24.69 0 0 1 -7,-1 l 7,-23.76 7,23.76 a 24.68,24.68 0 0 1 -7,1 z"
class="cls-3" />
<path
style="fill:#0052cc"
inkscape:connector-curvature="0"
id="path33"
d="m 464.41602,11.698546 h -16.72 l -19,54.44 h 14.5 l 2.69,-9.17 a 36,36 0 0 0 20.3,0 l 2.69,9.17 h 14.51 z m -8.36,35.46 a 24.8,24.8 0 0 1 -7,-1 l 7,-23.76 7,23.76 a 24.8,24.8 0 0 1 -7,1 z"
class="cls-3" />
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

@@ -1,8 +0,0 @@
<svg viewBox="0 0 494.35 38.19" xmlns="http://www.w3.org/2000/svg">
<path
d="M179.25 38.19c-12.58 0-20.74-9.07-20.74-18.51v-1.17c0-9.6 8.16-18.51 20.74-18.51 12.58 0 20.81 8.91 20.81 18.51v1.17c0 9.44-8.16 18.51-20.81 18.51Zm14.08-19.58c0-6.88-5-13-14.08-13s-14 6.13-14 13v.86c0 6.88 5 13.12 14 13.12s14.08-6.24 14.08-13.12v-.86Z"
fill="#3499CD" />
<path
d="M37.12 36.8 32.43 28H11.57L7 36.8H0L19.2 1.28h5.55L44.27 36.8h-7.15ZM21.81 8.21l-7.73 14.94h15.73l-8-14.94ZM74.56 38.19c-12.75 0-18.67-6.94-18.67-16.16V1.28h6.62v20.85c0 6.56 4.32 10.46 12.53 10.46 8.43 0 11.89-3.9 11.89-10.46V1.28h6.67V22c0 8.83-5.6 16.19-19.04 16.19ZM130.72 6.83v30h-6.67v-30h-15.52V1.28h37.71v5.55h-15.52ZM259.47 36.8V8.69l-1.76 3.1-14.88 25h-3.25l-14.72-25-1.76-3.1V36.8h-6.51V1.28h9.23l14 24.37 1.66 3 1.65-3L257 1.28h9.12V36.8h-6.65ZM316.54 36.8l-4.7-8.8H291l-4.53 8.8h-7l19.2-35.52h5.54l19.47 35.52h-7.14ZM301.23 8.21l-7.73 14.94h15.73l-8-14.94ZM350.56 6.83v30h-6.66v-30h-15.52V1.28h37.7v5.55h-15.52ZM399.47 6.83v30h-6.67v-30h-15.52V1.28H415v5.55h-15.53ZM431.42 36.8V4.64c2.66 0 3.73-1.44 3.73-3.36H438V36.8h-6.58ZM490.19 11.31A20.69 20.69 0 0 0 476 5.6c-9.49 0-14.83 6.51-14.83 13.28v.69c0 6.72 5.39 13 15.31 13a20.78 20.78 0 0 0 13.92-5.71l4 4.21a26.53 26.53 0 0 1-18.4 7.12c-13.44 0-21.6-8.75-21.6-18.35v-1.17c0-9.6 8.91-18.67 21.92-18.67 7.52 0 14.35 3.15 18 7.09l-4.13 4.22ZM180.072 13.173l-6.107 9.46a2.58 2.58 0 0 0 .767 3.567l.009.005a2.58 2.58 0 0 0 3.567-.768l6.108-9.46a2.58 2.58 0 0 0-.768-3.566l-.009-.006a2.58 2.58 0 0 0-3.567.768Z"
fill="#010101" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-38
View File
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 304 182" style="enable-background:new 0 0 304 182;" xml:space="preserve">
<style type="text/css">
.st0{fill:#252F3E;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
</style>
<g>
<path class="st0" d="M86.4,66.4c0,3.7,0.4,6.7,1.1,8.9c0.8,2.2,1.8,4.6,3.2,7.2c0.5,0.8,0.7,1.6,0.7,2.3c0,1-0.6,2-1.9,3l-6.3,4.2
c-0.9,0.6-1.8,0.9-2.6,0.9c-1,0-2-0.5-3-1.4C76.2,90,75,88.4,74,86.8c-1-1.7-2-3.6-3.1-5.9c-7.8,9.2-17.6,13.8-29.4,13.8
c-8.4,0-15.1-2.4-20-7.2c-4.9-4.8-7.4-11.2-7.4-19.2c0-8.5,3-15.4,9.1-20.6c6.1-5.2,14.2-7.8,24.5-7.8c3.4,0,6.9,0.3,10.6,0.8
c3.7,0.5,7.5,1.3,11.5,2.2v-7.3c0-7.6-1.6-12.9-4.7-16c-3.2-3.1-8.6-4.6-16.3-4.6c-3.5,0-7.1,0.4-10.8,1.3c-3.7,0.9-7.3,2-10.8,3.4
c-1.6,0.7-2.8,1.1-3.5,1.3c-0.7,0.2-1.2,0.3-1.6,0.3c-1.4,0-2.1-1-2.1-3.1v-4.9c0-1.6,0.2-2.8,0.7-3.5c0.5-0.7,1.4-1.4,2.8-2.1
c3.5-1.8,7.7-3.3,12.6-4.5c4.9-1.3,10.1-1.9,15.6-1.9c11.9,0,20.6,2.7,26.2,8.1c5.5,5.4,8.3,13.6,8.3,24.6V66.4z M45.8,81.6
c3.3,0,6.7-0.6,10.3-1.8c3.6-1.2,6.8-3.4,9.5-6.4c1.6-1.9,2.8-4,3.4-6.4c0.6-2.4,1-5.3,1-8.7v-4.2c-2.9-0.7-6-1.3-9.2-1.7
c-3.2-0.4-6.3-0.6-9.4-0.6c-6.7,0-11.6,1.3-14.9,4c-3.3,2.7-4.9,6.5-4.9,11.5c0,4.7,1.2,8.2,3.7,10.6
C37.7,80.4,41.2,81.6,45.8,81.6z M126.1,92.4c-1.8,0-3-0.3-3.8-1c-0.8-0.6-1.5-2-2.1-3.9L96.7,10.2c-0.6-2-0.9-3.3-0.9-4
c0-1.6,0.8-2.5,2.4-2.5h9.8c1.9,0,3.2,0.3,3.9,1c0.8,0.6,1.4,2,2,3.9l16.8,66.2l15.6-66.2c0.5-2,1.1-3.3,1.9-3.9c0.8-0.6,2.2-1,4-1
h8c1.9,0,3.2,0.3,4,1c0.8,0.6,1.5,2,1.9,3.9l15.8,67l17.3-67c0.6-2,1.3-3.3,2-3.9c0.8-0.6,2.1-1,3.9-1h9.3c1.6,0,2.5,0.8,2.5,2.5
c0,0.5-0.1,1-0.2,1.6c-0.1,0.6-0.3,1.4-0.7,2.5l-24.1,77.3c-0.6,2-1.3,3.3-2.1,3.9c-0.8,0.6-2.1,1-3.8,1h-8.6c-1.9,0-3.2-0.3-4-1
c-0.8-0.7-1.5-2-1.9-4L156,23l-15.4,64.4c-0.5,2-1.1,3.3-1.9,4c-0.8,0.7-2.2,1-4,1H126.1z M254.6,95.1c-5.2,0-10.4-0.6-15.4-1.8
c-5-1.2-8.9-2.5-11.5-4c-1.6-0.9-2.7-1.9-3.1-2.8c-0.4-0.9-0.6-1.9-0.6-2.8v-5.1c0-2.1,0.8-3.1,2.3-3.1c0.6,0,1.2,0.1,1.8,0.3
c0.6,0.2,1.5,0.6,2.5,1c3.4,1.5,7.1,2.7,11,3.5c4,0.8,7.9,1.2,11.9,1.2c6.3,0,11.2-1.1,14.6-3.3c3.4-2.2,5.2-5.4,5.2-9.5
c0-2.8-0.9-5.1-2.7-7c-1.8-1.9-5.2-3.6-10.1-5.2L246,52c-7.3-2.3-12.7-5.7-16-10.2c-3.3-4.4-5-9.3-5-14.5c0-4.2,0.9-7.9,2.7-11.1
c1.8-3.2,4.2-6,7.2-8.2c3-2.3,6.4-4,10.4-5.2c4-1.2,8.2-1.7,12.6-1.7c2.2,0,4.5,0.1,6.7,0.4c2.3,0.3,4.4,0.7,6.5,1.1
c2,0.5,3.9,1,5.7,1.6c1.8,0.6,3.2,1.2,4.2,1.8c1.4,0.8,2.4,1.6,3,2.5c0.6,0.8,0.9,1.9,0.9,3.3v4.7c0,2.1-0.8,3.2-2.3,3.2
c-0.8,0-2.1-0.4-3.8-1.2c-5.7-2.6-12.1-3.9-19.2-3.9c-5.7,0-10.2,0.9-13.3,2.8c-3.1,1.9-4.7,4.8-4.7,8.9c0,2.8,1,5.2,3,7.1
c2,1.9,5.7,3.8,11,5.5l14.2,4.5c7.2,2.3,12.4,5.5,15.5,9.6c3.1,4.1,4.6,8.8,4.6,14c0,4.3-0.9,8.2-2.6,11.6
c-1.8,3.4-4.2,6.4-7.3,8.8c-3.1,2.5-6.8,4.3-11.1,5.6C264.4,94.4,259.7,95.1,254.6,95.1z"/>
<g>
<path class="st1" d="M273.5,143.7c-32.9,24.3-80.7,37.2-121.8,37.2c-57.6,0-109.5-21.3-148.7-56.7c-3.1-2.8-0.3-6.6,3.4-4.4
c42.4,24.6,94.7,39.5,148.8,39.5c36.5,0,76.6-7.6,113.5-23.2C274.2,133.6,278.9,139.7,273.5,143.7z"/>
<path class="st1" d="M287.2,128.1c-4.2-5.4-27.8-2.6-38.5-1.3c-3.2,0.4-3.7-2.4-0.8-4.5c18.8-13.2,49.7-9.4,53.3-5
c3.6,4.5-1,35.4-18.6,50.2c-2.7,2.3-5.3,1.1-4.1-1.9C282.5,155.7,291.4,133.4,287.2,128.1z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.0"
width="1621.9041"
height="279.81476"
viewBox="0 0 16219.041 2798.1476"
preserveAspectRatio="xMidYMid meet"
id="svg30">
<defs
id="defs34" />
<g
id="g68"
transform="translate(-5889.2439,-88.063461)">
<path
style="fill:#3259b4;fill-opacity:1;stroke:none;stroke-width:10px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 5889.2439,198.14279 495.357,143.10312 V 2322.6738 l -495.357,143.1031 z"
id="path58" />
<path
style="fill:#3c8cff;fill-opacity:1;stroke:none;stroke-width:10px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 6670.8071,1210.8726 484.3491,121.0872 v 1067.7695 l -484.3491,99.0714 z"
id="path60" />
<path
style="fill:#00c8d2;fill-opacity:1;stroke:none;stroke-width:10px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 7496.4021,1001.7219 7947.7273,880.63461 V 2190.5786 l -451.3252,-132.0952 z"
id="path62" />
<path
style="fill:#78e6dd;fill-opacity:1;stroke:none;stroke-width:10px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 8244.9415,88.063461 495.357,143.103119 V 2454.769 l -495.357,121.0872 z"
id="path64" />
<path
id="path49"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2647.62px;line-height:1.25;font-family:'Geometr706 Md BT';-inkscape-font-specification:'Geometr706 Md BT';letter-spacing:0px;word-spacing:0px;fill:#3259b4;fill-opacity:1;stroke-width:93.437"
d="m 20976.329,1586.9411 h 874.026 q -22.003,-165.8898 -141.8,-268.8141 -119.796,-102.9243 -290.934,-102.9243 -173.582,0 -298.269,105.346 -123.464,104.1352 -143.023,266.3924 z m 0,184.0528 q 12.225,179.2094 141.8,290.6098 130.799,111.4005 328.83,111.4005 127.13,0 237.149,-53.2785 110.016,-53.2785 201.697,-157.4136 l 167.471,133.1961 q -125.909,145.3049 -276.266,214.3247 -149.134,69.0198 -337.385,69.0198 -155.246,0 -289.713,-55.7002 -134.465,-55.7002 -231.036,-158.6245 -84.347,-90.8156 -129.576,-209.4812 -45.229,-119.8766 -45.229,-250.651 0,-309.9837 187.03,-501.3018 187.028,-191.3181 488.966,-191.3181 308.047,0 497.521,204.6377 190.697,203.4269 190.697,537.628 v 16.9523 z m -512.541,-423.8059 q -60.745,-67.809 -138.847,-99.2917 -76.862,-32.6936 -176.038,-32.6936 -192.155,0 -316.126,135.6179 -122.73,134.4071 -122.73,345.0991 0,210.6921 121.491,343.8883 122.73,133.1962 317.365,133.1962 105.375,0 185.957,-36.3263 81.819,-36.3262 138.846,-107.7678 l 152.484,170.7333 q -104.136,93.2373 -219.428,136.8287 -114.053,42.3806 -257.859,42.3806 -147.525,0 -272.736,-48.4349 -125.209,-48.4351 -216.948,-140.4615 -97.937,-96.8698 -148.765,-221.5899 -49.589,-125.9309 -49.589,-272.4467 0,-146.5157 49.589,-272.4467 50.828,-125.9308 148.765,-222.8008 92.978,-93.2373 216.948,-140.4614 123.971,-48.4349 272.736,-48.4349 141.326,0 255.38,41.1697 115.292,41.1697 209.51,124.72 z m -2257.02,998.9712 V 1044.4695 h 232.044 v 148.9375 q 87.633,-88.3938 183.906,-130.7744 97.507,-43.5915 208.592,-43.5915 228.34,0 360.407,152.5702 132.067,152.5701 132.067,420.1733 v 754.3746 h -234.512 v -731.368 q 0,-204.6378 -72.822,-299.0859 -71.587,-95.6591 -225.871,-95.6591 -175.266,0 -264.134,101.7134 -87.633,100.5026 -87.633,302.7185 v 721.6811 z m -892.716,-181.6312 q 196.244,0 322.659,-131.9853 126.414,-131.9853 126.414,-339.0447 0,-205.8486 -126.414,-337.8338 -126.415,-133.1962 -322.659,-133.1962 -196.242,0 -322.659,131.9853 -126.414,131.9852 -126.414,339.0447 0,208.2703 126.414,340.2556 126.417,130.7744 322.659,130.7744 z m 665.785,181.6312 h -228.75 v -213.1139 q -81.87,115.033 -199.856,173.155 -116.783,56.911 -268.481,56.911 -134.843,0 -257.645,-50.8567 -121.6,-52.0676 -216.712,-150.1483 -90.296,-94.4482 -136.045,-213.1139 -45.751,-118.6656 -45.751,-257.9162 0,-301.5076 179.389,-486.7714 180.592,-185.2637 476.764,-185.2637 149.29,0 273.296,61.7546 124.007,60.5437 211.895,176.7876 v -213.1138 h 211.896 z M 15269.933,675.15287 V 2123.3582 h 180.032 q 393.672,0 592.908,-182.8419 200.436,-182.842 200.436,-541.2608 0,-359.6295 -200.436,-541.26064 -199.236,-182.84199 -592.908,-182.84199 z m 142.825,-224.0117 q 528.095,0 802.946,242.17481 276.05,240.96395 276.05,705.93952 0,463.7648 -276.05,705.9397 -274.851,240.964 -802.946,240.964 h -388.87 V 451.14117 Z M 13561.982,1586.9411 h 859.921 q -21.648,-165.8898 -139.511,-268.8141 -117.863,-102.9243 -286.24,-102.9243 -170.781,0 -293.455,105.346 -121.471,104.1352 -140.715,266.3924 z m 0,184.0528 q 12.028,179.2094 139.513,290.6098 128.687,111.4005 323.522,111.4005 125.079,0 233.321,-53.2785 108.241,-53.2785 198.443,-157.4136 l 164.768,133.1961 q -123.876,145.3049 -271.807,214.3247 -146.728,69.0198 -331.941,69.0198 -152.741,0 -285.037,-55.7002 -132.295,-55.7002 -227.307,-158.6245 -82.985,-90.8156 -127.484,-209.4812 -44.5,-119.8766 -44.5,-250.651 0,-309.9837 184.01,-501.3018 184.012,-191.3181 481.075,-191.3181 303.076,0 489.494,204.6377 187.618,203.4269 187.618,537.628 v 16.9523 z m -868.74,-1043.76734 v 317.24614 h -194.609 -260.176 l -349.805,823.3984 c -5.809,13.7232 -17.015,44.384 -33.613,92.0117 -15.769,46.8205 -35.259,104.5479 -58.496,173.1641 -20.747,-72.6525 -38.597,-131.9946 -53.535,-178.0078 -14.109,-46.0133 -24.074,-75.0593 -29.883,-87.168 l -344.824,-823.3984 h -265.137 l 558.926,1301.6796 -235.274,540.0586 h 240.254 l 747.813,-1656.2695 278.359,8.2617 v 837.9297 c 0,108.9787 28.069,186.8709 84.199,233.6914 56.129,46.0132 150.447,69.0235 282.95,69.0235 h 147.695 v -219.1602 h -138.027 c -39.567,0 -68.551,-8.8811 -86.954,-26.6406 -17.483,-17.7595 -26.23,-46.0176 -26.23,-84.7657 v -810.0781 h 251.211 v -193.7304 h -251.211 V 727.22656 Z M 9676.5955,2346.1592 V 451.14117 h 367.1415 q 212.556,0 335.397,19.37398 124.221,18.16311 205.655,60.5437 139.403,72.65245 211.175,184.05287 73.153,110.18953 73.153,249.4401 0,115.03298 -67.632,210.69198 -66.25,95.6591 -189.091,156.2028 201.513,58.122 305.031,181.6312 104.898,122.2982 104.898,302.7185 0,224.0117 -201.514,377.7927 -200.134,152.5702 -499.645,152.5702 z m 280.1873,-1078.8889 h 231.8792 q 193.233,0 298.13,-78.7068 106.278,-78.7069 106.278,-221.59 0,-150.14836 -106.278,-226.43343 -106.278,-76.28507 -317.453,-76.28507 h -212.5562 z m 0,856.0879 h 285.7082 q 241.541,0 365.762,-81.1284 125.601,-81.1286 125.601,-239.7531 0,-156.2028 -121.46,-236.1205 -121.461,-81.1285 -358.861,-81.1285 h -296.7502 z" />
</g>
<metadata
id="metadata70">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2333.95 530.79">
<defs>
<style>
.cls-1 {
fill: #1d63ed;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1" d="M661.56,218.08c-16.49-11.1-59.81-15.84-91.3-7.35-1.69-31.37-17.88-57.81-47.47-80.88l-10.95-7.35-7.3,11.03c-14.35,21.78-20.4,50.81-18.26,77.2,1.69,16.26,7.34,34.53,18.26,47.79-40.99,23.78-78.78,18.38-246.12,18.38H.06c-.75,37.79,5.32,110.47,51.54,169.64,5.11,6.54,10.7,12.86,16.78,18.95,37.58,37.63,94.36,65.23,179.26,65.3,129.53.12,240.5-69.9,308.01-239.18,22.21.36,80.85,3.98,109.55-51.47.7-.93,7.3-14.7,7.3-14.7l-10.94-7.35ZM168.67,183.53h-72.65v72.65h72.65v-72.65ZM262.52,183.53h-72.65v72.65h72.65v-72.65ZM356.38,183.53h-72.65v72.65h72.65v-72.65ZM450.24,183.53h-72.65v72.65h72.65v-72.65ZM74.81,183.53H2.16v72.65h72.65v-72.65ZM168.67,91.77h-72.65v72.65h72.65v-72.65ZM262.52,91.77h-72.65v72.65h72.65v-72.65ZM356.38,91.77h-72.65v72.65h72.65v-72.65ZM356.38,0h-72.65v72.65h72.65V0Z"/>
<g>
<path class="cls-1" d="M2329.44,419.3c0,18.94-14.87,33.81-34.21,33.81s-34.42-14.87-34.42-33.81,15.27-33.4,34.42-33.4,34.21,14.87,34.21,33.4ZM2269.37,419.3c0,14.87,11,26.68,26.07,26.68s25.46-11.81,25.46-26.47-10.8-26.89-25.65-26.89-25.87,12.02-25.87,26.68ZM2289.95,436.82h-7.74v-33.4c3.04-.61,7.33-1.02,12.82-1.02,6.32,0,9.16,1.02,11.61,2.45,1.84,1.42,3.26,4.07,3.26,7.33,0,3.67-2.85,6.52-6.91,7.74v.41c3.24,1.21,5.08,3.66,6.1,8.14,1.01,5.09,1.62,7.13,2.45,8.35h-8.35c-1.02-1.22-1.64-4.27-2.65-8.15-.61-3.66-2.65-5.29-6.93-5.29h-3.66v13.45ZM2290.14,417.88h3.66c4.28,0,7.74-1.42,7.74-4.88,0-3.06-2.23-5.11-7.13-5.11-2.03,0-3.46.21-4.27.43v9.56Z"/>
<path class="cls-1" d="M1017.16,81.28c-4.79-4.68-10.54-7.06-17.43-7.06s-12.81,2.38-17.42,7.06c-4.62,4.68-6.88,10.68-6.88,17.83v119.4c-23.7-19.59-51.05-29.47-82.16-29.47-36.16,0-67.08,13.06-92.7,39.27-25.62,26.12-38.34,57.72-38.34,94.78s12.81,68.57,38.34,94.78c25.62,26.12,56.46,39.27,92.7,39.27s66.74-13.06,92.7-39.27c25.62-25.86,38.34-57.45,38.34-94.78V99.11c0-7.15-2.35-13.15-7.15-17.83ZM968.98,355.39v.18c-4.27,10.15-10.11,19.06-17.51,26.65-7.4,7.68-16.12,13.68-26.05,18.18-10.02,4.5-20.65,6.71-32.06,6.71s-22.3-2.21-32.32-6.71c-10.02-4.5-18.65-10.5-25.96-18.09-7.32-7.59-13.15-16.5-17.42-26.65-4.27-10.24-6.45-21.09-6.45-32.57s2.18-22.33,6.45-32.57c4.27-10.24,10.11-19.06,17.42-26.65,7.32-7.59,16.03-13.59,25.96-18.09,10.02-4.5,20.74-6.71,32.32-6.71s22.04,2.21,32.06,6.71c10.02,4.5,18.65,10.5,26.05,18.18,7.4,7.68,13.24,16.59,17.51,26.65,4.27,10.15,6.45,20.92,6.45,32.39s-2.18,22.33-6.45,32.39Z"/>
<path class="cls-1" d="M2099.77,271.64c-6.36-15.89-16.05-30.27-28.76-43.16l-.17-.09c-25.88-26.12-56.82-39.27-92.7-39.27s-67.09,13.06-92.71,39.27c-25.62,26.12-38.33,57.72-38.33,94.78s12.81,68.57,38.33,94.78c25.62,26.12,56.47,39.27,92.71,39.27,32.92,0,61.41-10.85,85.64-32.56,4.69-4.94,7.06-10.94,7.06-17.92s-2.26-13.15-6.89-17.83c-4.61-4.68-10.45-7.06-17.42-7.06-6.09.18-11.5,2.21-16.11,6.27-7.32,6.35-15.25,11.21-23.87,14.39-8.63,3.18-18.04,4.77-28.31,4.77-9.07,0-17.78-1.41-26.05-4.32-8.29-2.91-16.03-6.89-22.92-12.09-6.98-5.21-12.98-11.38-18.12-18.71-5.14-7.24-9.06-15.27-11.67-24.09h185.32c6.87,0,12.62-2.38,17.42-7.06,4.8-4.68,7.15-10.68,7.15-17.83,0-18.53-3.24-35.74-9.58-51.54ZM1899.29,298.29c2.53-8.74,6.36-16.77,11.5-24.09,5.15-7.24,11.24-13.5,18.21-18.71,7.06-5.21,14.72-9.18,23.17-12.09,8.44-2.91,17.06-4.32,25.97-4.32s17.51,1.41,25.86,4.32c8.37,2.91,16.05,6.88,22.92,12.09,6.98,5.21,13.07,11.38,18.21,18.71,5.22,7.24,9.16,15.27,11.86,24.09h-157.71Z"/>
<path class="cls-1" d="M2327.51,205.89c-4.36-4.32-9.85-7.68-16.47-10.15-6.62-2.47-13.85-4.15-21.78-5.12-7.84-.97-15.25-1.41-22.12-1.41-15.61,0-30.24,2.56-44,7.68-13.77,5.12-26.49,12.44-38.17,21.97v-4.76c0-6.88-2.35-12.71-7.15-17.56-4.78-4.85-10.45-7.32-17.15-7.32s-12.64,2.47-17.42,7.32c-4.8,4.85-7.15,10.77-7.15,17.56v218.25c0,6.88,2.35,12.71,7.15,17.56,4.78,4.85,10.53,7.32,17.42,7.32s12.45-2.47,17.15-7.32c4.8-4.85,7.15-10.77,7.15-17.56v-109.17c0-11.65,2.18-22.59,6.45-32.83,4.27-10.24,10.11-19.06,17.51-26.65,7.42-7.59,16.13-13.59,26.05-17.92,10.02-4.41,20.66-6.62,32.08-6.62s22.2,2.03,32.06,6c3.91,1.77,7.32,2.65,10.28,2.65,3.4,0,6.62-.62,9.58-1.94,2.96-1.32,5.58-3.09,7.76-5.38,2.18-2.29,3.91-4.94,5.22-8.03,1.31-3,2.01-6.27,2.01-9.8,0-6.88-2.18-12.44-6.53-16.77h.08Z"/>
<path class="cls-1" d="M1304.49,271.73c-6.36-15.8-15.86-30.27-28.66-43.33-25.87-26.12-56.8-39.27-92.7-39.27s-67.08,13.06-92.7,39.27c-25.62,26.12-38.33,57.72-38.33,94.78s12.81,68.57,38.33,94.78c25.62,26.12,56.46,39.27,92.7,39.27s66.74-13.06,92.7-39.27c25.62-25.86,38.34-57.45,38.34-94.78-.18-18.53-3.4-35.65-9.67-51.45ZM1258.84,355.39v.18c-4.27,10.15-10.11,19.06-17.51,26.65-7.4,7.68-16.12,13.68-26.05,18.18-9.93,4.5-20.65,6.71-32.06,6.71s-22.3-2.21-32.32-6.71c-10.02-4.5-18.65-10.5-25.96-18.09-7.32-7.59-13.15-16.5-17.42-26.65-4.27-10.24-6.45-21.09-6.45-32.57s2.18-22.33,6.45-32.57c4.27-10.24,10.11-19.06,17.42-26.65,7.32-7.59,16.03-13.59,25.96-18.09,10.02-4.5,20.74-6.71,32.32-6.71s22.04,2.21,32.06,6.71c10.02,4.5,18.65,10.5,26.05,18.18,7.4,7.68,13.24,16.59,17.51,26.65,4.27,10.15,6.45,20.92,6.45,32.39s-2.18,22.33-6.45,32.39Z"/>
<path class="cls-1" d="M1828.62,214.01c0-3.35-.7-6.53-2-9.53-1.31-3-3.05-5.73-5.23-8.03-2.18-2.29-4.79-4.15-7.75-5.38-2.96-1.23-6.18-1.94-9.58-1.94-4.88,0-9.24,1.24-13.07,3.8l-139.92,93.11V99.29c0-7.06-2.35-12.97-7.14-17.83-4.79-4.85-10.45-7.32-17.16-7.32s-12.63,2.47-17.43,7.32c-4.79,4.85-7.14,10.77-7.14,17.83v332.71c0,6.88,2.35,12.8,7.14,17.74,4.79,4.94,10.54,7.41,17.43,7.41s12.46-2.47,17.16-7.41c4.79-4.94,7.14-10.86,7.14-17.74v-86.4l28.58-19.15,108.12,124.17c4.36,4.32,9.85,6.44,16.38,6.44,3.4,0,6.62-.62,9.58-1.94,2.96-1.24,5.58-3.09,7.75-5.38,2.18-2.29,3.92-4.94,5.23-8.03,1.31-3,2-6.27,2-9.53,0-6.53-2.26-12.36-6.8-17.47l-100.63-115.87,98.01-65.13c6.27-4.32,9.32-10.94,9.32-19.86v.18Z"/>
<path class="cls-1" d="M1414.36,263.7c7.49-7.59,16.21-13.59,26.23-17.92,10.02-4.41,20.65-6.62,32.06-6.62,10.28,0,19.78,1.77,28.58,5.29,8.71,3.53,17.08,8.74,25,15.53,4.7,3.79,10.02,5.73,15.94,5.73,7.06,0,12.81-2.38,17.43-7.15,4.62-4.77,6.88-10.77,6.88-17.92s-2.79-13.77-8.45-18.88c-24.05-21.71-52.53-32.57-85.38-32.57-36.16,0-67.08,13.06-92.7,39.27-25.62,26.12-38.33,57.72-38.33,94.78s12.81,68.57,38.33,94.78c25.62,26.12,56.46,39.27,92.7,39.27,32.76,0,61.25-10.85,85.38-32.57,5.14-5.29,7.76-11.38,7.76-18.44s-2.27-13.15-6.88-17.83c-4.62-4.68-10.45-7.06-17.42-7.06-5.92.18-11.07,1.94-15.42,5.29-7.84,6.88-16.03,12-24.83,15.44-8.71,3.44-18.21,5.12-28.58,5.12-11.41,0-22.04-2.21-32.06-6.62-10.02-4.41-18.73-10.41-26.23-17.91-7.49-7.5-13.42-16.5-17.69-26.65-4.27-10.24-6.45-21.18-6.45-32.83s2.18-22.59,6.45-32.83c4.27-10.24,10.19-19.06,17.69-26.65v-.09Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 92" width="272" height="92"><path fill="#EA4335" d="M115.75 47.18c0 12.77-9.99 22.18-22.25 22.18s-22.25-9.41-22.25-22.18C71.25 34.32 81.24 25 93.5 25s22.25 9.32 22.25 22.18zm-9.74 0c0-7.98-5.79-13.44-12.51-13.44S80.99 39.2 80.99 47.18c0 7.9 5.79 13.44 12.51 13.44s12.51-5.55 12.51-13.44z"/><path fill="#FBBC05" d="M163.75 47.18c0 12.77-9.99 22.18-22.25 22.18s-22.25-9.41-22.25-22.18c0-12.85 9.99-22.18 22.25-22.18s22.25 9.32 22.25 22.18zm-9.74 0c0-7.98-5.79-13.44-12.51-13.44s-12.51 5.46-12.51 13.44c0 7.9 5.79 13.44 12.51 13.44s12.51-5.55 12.51-13.44z"/><path fill="#4285F4" d="M209.75 26.34v39.82c0 16.38-9.66 23.07-21.08 23.07-10.75 0-17.22-7.19-19.66-13.07l8.48-3.53c1.51 3.61 5.21 7.87 11.17 7.87 7.31 0 11.84-4.51 11.84-13v-3.19h-.34c-2.18 2.69-6.38 5.04-11.68 5.04-11.09 0-21.25-9.66-21.25-22.09 0-12.52 10.16-22.26 21.25-22.26 5.29 0 9.49 2.35 11.68 4.96h.34v-3.61h9.25zm-8.56 20.92c0-7.81-5.21-13.52-11.84-13.52-6.72 0-12.35 5.71-12.35 13.52 0 7.73 5.63 13.36 12.35 13.36 6.63 0 11.84-5.63 11.84-13.36z"/><path fill="#34A853" d="M225 3v65h-9.5V3h9.5z"/><path fill="#EA4335" d="M262.02 54.48l7.56 5.04c-2.44 3.61-8.32 9.83-18.48 9.83-12.6 0-22.01-9.74-22.01-22.18 0-13.19 9.49-22.18 20.92-22.18 11.51 0 17.14 9.16 18.98 14.11l1.01 2.52-29.65 12.28c2.27 4.45 5.8 6.72 10.75 6.72 4.96 0 8.4-2.44 10.92-6.14zm-23.27-7.98l19.82-8.23c-1.09-2.77-4.37-4.7-8.23-4.7-4.95 0-11.84 4.37-11.59 12.93z"/><path fill="#4285F4" d="M35.29 41.41V32H67c.31 1.64.47 3.58.47 5.68 0 7.06-1.93 15.79-8.15 22.01-6.05 6.3-13.78 9.66-24.02 9.66C16.32 69.35.36 53.89.36 34.91.36 15.93 16.32.47 35.3.47c10.5 0 17.98 4.12 23.6 9.49l-6.64 6.64c-4.03-3.78-9.49-6.72-16.97-6.72-13.86 0-24.7 11.17-24.7 25.03 0 13.86 10.84 25.03 24.7 25.03 8.99 0 14.11-3.61 17.39-6.89 2.66-2.66 4.41-6.46 5.1-11.65l-22.49.01z"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-244
View File
@@ -1,244 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Livello_1"
x="0px"
y="0px"
width="1000px"
height="401.149px"
viewBox="0 0 1000 401.149"
enable-background="new 0 0 1000 401.149"
xml:space="preserve"
sodipodi:docname="IBM_rgb_logo.svg"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4" /><sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.261"
inkscape:cx="500"
inkscape:cy="201.14943"
inkscape:window-width="1366"
inkscape:window-height="682"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="g4" />
<g
id="g4">
<g
id="g1"
style="fill:#fd1813;fill-opacity:1">
<polygon
id="Rectangle-path"
fill="#1F70C1"
points="0,373.217 194.433,373.217 194.433,401.05 0,401.05 "
style="fill:#fd1813;fill-opacity:1" />
<polygon
id="Rectangle-path_1_"
fill="#1F70C1"
points="0,319.83 194.433,319.83 194.433,347.761 0,347.761 "
style="fill:#fd1813;fill-opacity:1" />
<polygon
id="Rectangle-path_2_"
fill="#1F70C1"
points="55.468,266.541 138.867,266.541 138.867,294.473 55.468,294.473 "
style="fill:#fd1813;fill-opacity:1" />
<polygon
id="Rectangle-path_3_"
fill="#1F70C1"
points="55.468,213.253 138.867,213.253 138.867,241.185 55.468,241.185 "
style="fill:#fd1813;fill-opacity:1" />
<polygon
id="Rectangle-path_4_"
fill="#1F70C1"
points="55.468,159.964 138.867,159.964 138.867,187.896 55.468,187.896 "
style="fill:#fd1813;fill-opacity:1" />
<polygon
id="Rectangle-path_5_"
fill="#1F70C1"
points="55.468,106.577 138.867,106.577 138.867,134.509 55.468,134.509 "
style="fill:#fd1813;fill-opacity:1" />
<rect
id="Rectangle-path_6_"
y="53.288"
fill="#1F70C1"
width="194.433"
height="27.932"
style="fill:#fd1813;fill-opacity:1" />
<rect
id="Rectangle-path_7_"
fill="#1F70C1"
width="194.433"
height="27.932"
style="fill:#fd1813;fill-opacity:1" />
</g>
<g
id="g2"
style="fill:#0fa14c;fill-opacity:1">
<path
id="Shape_16_"
fill="#1F70C1"
d="M222.167,347.761h299.029c5.051-8.617,8.815-18.027,11.094-27.932H222.167V347.761z"
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_17_"
fill="#1F70C1"
d="M497.92,213.253H277.734v27.932h243.463C514.857,230.487,507.032,221.078,497.92,213.253z"
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_18_"
fill="#1F70C1"
d="M277.734,159.964v27.932H497.92c9.311-7.825,17.135-17.235,23.277-27.932H277.734z"
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_19_"
fill="#1F70C1"
d="M521.197,53.288H222.167V81.22H532.29C529.715,71.315,525.951,61.906,521.197,53.288z"
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_20_"
fill="#1F70C1"
d="M429.279,0H222.167v27.932h278.526C482.072,10.697,456.815,0,429.279,0z"
style="fill:#0fa14c;fill-opacity:1" />
<rect
id="Rectangle-path_8_"
x="277.734"
y="106.577"
fill="#1F70C1"
width="83.3"
height="27.932"
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_21_"
fill="#1F70C1"
d="M444.433,134.509h87.163c2.476-8.914,3.764-18.324,3.764-27.932h-90.927L444.433,134.509 L444.433,134.509z"
style="fill:#0fa14c;fill-opacity:1" />
<polygon
id="Rectangle-path_9_"
fill="#1F70C1"
points="277.734,266.541 361.034,266.541 361.034,294.473 277.734,294.473 "
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_22_"
fill="#1F70C1"
d="M444.433,266.541v27.932h90.927c0-9.608-1.288-19.017-3.764-27.932H444.433z"
style="fill:#0fa14c;fill-opacity:1" />
<path
id="Shape_23_"
fill="#1F70C1"
d="M222.167,400.852l207.112,0.297c27.734,0,52.793-10.697,71.513-27.932H222.167V400.852z"
style="fill:#0fa14c;fill-opacity:1" />
</g>
<g
id="g3"
style="fill:#1f70c1;fill-opacity:1">
<polygon
id="Rectangle-path_10_"
fill="#1F70C1"
points="555.567,373.217 694.433,373.217 694.433,401.05 555.567,401.05 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_11_"
fill="#1F70C1"
points="555.567,319.83 694.433,319.83 694.433,347.761 555.567,347.761 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_12_"
fill="#1F70C1"
points="611.034,266.541 694.433,266.541 694.433,294.473 611.034,294.473 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_13_"
fill="#1F70C1"
points="611.034,213.253 694.433,213.253 694.433,241.185 611.034,241.185 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_24_"
fill="#1F70C1"
points="733.063,53.288 555.567,53.288 555.567,81.22 742.67,81.22 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_25_"
fill="#1F70C1"
points="714.639,0 555.567,0 555.567,27.932 724.247,27.932 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_14_"
fill="#1F70C1"
points="861.034,373.217 1000,373.217 1000,401.05 861.034,401.05 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_15_"
fill="#1F70C1"
points="861.034,319.83 1000,319.83 1000,347.761 861.034,347.761 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_16_"
fill="#1F70C1"
points="861.034,266.541 944.433,266.541 944.433,294.473 861.034,294.473 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Rectangle-path_17_"
fill="#1F70C1"
points="861.034,213.253 944.433,213.253 944.433,241.185 861.034,241.185 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_26_"
fill="#1F70C1"
points="861.034,187.896 944.433,187.896 944.433,159.964 861.034,159.964 861.034,159.964 785.559,159.964 777.734,182.548 769.909,159.964 694.433,159.964 694.433,159.964 611.034,159.964 611.034,187.896 694.433,187.896 694.433,162.242 703.249,187.896 852.219,187.896 861.034,162.242 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_27_"
fill="#1F70C1"
points="944.433,106.577 803.982,106.577 794.374,134.509 944.433,134.509 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_28_"
fill="#1F70C1"
points="840.927,0 831.319,27.932 1000,27.932 1000,0 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_29_"
fill="#1F70C1"
points="777.734,400.852 787.341,373.217 768.126,373.217 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_30_"
fill="#1F70C1"
points="759.311,347.761 796.157,347.761 806.062,319.83 749.505,319.83 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_31_"
fill="#1F70C1"
points="740.59,294.473 814.877,294.473 824.683,266.541 730.784,266.541 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_32_"
fill="#1F70C1"
points="721.969,241.185 833.597,241.185 843.106,213.253 712.361,213.253 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_33_"
fill="#1F70C1"
points="611.034,134.509 761.093,134.509 751.486,106.577 611.034,106.577 "
style="fill:#1f70c1;fill-opacity:1" />
<polygon
id="Shape_34_"
fill="#1F70C1"
points="812.896,81.22 1000,81.22 1000,53.288 822.405,53.288 "
style="fill:#1f70c1;fill-opacity:1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.6 KiB

@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="298" height="64" fill="none" viewBox="0 0 298 64">
<defs>
<linearGradient id="a" x1=".850001" x2="62.62" y1="62.72" y2="1.81" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9419"/>
<stop offset=".43" stop-color="#FF021D"/>
<stop offset=".99" stop-color="#E600FF"/>
</linearGradient>
</defs>
<path fill="#000" d="M86.4844 40.5858c0 .8464-.1792 1.5933-.5377 2.2505-.3585.6573-.8564 1.1651-1.5137 1.5236-.6572.3585-1.3941.5378-2.2406.5378H78v6.1044h5.0787c1.912 0 3.6248-.4282 5.1484-1.2846 1.5236-.8564 2.7186-2.0415 3.585-3.5452.8663-1.5037 1.3045-3.1966 1.3045-5.0886V21.0178h-6.6322v19.568Zm17.8556-1.8224h13.891v-5.6065H104.34v-6.3633h15.355v-5.7758H97.8766v29.9743h22.2464v-5.7757H104.34v-6.453Zm17.865-11.8005h8.882v24.0193h6.633V26.9629h8.842v-5.9451h-24.367v5.9551l.01-.01Zm47.022 9.0022c-.517-.2788-1.085-.4879-1.673-.6472.449-.1295.877-.2888 1.275-.488 1.096-.5676 1.962-1.3643 2.579-2.39.618-1.0257.936-2.2007.936-3.5351 0-1.5237-.418-2.8879-1.244-4.0929-.827-1.195-1.992-2.131-3.486-2.8082-1.494-.6672-3.206-1.0058-5.118-1.0058h-13.315v29.9743h13.574c2.011 0 3.804-.3485 5.387-1.0556 1.573-.707 2.798-1.6829 3.675-2.9476.866-1.2547 1.304-2.6887 1.304-4.302 0-1.4837-.338-2.8082-1.026-3.9833-.687-1.175-1.633-2.0812-2.858-2.7285l-.01.0099Zm-13.603-9.9184h5.886c.816 0 1.533.1494 2.161.4382.627.2888 1.115.707 1.464 1.2547.348.5378.527 1.1751.527 1.9021 0 .7269-.179 1.414-.527 1.9817-.349.5676-.837.9958-1.464 1.3045-.628.3087-1.345.4581-2.161.4581h-5.886v-7.3492.0099Zm10.138 18.134c-.378.5676-.916 1.0058-1.603 1.3145-.697.3087-1.484.4581-2.39.4581h-6.145v-7.6878h6.145c.886 0 1.673.1693 2.37.4979.687.3286 1.235.7867 1.613 1.3842.378.5975.578 1.2747.578 2.0414 0 .7668-.19 1.4241-.568 1.9917Zm29.596-5.3077c1.663-.7967 2.947-1.922 3.864-3.3659.916-1.444 1.374-3.117 1.374-5.0289 0-1.912-.448-3.5253-1.344-4.9592-.897-1.434-2.171-2.5394-3.814-3.3261-1.644-.7867-3.546-1.1751-5.717-1.1751h-13.124v29.9743h6.642V40.0779h4.322l6.084 10.9142h7.578l-6.851-11.7208c.339-.1195.677-.249.996-.3983h-.01Zm-2.151-6.1244c-.369.6274-.896 1.1154-1.583 1.444-.688.3386-1.494.5079-2.42.5079h-5.975v-8.2953h5.975c.926 0 1.732.1693 2.42.4979.687.3287 1.214.8166 1.583 1.434.368.6174.558 1.3544.558 2.1908 0 .8365-.19 1.5734-.558 2.2008v.0199Zm20.594-11.7308-10.706 29.9743h6.742l2.121-6.6122h11.114l2.27 6.6122h6.612L220.99 21.0178h-7.189Zm-.339 18.3431 3.445-10.5756.409-1.922.408 1.922 3.685 10.5756h-7.947Zm20.693 11.6312h6.851V21.0178h-6.851v29.9743Zm31.02-9.6993-12.896-20.275h-6.463v29.9743h6.055V30.7172l12.826 20.2749h6.533V21.0178h-6.055v20.275Zm31.528-3.3559c-.647-1.2448-1.564-2.2904-2.729-3.1369-1.165-.8464-2.509-1.4041-4.023-1.6929l-5.098-1.0456c-.797-.1892-1.434-.5178-1.902-.9958-.469-.478-.708-1.0755-.708-1.7825 0-.6473.17-1.205.518-1.683.339-.478.827-.8464 1.444-1.1153.618-.2689 1.335-.3983 2.151-.3983.817 0 1.554.1394 2.181.4182.627.2788 1.115.6672 1.464 1.1751s.528 1.0755.528 1.7228h6.642c-.04-1.7427-.528-3.2863-1.444-4.6207-.916-1.3443-2.201-2.3899-3.834-3.1468-1.633-.7568-3.505-1.1352-5.597-1.1352-2.091 0-3.943.3884-5.566 1.1751-1.623.7867-2.898 1.8721-3.804 3.2663-.906 1.3941-1.364 2.9775-1.364 4.76 0 1.444.288 2.7485.876 3.9036.587 1.1652 1.414 2.1311 2.479 2.8979 1.076.7668 2.311 1.3045 3.725 1.6033l5.397 1.1153c.886.2091 1.584.5975 2.101 1.1551.518.5577.767 1.2448.767 2.0813 0 .6672-.189 1.2747-.567 1.8025-.379.5277-.907.936-1.584 1.2248-.677.2888-1.474.4282-2.39.4282-.916 0-1.782-.1593-2.529-.478-.747-.3186-1.325-.7767-1.733-1.3742-.418-.5875-.617-1.2747-.617-2.0414h-6.642c.029 1.8721.527 3.5152 1.513 4.9492.976 1.424 2.32 2.5394 4.033 3.336 1.713.7967 3.675 1.195 5.886 1.195 2.21 0 4.202-.4083 5.915-1.2249 1.723-.8165 3.057-1.9418 4.023-3.3758.966-1.434 1.444-3.0572 1.444-4.8696 0-1.4838-.329-2.848-.976-4.1028l.02.01Z"/>
<path fill="url(#a)" d="M20.34 3.66 3.66 20.34C1.32 22.68 0 25.86 0 29.18V59c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84V5c0-2.76-2.24-5-5-5H29.18c-3.32 0-6.49 1.32-8.84 3.66Z"/>
<path fill="#000" d="M48 16H8v40h40V16Z"/>
<path fill="#fff" d="M30 47H13v4h17v-4Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.0 MiB

-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="656" height="120" viewBox="0 0 164 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<path d="M160.352,24.069L160.352,23.62L160.64,23.62C160.797,23.62 161.011,23.632 161.011,23.824C161.011,24.032 160.901,24.069 160.715,24.069L160.352,24.069M160.352,24.384L160.544,24.384L160.991,25.168L161.481,25.168L160.987,24.352C161.242,24.333 161.452,24.212 161.452,23.868C161.452,23.441 161.157,23.303 160.659,23.303L159.938,23.303L159.938,25.168L160.352,25.168L160.352,24.384M162.45,24.238C162.45,23.143 161.599,22.508 160.65,22.508C159.695,22.508 158.845,23.143 158.845,24.238C158.845,25.333 159.695,25.971 160.65,25.971C161.598,25.971 162.45,25.333 162.45,24.238M161.93,24.238C161.93,25.036 161.343,25.572 160.65,25.572L160.65,25.566C159.937,25.572 159.361,25.036 159.361,24.238C159.361,23.441 159.938,22.907 160.65,22.907C161.344,22.907 161.93,23.441 161.93,24.238" style="fill:black;"/>
<path d="M96.374,5.707L96.376,25.367L101.928,25.367L101.928,5.707L96.374,5.707ZM52.697,5.681L52.697,25.367L58.3,25.367L58.3,10.086L62.67,10.1C64.107,10.1 65.1,10.445 65.793,11.184C66.672,12.12 67.03,13.628 67.03,16.389L67.03,25.367L72.457,25.367L72.457,14.49C72.457,6.727 67.509,5.68 62.668,5.68L52.698,5.68L52.697,5.681ZM105.314,5.708L105.314,25.367L114.32,25.367C119.118,25.367 120.684,24.569 122.377,22.78C123.575,21.524 124.348,18.766 124.348,15.753C124.348,12.99 123.693,10.525 122.551,8.99C120.494,6.245 117.531,5.708 113.106,5.708L105.314,5.708ZM110.822,9.988L113.209,9.988C116.672,9.988 118.912,11.544 118.912,15.579C118.912,19.616 116.672,21.171 113.209,21.171L110.822,21.171L110.822,9.988ZM88.369,5.708L83.735,21.288L79.295,5.709L73.302,5.708L79.642,25.367L87.645,25.367L94.036,5.708L88.369,5.708ZM126.932,25.367L132.485,25.367L132.485,5.709L126.93,5.708L126.932,25.367ZM142.496,5.715L134.743,25.36L140.218,25.36L141.445,21.888L150.62,21.888L151.781,25.36L157.725,25.36L149.913,5.714L142.496,5.715ZM146.1,9.3L149.464,18.504L142.631,18.504L146.101,9.3L146.1,9.3Z" style="fill:black;"/>
<path d="M16.889,8.985L16.889,6.28C17.151,6.26 17.417,6.247 17.687,6.238C25.087,6.006 29.942,12.597 29.942,12.597C29.942,12.597 24.698,19.879 19.076,19.879C18.333,19.882 17.594,19.764 16.889,19.529L16.889,11.325C19.769,11.673 20.349,12.945 22.081,15.833L25.933,12.585C25.933,12.585 23.121,8.897 18.381,8.897C17.866,8.897 17.373,8.933 16.889,8.985ZM16.889,0.047L16.889,4.09C17.154,4.069 17.42,4.052 17.687,4.042C27.977,3.696 34.682,12.482 34.682,12.482C34.682,12.482 26.982,21.846 18.959,21.846C18.224,21.846 17.535,21.778 16.889,21.663L16.889,24.161C17.442,24.231 18.015,24.273 18.613,24.273C26.078,24.273 31.477,20.461 36.705,15.948C37.572,16.642 41.121,18.331 41.85,19.071C36.879,23.231 25.295,26.586 18.727,26.586C18.113,26.584 17.5,26.552 16.889,26.49L16.889,30L45.264,30L45.264,0.047L16.889,0.047ZM16.889,19.529L16.889,21.662C9.984,20.432 8.067,13.254 8.067,13.254C8.067,13.254 11.383,9.58 16.889,8.985L16.889,11.325L16.878,11.324C13.988,10.977 11.731,13.677 11.731,13.677C11.731,13.677 12.996,18.221 16.889,19.529ZM4.625,12.943C4.625,12.943 8.717,6.903 16.889,6.28L16.889,4.088C7.838,4.815 0,12.48 0,12.48C0,12.48 4.439,25.313 16.889,26.488L16.889,24.16C7.753,23.011 4.625,12.943 4.625,12.943Z" style="fill:rgb(118,185,0);"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.9 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222 66" width="400" height="119" aria-hidden="true"><path d="M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z" transform="translate(11, 11)" fill="#181225"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.9 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" viewBox="0 0 284 65"><path d="M141.68 16.25c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zm117.14-14.5c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zm-39.03 3.5c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9v-46h9zM37.59.25l36.95 64H.64l36.95-64zm92.38 5l-27.71 48-27.71-48h10.39l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10v14.8h-9v-34h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" /></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg width="2367" height="632" viewBox="0 0 2367 632" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1154.02 497.979H1223.16L1301.49 287.982H1302.36L1380.25 497.979H1450.27L1527.29 193.004H1459.02L1406.95 403.872H1406.07L1331.68 193.004H1272.61L1198.21 403.872H1197.34L1145.7 193.004H1077L1154.02 497.979Z" fill="#0E0E0F"/>
<path d="M1546.9 497.979H1611.67V374.682C1611.67 341.135 1637.05 318.044 1674.68 318.044C1712.31 318.044 1735.07 339.392 1735.07 374.682V497.979H1799.83V365.968C1799.83 303.667 1755.2 260.534 1688.68 260.534C1658.05 260.534 1631.8 270.555 1612.54 288.853H1611.67V186H1546.9V497.979Z" fill="#0E0E0F"/>
<path d="M1953.81 504.514C2030.39 504.514 2084.65 451.361 2084.65 382.524C2084.65 313.687 2030.39 260.534 1953.81 260.534C1877.67 260.534 1822.97 313.687 1822.97 382.524C1822.97 451.361 1877.67 504.514 1953.81 504.514ZM1953.81 447.876C1913.55 447.876 1887.73 419.992 1887.73 382.524C1887.73 345.056 1913.55 317.173 1953.81 317.173C1994.07 317.173 2019.89 345.056 2019.89 382.524C2019.89 419.992 1994.07 447.876 1953.81 447.876Z" fill="#0E0E0F"/>
<path d="M2108.82 584H2173.58V480.116H2174.46C2189.34 494.493 2218.22 504.514 2247.1 504.514C2315.8 504.514 2367 452.233 2367 382.524C2367 313.251 2315.36 260.534 2246.66 260.534C2216.9 260.534 2189.77 269.248 2171.83 287.546H2170.96V267.07H2108.82V584ZM2237.03 447.876C2201.15 447.876 2173.58 424.785 2173.58 396.902V367.711C2173.58 340.699 2202.46 317.608 2236.6 317.608C2275.54 317.608 2302.24 343.749 2302.24 382.524C2302.24 421.299 2275.1 447.876 2237.03 447.876Z" fill="#0E0E0F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.62 160.782L138.31 299.092L0.266129 160.515C67.5354 93.2462 188.277 72.4398 276.62 160.782Z" fill="#FF6243"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.282 465.064L594.733 174.613C523.829 103.709 415.402 77.323 311.168 181.558L165.971 326.754L304.282 465.064Z" fill="#FF6243"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M622.127 479.16L926.675 174.613C862.514 110.452 742.643 82.0246 636.489 188.179L331.941 492.726C401.096 561.881 521.142 580.146 622.127 479.16Z" fill="#FF6243"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -1,6 +1,5 @@
<svg role="img" aria-label="Herdr logo" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<rect width="512" height="512" fill="#d9dad8"/>
<g fill="#303438" transform="translate(0 512) scale(.1 -.1)" stroke="none">
<svg role="img" aria-label="Herdr" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<g fill="currentColor" transform="translate(0 512) scale(.1 -.1)" stroke="none">
<path d="M2794 3710 c-129 -33 -299 -135 -359 -214 -21 -28 -26 -42 -21 -63 9 -38 154 -178 199 -192 32 -11 41 -9 104 23 171 86 354 70 475 -43 150 -138 150 -379 0 -511 -107 -95 -278 -94 -386 2 l-46 40 -11 -29 c-16 -40 -14 -122 4 -164 60 -144 264 -222 452 -174 360 92 559 494 430 868 -36 103 -81 173 -175 267 -71 72 -100 93 -180 132 -52 26 -127 54 -167 62 -96 21 -230 20 -319 -4z M2183 3695 c-116 -32 -221 -108 -273 -199 -17 -28 -30 -54 -30 -58 0 -4 20 1 45 12 66 28 220 68 294 76 64 7 65 7 137 83 40 41 71 77 69 79 -2 2 -21 8 -42 13 -55 12 -140 10 -200 -6z M2212 3388 c-159 -22 -390 -122 -559 -241 -299 -210 -585 -600 -609 -828 -12 -118 40 -251 125 -318 96 -76 178 -98 426 -116 110 -8 224 -21 254 -29 125 -34 230 -115 272 -211 11 -24 24 -81 30 -127 20 -170 65 -271 166 -374 34 -35 63 -65 63 -67 0 -1 -10 -27 -22 -57 -29 -76 -37 -259 -14 -350 42 -170 158 -318 311 -397 44 -23 98 -46 120 -52 22 -6 45 -14 51 -18 5 -5 15 -48 22 -96 6 -48 14 -92 17 -97 4 -6 415 -10 1131 -10 l1124 0 0 1584 0 1585 -55 -19 c-84 -29 -143 -68 -232 -154 l-83 -78 -54 49 c-111 102 -233 151 -391 160 -113 6 -199 -10 -298 -54 l-60 -27 -26 34 c-37 51 -120 134 -127 128 -3 -4 0 -34 7 -67 17 -89 7 -268 -21 -356 -103 -328 -377 -545 -688 -545 -161 0 -273 41 -373 137 -37 35 -66 75 -85 116 -79 173 -8 407 124 407 44 0 68 -14 117 -66 74 -78 167 -82 238 -9 60 62 72 147 33 231 -42 90 -120 130 -238 122 -50 -4 -85 -14 -131 -37 -89 -45 -122 -52 -176 -40 -92 20 -262 163 -302 253 -11 25 -21 45 -22 45 -1 -1 -30 -6 -65 -11z m-256 -505 c115 -88 129 -102 132 -131 2 -18 -2 -40 -9 -49 -7 -8 -69 -55 -138 -105 -103 -73 -130 -88 -151 -83 -35 8 -51 34 -48 74 3 31 12 42 83 92 44 31 80 61 82 66 1 4 -30 31 -69 58 -39 28 -77 56 -85 63 -18 19 -16 72 4 94 32 35 63 23 199 -79z m528 -88 c23 -24 28 -52 14 -82 l-13 -28 -141 -3 c-130 -2 -142 -1 -158 17 -23 26 -24 66 -1 91 16 18 32 20 151 20 105 0 136 -3 148 -15z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

+22 -4
View File
@@ -54,6 +54,10 @@ export default defineConfig({
redirects: {
'/ja': '/ja/docs/',
'/zh-cn': '/zh-cn/docs/',
/* /stats/ was public and is linked from elsewhere; the numbers it carried
now live in the hero strip and the nav count. One key only — Astro
normalizes the trailing slash and treats both spellings as one route. */
'/stats': '/',
},
integrations: [
sitemap({
@@ -90,6 +94,20 @@ export default defineConfig({
},
customCss: ['./src/styles/starlight.css'],
head: [
// the brand display face; Starlight owns its own <head>, so the
// marketing pages' font link doesn't reach docs
{ tag: 'link', attrs: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } },
{
tag: 'link',
attrs: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: true },
},
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&family=Inter:wght@400;500;600&display=swap',
},
},
{
// First-visit locale redirect: honors browser language order, then
// remembers the last locale the reader actually used.
@@ -122,7 +140,7 @@ export default defineConfig({
},
{
tag: 'meta',
attrs: { property: 'og:image', content: 'https://herdr.dev/assets/og-card-v8.png' },
attrs: { property: 'og:image', content: 'https://herdr.dev/assets/og-card-v9.png' },
},
{ tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } },
{ tag: 'meta', attrs: { property: 'og:image:height', content: '630' } },
@@ -130,18 +148,18 @@ export default defineConfig({
tag: 'meta',
attrs: {
property: 'og:image:alt',
content: 'Herdr documentation — One terminal. The whole herd.',
content: 'Herdr documentation — run them anywhere, leave them running.',
},
},
{
tag: 'meta',
attrs: { name: 'twitter:image', content: 'https://herdr.dev/assets/og-card-v8.png' },
attrs: { name: 'twitter:image', content: 'https://herdr.dev/assets/og-card-v9.png' },
},
{
tag: 'meta',
attrs: {
name: 'twitter:image:alt',
content: 'Herdr documentation — One terminal. The whole herd.',
content: 'Herdr documentation — run them anywhere, leave them running.',
},
},
],
+102
View File
@@ -0,0 +1,102 @@
/* ──────────────────────────────────────────────────────────────
Shared chassis for the OG cards.
Cards are authored as real 1200x630 HTML so they use the same fonts
and palette as the site and can be regenerated with a screenshot
instead of a design session. Tokens mirror site.css's ink ground —
duplicated deliberately, because a card is a frozen artifact and
must not change shape when the site's CSS does.
────────────────────────────────────────────────────────────── */
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400 700;
font-display: block;
src: url("/assets/jbm.woff2?v=2") format("woff2");
}
:root {
--bg: #0c0c12;
--panel: #111119;
--mass: #1a1a24;
--ink: #e9e6f2;
--dim: #a29db8;
--faint: #736e8a;
--line: #1a1a24;
--line2: #2a2839;
--spot: #cba6f7;
--run: #5fae74;
--wait: #d3a027;
--idle: #5a615c;
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--disp: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
/* On a card page the document IS the card, so a full-page capture is the
finished asset at exactly 1200x630 whatever the window size. Scoped to
.og so the comparison viewer, which loads this same file for the tokens,
isn't clamped to card dimensions. */
html.og, body.og {
margin: 0; padding: 0; background: var(--bg);
width: 1200px; height: 630px; overflow: hidden;
}
.card {
position: relative;
width: 1200px;
height: 630px;
overflow: hidden;
background: var(--bg);
color: var(--ink);
font-family: var(--mono);
-webkit-font-smoothing: antialiased;
background-image:
linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px);
background-size: 60px 60px;
}
/* the ram as mass, bleeding off the right edge */
.card .ram {
background: currentColor;
-webkit-mask: url("/assets/ram.svg") center/contain no-repeat;
mask: url("/assets/ram.svg") center/contain no-repeat;
display: block;
}
.card .mass {
position: absolute;
right: -110px;
top: 50%;
transform: translateY(-50%);
width: 620px;
height: 620px;
color: var(--mass);
z-index: 0;
}
.card .in { position: relative; z-index: 1; height: 100%; }
/* lockup */
.lockup { display: flex; align-items: center; gap: 14px; }
.lockup .ram { width: 38px; height: 38px; color: var(--ink); }
.lockup b {
font-family: var(--disp);
font-weight: 900;
font-size: 34px;
letter-spacing: -0.045em;
}
.eyebrow {
display: flex;
align-items: center;
gap: 18px;
color: var(--spot);
font-size: 19px;
letter-spacing: 0.3em;
text-transform: uppercase;
}
.eyebrow s { width: 56px; height: 2px; background: var(--spot); text-decoration: none; }
/* keep the safe area generous: some surfaces crop toward square */
.pad { padding: 54px 62px; }
+992
View File
@@ -0,0 +1,992 @@
/* ──────────────────────────────────────────────────────────────
site.css — herdr.dev chassis. Loaded after style.css, which is
kept only for the session mock (.herdr-ui-mock) and its tokens.
Ink is the default ground and the presentation layer; paper is an opt-in
preference shared with the docs. html[data-palette] themes the mock only
and follows the ground, so page and plate always share one mauve.
────────────────────────────────────────────────────────────── */
/* Ink is the presentation layer: terminals are dark and the audience has
internalised that, so it is the default and never follows the system.
Paper is a preference the reader opts into, shared with the docs through
Starlight's own `starlight-theme` key so one choice covers both surfaces.
The mock follows the ground, and the mauve shifts with it — one mauve in
view at any time, never two. */
html,
html[data-mode="ink"] {
/* The ground is near-neutral by test, not by taste. A blue-violet dark at
20% saturation measurably matched the old site's hue, but read as harder
— saturated blue at low luminance softens the edges of light text, and
the eye has least acuity there. Draining it to 6% won a side-by-side.
Colour now lives in the mauve accent, which pops harder against neutral.
Contrast: ink 14.7, dim 9.3, faint 5.3 — all above AA. */
--bg: #17171a; --panel: #1e1e22; --mass: #26262b;
--ink: #eae8ee; --dim: #bdb9c9; --faint: #8e8a98;
--line: #26262b; --line2: #35353d;
--spot: #cba6f7; --spot-ink: #17171a;
--grid: #202024;
}
html[data-mode="paper"] {
--st-done: #2aa198;
--bg: #efece5; --panel: #e7e3da; --mass: #ddd8cc;
--ink: #15140f; --dim: #55534a; --faint: #86826f;
--line: #e2ded4; --line2: #cbc5b6;
--spot: #8839ef; --spot-ink: #fff;
--grid: #e4e0d6;
}
:root {
--run: #5fae74; --wait: #d3a027; --idle: #5a615c; --done: #6f6a86;
/* The TUI's own mapping, not a palette invented for the website:
src/ui/status.rs state_dot() paints blocked with p.red, working with
p.yellow and idle with p.green once seen, and the default palette in
src/app/state.rs is Catppuccin Mocha — the same #f38ba8 / #f9e2af /
#a6e3a1 that style.css already defines for the plate. Aliasing rather
than copying keeps the legend, the plate and the app in step, and lets
all three follow the ground when it changes. */
--st-blocked: var(--red);
--st-working: var(--yellow);
--st-idle: var(--green);
/* done is p.teal, and style.css has no teal token to alias, so it is set
per ground: Catppuccin Mocha teal on ink, Solarized cyan on paper */
--st-done: #94e2d5;
--disp: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
/* Sentences are not code. Mono removes the word-shape cues you read by, so
it costs speed in prose and gets worse below ~14px. Inter is already what
the docs declare, so using it here makes the two surfaces agree. */
--body: "Inter", system-ui, -apple-system, "Segoe UI", Helvetica, sans-serif;
--gut: 34px;
}
/* The plate's chrome takes the page's spot, so there is exactly one mauve
in view. Agent identity colours inside the panes (claude coral, opencode
purple, codex) stay native. */
/* The plate's agent states follow the TUI, not the page accent. `done` reads
var(--accent) in style.css, and aliasing accent to the page mauve painted a
done agent purple — a colour Herdr's sidebar never shows. src/ui/status.rs
state_dot() is the authority: blocked red, working filled yellow, done
filled teal, idle hollow green, unknown grey. */
body.site .mock-agent.done > span,
body.site .mock-agent.done small { color: var(--st-done); }
html[data-palette] {
--accent: var(--spot);
--term-tab-active-bg: var(--spot);
--term-tab-active-text: var(--spot-ink);
--term-prompt: var(--spot);
--term-blue: var(--spot);
}
/* The ported plugins page still reads style.css tokens directly, and style.css
only knows the 20 mock palettes — so catppuccin's violet #181825 was reaching
the publish card, which is why that block stayed tinted after the ground went
neutral. Bridge the legacy names onto the neutral scale at the page wrapper.
Scoped to this page, not to body.site, because the mock reads --radius-* for
its tabs and panes and must keep its own corners. Terminal tokens are left
alone on purpose: the plate stays catppuccin. */
body.site .plugins-page {
--bg-elevated: var(--panel);
--ink-soft: var(--dim);
--muted: var(--faint);
--muted-2: var(--faint);
--line-strong: var(--line2);
--accent-soft: color-mix(in srgb, var(--spot) 8%, transparent);
/* the language is hairline cells in a sheet; nothing here is a rounded box */
--radius-sm: 0; --radius-md: 0; --radius-lg: 0;
}
body.site {
margin: 0; background: var(--bg); color: var(--ink);
font-family: var(--body); font-size: 14.5px; line-height: 1.65;
-webkit-font-smoothing: antialiased;
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 72px 72px;
overflow-x: clip;
/* exactly one grid module of ground below the sheet */
padding-bottom: 72px;
}
body.site ::selection { background: var(--spot); color: var(--spot-ink); }
body.site a { color: inherit; }
body.site .ram {
background: currentColor; display: block;
-webkit-mask: url("/assets/ram.svg") center/contain no-repeat;
mask: url("/assets/ram.svg") center/contain no-repeat;
}
body.site .hd-frame {
max-width: 1440px; margin: 0 auto; background: var(--bg); position: relative;
border-left: 1px solid var(--line2); border-right: 1px solid var(--line2);
/* close the sheet: without this the two vertical rules stopped in mid-air on
the last pixel of the document, which read as a crop rather than an end */
border-bottom: 1px solid var(--line2);
}
/* ── nav ── */
body.site .hd-nav {
display: flex; align-items: center; gap: 24px;
height: 60px; padding: 0 var(--gut); border-bottom: 1px solid var(--line2);
}
body.site .lock { display: flex; align-items: center; gap: 10px; text-decoration: none; }
body.site .lock .ram { width: 25px; height: 25px; color: var(--ink); }
body.site .lock b { font-family: var(--disp); font-weight: 900; font-size: 20px; letter-spacing: -.045em; color: var(--ink); }
body.site .hd-nav .sp { flex: 1; }
body.site .hd-nav a.lnk { color: var(--dim); text-decoration: none; font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase; }
body.site .hd-nav a.lnk:hover { color: var(--ink); }
body.site .hd-nav .btn { border: 1px solid var(--line2); padding: 7px 15px; color: var(--ink); text-decoration: none; font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase; }
body.site .hd-nav .btn:hover { background: var(--spot); color: var(--spot-ink); border-color: var(--spot); }
/* Square hairline cells, the same box as the mode toggle and the Install
button — so the nav reads as one row of cells rather than text plus chrome.
The GitHub one widens to carry its count; the count is real, baked at build
and refreshed live, so it is proof rather than decoration. */
body.site .hd-nav .ico {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; flex: 0 0 auto;
border: 1px solid var(--line2); color: var(--dim); text-decoration: none;
}
body.site .hd-nav .ico.wide { width: auto; gap: 8px; padding: 0 10px; }
body.site .hd-nav .ico svg { width: 14px; height: 14px; fill: currentColor; }
body.site .hd-nav .ico b {
font-family: var(--mono); font-weight: 400; font-size: 11.5px;
letter-spacing: .04em; font-variant-numeric: tabular-nums;
}
/* an empty count must not leave a lopsided cell */
body.site .hd-nav .ico.wide:has(b:empty) { width: 30px; gap: 0; padding: 0; }
body.site .hd-nav .ico:hover { color: var(--spot); border-color: var(--spot); }
/* the menu cell is the same box as the icons and Install, so the nav stays one
row of cells; it only exists below the nav's own breakpoint */
body.site .hd-nav .menu {
display: none; align-items: center; justify-content: center; gap: 4px;
flex-direction: column; width: 30px; height: 30px; flex: 0 0 auto; padding: 0;
border: 1px solid var(--line2); background: none; color: var(--dim); cursor: pointer;
}
body.site .hd-nav .menu span { display: block; width: 13px; height: 1px; background: currentColor; }
body.site .hd-nav .menu[aria-expanded="true"] { color: var(--spot); border-color: var(--spot); }
/* The sheet is a continuation of the nav rather than a modal: same hairline
rows, same cell language, dropped under the bar it belongs to. */
body.site .sheet { display: none; border-bottom: 1px solid var(--line2); }
body.site .sheet a {
display: block; padding: 15px var(--gut); text-decoration: none; color: var(--ink);
font-size: 12px; letter-spacing: .07em; text-transform: uppercase;
border-bottom: 1px solid var(--line);
}
body.site .sheet a:last-of-type { color: var(--dim); }
body.site .sheet a:active { background: color-mix(in srgb, var(--spot) 8%, transparent); }
body.site .sheet-ground {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
padding: 13px var(--gut);
}
body.site .sheet-ground > span {
color: var(--faint); font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase;
}
body.site .mode { display: flex; border: 1px solid var(--line2); }
body.site .mode button {
background: none; border: 0; color: var(--faint); cursor: pointer;
font-family: var(--mono); font-size: 10px; letter-spacing: .16em;
text-transform: uppercase; padding: 7px 10px;
}
body.site .mode button:hover { color: var(--ink); }
body.site .mode button[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
/* ── hero ──
Single column. The ram is mass bleeding off the right edge, not an
object sitting in a second column with nothing to do. */
body.site .hd-hero { position: relative; overflow: hidden; padding: 68px var(--gut) 56px; }
body.site .ghost {
position: absolute; right: -14%; top: 50%; transform: translateY(-50%);
width: min(880px, 62vw); height: min(880px, 62vw);
color: var(--mass); z-index: 0; pointer-events: none;
}
body.site .hd-hero .in { position: relative; z-index: 1; }
body.site .hd-eyebrow { display: flex; align-items: center; gap: 14px; color: var(--spot); font-size: 11px; letter-spacing: .3em; text-transform: uppercase; }
body.site .hd-eyebrow s { width: 42px; height: 1px; background: var(--spot); text-decoration: none; }
body.site h1 {
font-family: var(--disp); font-weight: 900; letter-spacing: -.055em; line-height: .88;
/* wide enough that each sentence takes one line and the type reaches
across the frame — a narrow measure is what left the right side empty */
font-size: clamp(38px, 6.6vw, 132px); margin: 24px 0 0; max-width: 20ch; color: var(--ink);
}
body.site h1 em { font-style: normal; color: var(--spot); }
body.site .lede { color: var(--dim); font-size: 15px; line-height: 1.75; max-width: 50ch; margin: 32px 0 0; }
body.site .lede b { color: var(--ink); font-weight: 400; }
/* one command, one link — every other install path lives on /install/ */
body.site .go { display: flex; align-items: center; gap: 22px; margin-top: 38px; flex-wrap: wrap; }
body.site .hd-cmd {
display: inline-flex; align-items: center; border: 1px solid var(--line2);
background: color-mix(in srgb, var(--bg) 80%, transparent); backdrop-filter: blur(6px);
}
body.site .hd-cmd code { padding: 14px 19px; font-size: 13.5px; font-family: var(--mono); }
body.site .hd-cmd code b { color: var(--spot); font-weight: 400; margin-right: 11px; }
body.site .hd-cmd button {
padding: 14px 16px; border: 0; border-left: 1px solid var(--line2); background: none; cursor: pointer;
color: var(--faint); font-family: var(--mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
}
body.site .hd-cmd button:hover { color: var(--ink); }
body.site .meta { margin-top: 20px; color: var(--faint); font-size: 11.5px; letter-spacing: .04em; }
body.site .meta a { color: var(--spot); text-decoration: none; }
body.site .meta a:hover { text-decoration: underline; }
/* ── numbers ──
Every value is real: baked at build from the R2 snapshots, refreshed live
and counted up if it moved since the last deploy. */
body.site .strip { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line2); border-bottom: 1px solid var(--line2); }
body.site .stat {
position: relative; display: block; overflow: hidden; text-decoration: none;
padding: 24px var(--gut) 26px; border-right: 1px solid var(--line);
}
body.site .stat:last-child { border-right: 0; }
body.site .stat:hover { background: color-mix(in srgb, var(--spot) 5%, transparent); }
body.site .stat b {
position: relative; z-index: 1; display: block;
font-family: var(--disp); font-weight: 900; font-size: clamp(26px, 3vw, 40px);
letter-spacing: -.045em; line-height: 1; color: var(--ink);
font-variant-numeric: tabular-nums;
}
body.site .stat span {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 9px; margin-top: 11px;
color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
}
body.site .stat:hover span { color: var(--dim); }
body.site .stat .ic {
width: 14px; height: 14px; flex: 0 0 14px;
fill: currentColor; stroke: currentColor; color: var(--spot); opacity: .85;
}
body.site .stat .ic [fill="none"] { fill: none; }
body.site .stat i.live {
width: 6px; height: 6px; flex: 0 0 6px; background: var(--run);
animation: site-pulse 2.2s ease-in-out infinite;
}
@keyframes site-pulse { 0%,100% { opacity: .25 } 50% { opacity: 1 } }
@media (prefers-reduced-motion: reduce) {
body.site .stat i.live { animation: none; }
}
/* ── FIG 1 · the plate ──
Inset with paper around it and a hairline border, so it reads as a plate
resting on the sheet rather than a hole punched through it. */
body.site .fig { padding: 40px var(--gut) 46px; border-bottom: 1px solid var(--line2); }
body.site .fighd {
display: flex; align-items: baseline; gap: 16px; padding: 0 0 11px;
color: var(--faint); font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase;
}
body.site .fighd i { font-style: normal; color: var(--spot); letter-spacing: .1em; }
body.site .fighd .r { margin-left: auto; text-align: right; }
body.site .figcap {
margin: 13px 0 0; color: var(--faint); font-size: 11.5px; line-height: 1.7; max-width: 74ch;
}
body.site .figcap b { color: var(--dim); font-weight: 400; }
body.site .plate {
border: 1px solid var(--line2); overflow: hidden;
aspect-ratio: 1438 / 620; background: var(--term-bg);
}
body.site .plate .hero-product-shot { height: 100%; margin: 0; padding: 0; }
body.site .plate .hero-product-shot figcaption { display: none; }
body.site .plate .herdr-ui-mock {
max-width: none; width: 100%; height: 100%; aspect-ratio: auto;
border: 0; border-radius: 0; box-shadow: none;
font-size: clamp(.6rem, .92cqw, .95rem);
}
/* .mock-main only sets min-width: 0, so as a grid item it grows to its
content height and the plate never clamps the pane */
body.site .plate .mock-main { min-height: 0; }
body.site .plate .mock-panes { grid-template-rows: minmax(0, 1fr); }
/* ── router rows: signposts, not pillars ──
Stacked so each gets room and the destination is the heaviest thing in
the row — these are for going somewhere, not for persuading. */
body.site .caps { border-bottom: 1px solid var(--line2); }
/* The whole row is the link, so the destination button is gone and the marks
move to the right — which is also where the frame had a half-empty column
doing nothing. The arrow is the affordance and it is always visible, since a
row that only reveals it on hover gives a reader nothing to go on. */
/* Three columns: index, text at its measure, evidence. The row used to be text
plus a lot of nothing, and making the whole row a hover target drew a box
around the nothing. Every row now ends in something real — the session the
plate above shows, the agent states the copy names, the commands agents run,
the agents themselves — so the width is used rather than merely spanned. */
body.site .caps > .cap {
display: grid; grid-template-columns: 130px minmax(0, 58ch) minmax(0, 1fr);
gap: 0 40px; align-items: start;
padding: 26px var(--gut); border-bottom: 1px solid var(--line);
text-decoration: none; cursor: pointer;
}
body.site .caps > .cap:last-child { border-bottom: 0; }
body.site .caps > .cap:hover { background: color-mix(in srgb, var(--spot) 4%, transparent); }
body.site .caps > .cap:hover h3 { color: var(--spot); }
/* .arw, not .go — .go is already the hero's button group and carries a
38px top margin that pushed the arrow to the bottom of the column.
It rides the headline rather than sitting in the mark column: with the
marks moved left, an arrow down there would point away from the text and
away from the destination. On the headline it sits where the eye lands. */
/* Inline, not inline-block: an atomic inline carries a wrap opportunity before
it whatever the whitespace does, so at narrow widths the arrow dropped to a
line of its own under the heading. As plain inline text the no-break space in
the markup binds it to the last word. The hover nudge moves the margin for
the same reason — transforms don't apply to a non-replaced inline. */
body.site .caps .arw {
margin-left: 4px; vertical-align: 2px;
font-family: var(--body); font-weight: 400; font-size: 15px; line-height: 1;
color: var(--faint); transition: color .12s ease, margin-left .12s ease;
}
body.site .caps > .cap:hover .arw { color: var(--spot); margin-left: 8px; }
/* The marks lead the row. On the right they sat past the end of a 62ch
measure and read as debris pinned to the frame edge; on the left they are a
margin the text hangs off, and the row's ragged right edge is just how
columns end. The index is set large for mass but kept low in contrast — it
is texture and position, not a second headline competing with the h3. */
body.site .caps .n {
display: flex; flex-direction: column; gap: 13px; align-items: flex-start;
align-self: start; line-height: 1;
}
body.site .caps .n > span:first-child {
font-family: var(--disp); font-weight: 900; font-size: 44px;
letter-spacing: -.05em; color: var(--mass); transition: color .12s ease;
}
body.site .caps > .cap:hover .n > span:first-child { color: var(--line2); }
/* One frame for all four, at one width, flush to the right gutter — so the
evidence reads as the same kind of object in every row and the column has a
straight edge instead of a ragged one. The frame is the terminal's, moved up
from .tape so the typing line no longer owns a border the others lack. */
body.site .caps .ev {
align-self: center; justify-self: end;
width: 100%; max-width: 520px;
border: 1px solid var(--line2); background: var(--panel);
padding: 13px 15px;
font-family: var(--mono); font-size: 11.5px; color: var(--faint);
}
body.site .caps .ev-row { display: flex; align-items: center; gap: 14px; padding: 6px 0; }
body.site .caps .ev-row > span { color: var(--dim); }
body.site .caps .ev-row b {
margin-left: auto; font-weight: 400;
display: inline-flex; align-items: center; gap: 9px;
}
body.site .caps .ev-row i { display: block; width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
body.site .caps .ev-note { margin-top: 9px; line-height: 1.7; }
/* ── 05, the tease ──
Inert by design: no link, no pointer, no hover reveal, no arrow. It reads
the same to everyone on every device, which is the only version of this
that a phone gets too. The blocks are set in mono because JetBrains Mono
actually carries U+2580259F; in the display face they would fall back to
whatever the system happens to have. */
body.site .caps .cap-soon { cursor: default; }
body.site .caps .cap-soon .mask { font-family: var(--mono); color: var(--line2); letter-spacing: -.02em; }
body.site .caps .cap-soon .tape { color: var(--faint); }
/* The line is fully redacted, so there is nothing being typed. A caret would
imply a live prompt and put the one moving thing on the page next to the row
that is deliberately saying nothing. */
body.site .caps .cap-soon .tape > span { border-right: 0; animation: none; }
/* available to a screen reader, never on screen */
body.site .sr {
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* green live dot: "this is up right now" */
body.site .caps .beat {
display: block; width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
background: var(--run); animation: site-pulse 2.2s ease-in-out infinite;
}
/* blocked, working, idle — traffic-light order, and all three sit still: they
are a legend, and a legend that moves reads as an alarm. Only the heartbeat
in the row above animates. */
body.site .caps .ev-row .b { background: var(--st-blocked); }
body.site .caps .ev-row .w { background: var(--st-working); }
/* hollow, because state_dot() draws idle as ○ and every other state as ● —
the fill is part of the signal, not decoration */
body.site .caps .ev-row .i { background: none; box-shadow: inset 0 0 0 2px var(--st-idle); }
/* the typing line. The frame lives on .ev now, so this is just the line —
still nowrap and clipped, so a long command can never reflow the box. */
body.site .caps .tape {
display: flex; align-items: baseline; gap: 9px; margin: 0;
color: var(--dim); min-height: 19px; overflow: hidden; white-space: nowrap;
}
body.site .caps .tape b { color: var(--spot); font-weight: 400; }
/* The caret is the span's own right border, not a sibling box. As a sibling it
sat at the shrunk span's edge while the text overflowed past it, which drew
the caret on top of the words — and a block sibling can't sit on the text
baseline. A border rides the line box, so it lands after the last character
at the right height by construction. */
/* direct child only: row 05's line wraps a nested redaction block, and a
descendant selector gave that block a caret of its own mid-line */
body.site .caps .tape > span {
flex: 0 1 auto; min-width: 0;
border-right: 7px solid var(--dim);
animation: site-caret 1.05s steps(1) infinite;
}
@keyframes site-caret { 0%, 49% { border-color: var(--dim) } 50%, 100% { border-color: transparent } }
/* Masked, not inlined: the source files carry their own brand fills, and one
of them is white — which would vanish on paper. Silhouettes in one colour
also stop the row turning into a logo salad. */
/* two columns so eight entries stay a compact block rather than a tall list
that would tower over the other three evidence boxes */
body.site .caps .herd {
display: grid; grid-template-columns: 1fr 1fr; gap: 11px 24px; margin: 0;
}
body.site .caps .herd span { display: flex; align-items: center; gap: 11px; color: var(--dim); }
body.site .caps .herd i {
width: 15px; height: 15px; flex: 0 0 auto; background: var(--faint);
-webkit-mask: var(--m) center/contain no-repeat;
mask: var(--m) center/contain no-repeat;
}
body.site .caps > .cap:hover .herd i { background: var(--dim); }
@media (prefers-reduced-motion: reduce) {
body.site .caps .beat,
body.site .caps .tape > span { animation: none; }
}
body.site .caps h3 { margin: 0; font-family: var(--disp); font-weight: 800; letter-spacing: -.03em; font-size: 21px; color: var(--ink); }
body.site .caps p { margin: 9px 0 0; color: var(--dim); font-size: 13px; line-height: 1.7; max-width: 62ch; }
body.site .caps h3 { transition: color .12s ease; }
/* ── page head: shared by blog and any other framed page ── */
body.site .ph { padding: 68px var(--gut) 44px; border-bottom: 1px solid var(--line2); }
/* a wide frame left the right half of the head empty — give it a slot with
something real in it rather than widening the measure past readability */
body.site .ph-split {
display: grid; grid-template-columns: minmax(0, 1fr) auto;
gap: 60px; align-items: end;
}
body.site .ph-side { text-align: right; }
body.site .ph-side b {
display: block; font-family: var(--disp); font-weight: 900;
font-size: clamp(40px, 4.6vw, 68px); letter-spacing: -.05em; line-height: 1; color: var(--ink);
}
body.site .ph-side span {
display: block; margin-top: 10px;
color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
}
body.site .ph-side a { display: inline-block; margin-top: 18px; color: var(--spot); text-decoration: none; font-size: 12px; }
body.site .ph-side a:hover { text-decoration: underline; }
body.site .ph h1 {
font-family: var(--disp); font-weight: 900; letter-spacing: -.05em; line-height: .92;
font-size: clamp(34px, 5vw, 72px); margin: 22px 0 0; max-width: 18ch; color: var(--ink);
}
body.site .ph p { margin: 22px 0 0; color: var(--dim); font-size: 14.5px; line-height: 1.75; max-width: 56ch; }
body.site .ph code {
color: var(--ink); background: var(--panel);
border: 1px solid var(--line2); padding: 1px 6px; font-size: 12px;
/* a chip split across two lines reads as a rendering fault, not a wrap */
white-space: nowrap;
}
body.site .ph-actions { display: flex; gap: 12px; margin-top: 30px; flex-wrap: wrap; }
body.site .btn-solid, body.site .btn-ghost {
text-decoration: none; padding: 12px 18px; font-size: 11.5px;
letter-spacing: .07em; text-transform: uppercase; border: 1px solid var(--line2);
}
body.site .btn-solid { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
body.site .btn-ghost { color: var(--ink); }
body.site .btn-ghost:hover { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
/* The ported pages still carry style.css's .button classes on the publish
links and the load-more control. Given the site language here rather than
swapped in the markup, so all three stay in step. The solid one rendered as
an empty light slab because `body.site a { color: inherit }` outranks
.button-primary's own `color`, so the label took the card's near-white. */
body.site .button {
min-height: 0; padding: 12px 18px; border-radius: 0;
border: 1px solid var(--line2); font-family: var(--mono); font-weight: 400;
font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase;
}
body.site .button-primary { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
body.site .button-primary:hover { background: var(--ink); border-color: var(--ink); color: var(--bg); }
body.site .button-secondary { background: none; color: var(--ink); }
body.site .button-secondary:hover { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
/* ── plugins ──
The markup is the original page's; only the language is restyled, so the
live fetch, search and sort behaviour is untouched. ── */
/* not flex: a wrapping flex row put the status dot on a line of its own */
body.site .plugins-trust {
display: block; margin: 20px 0 0;
color: var(--faint); font-size: 12px; line-height: 1.7; max-width: 78ch;
}
body.site .plugins-trust .status-dot {
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
margin-right: 9px; vertical-align: 1px; background: var(--idle);
}
body.site .plugins-trust a { color: var(--spot); text-decoration: none; }
body.site .plugins-controls {
display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
padding: 20px var(--gut); border-bottom: 1px solid var(--line2);
}
body.site .plugins-search-wrap { position: relative; flex: 1; min-width: 240px; }
body.site .plugins-search-icon {
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
color: var(--spot); font-size: 13px;
}
body.site .plugins-search {
width: 100%; background: var(--panel); border: 1px solid var(--line2); border-radius: 0;
color: var(--ink); font-family: var(--mono); font-size: 13px; padding: 12px 14px 12px 34px;
}
body.site .plugins-search::placeholder { color: var(--faint); }
body.site .plugins-search:focus { outline: 0; border-color: var(--spot); }
body.site .plugins-sort-wrap { display: flex; align-items: center; gap: 10px; }
body.site .plugins-sort-label { color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase; }
body.site .plugins-sort {
background: var(--panel); border: 1px solid var(--line2); border-radius: 0;
color: var(--ink); font-family: var(--mono); font-size: 12.5px;
padding: 11px 34px 11px 13px; cursor: pointer;
appearance: none; -webkit-appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--faint) 50%),
linear-gradient(135deg, var(--faint) 50%, transparent 50%);
background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
body.site .plugins-sort:focus { outline: 0; border-color: var(--spot); }
/* the count sits flush against the frame otherwise */
body.site .plugins-status { margin-left: auto; padding: 0; white-space: nowrap; }
body.site .plugins-metabar { border-bottom: 1px solid var(--line); }
body.site .plugins-meta,
body.site .plugins-status {
margin: 0; padding: 13px var(--gut); color: var(--faint); font-size: 10.5px;
letter-spacing: .18em; text-transform: uppercase;
}
body.site .plugins-status { border-top: 1px solid var(--line); }
/* the old page wraps every block in .section, which brings its own vertical
padding, a top border and a horizontal inset — so blocks drifted further
right the deeper they were nested. Neutralise it and let each block own
its gutter, aligned with the page head. */
body.site .plugins-page .section {
padding: 0; border-top: 0; max-width: none; margin: 0;
}
body.site .plugins-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 14px; padding: 26px var(--gut) 34px;
}
/* cards are hairline cells in a sheet, not filled boxes floating on it */
body.site .plugin-card {
display: flex; flex-direction: column; min-width: 0;
padding: 20px 22px 22px; text-decoration: none;
background: none; border: 1px solid var(--line2); border-radius: 0;
text-transform: none; letter-spacing: normal;
}
body.site .plugin-card:hover {
background: color-mix(in srgb, var(--spot) 5%, transparent); border-color: var(--spot);
}
body.site .plugin-card-head { display: flex; align-items: baseline; gap: 12px; }
body.site .plugin-title { min-width: 0; font-size: 15px; letter-spacing: -.01em; }
body.site .plugin-owner { color: var(--faint); }
body.site .plugin-name { color: var(--ink); font-weight: 500; }
body.site .plugin-card:hover .plugin-name { color: var(--spot); }
body.site .plugin-go { margin-left: auto; color: var(--faint); font-size: 14px; }
body.site .plugin-card:hover .plugin-go { color: var(--spot); }
/* clamp to two lines so every cell has the same shape — the feed's own
ellipsis truncated at a character count, which made the rows ragged */
body.site .plugin-desc {
margin: 11px 0 0; color: var(--dim); font-size: 12.5px; line-height: 1.65;
text-transform: none; letter-spacing: normal;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; min-height: calc(2 * 1.65em);
}
body.site .plugin-meta-row {
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
margin-top: auto; padding-top: 18px;
color: var(--faint); font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
}
body.site .plugin-stat { display: flex; align-items: center; gap: 7px; }
body.site .plugin-star { color: var(--spot); }
body.site .plugin-lang-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
body.site .plugin-updated { margin-left: auto; }
/* .plugins-more is the button, not a wrapper — given section padding it
inflated into a floating slab. It's the foot of a long list, so make it the
full-width row a list foot should be rather than a box parked in the middle. */
body.site .plugins-more {
display: block; width: 100%; margin: 0; padding: 22px var(--gut);
border: 0; border-top: 1px solid var(--line2); background: none;
color: var(--dim); text-align: center; cursor: pointer;
}
body.site .plugins-more:hover {
background: color-mix(in srgb, var(--spot) 5%, transparent); color: var(--spot);
}
/* the rule above outranks the page's own .plugins-more[hidden] guard, so the
exhausted button stayed on screen; restate the guard at this weight */
body.site .plugins-more[hidden] { display: none; }
body.site .plugins-publish { padding: 40px var(--gut) 48px; border-bottom: 1px solid var(--line2); }
body.site .plugins-publish-card { border: 1px solid var(--line2); padding: 26px 28px; }
/* the card's heading is an h3, so the h2 rule missed it and the page's own
mono declaration stood — the one section heading not in Archivo */
body.site .plugins-publish h2,
body.site .plugins-publish h3 {
margin: 0; font-family: var(--disp); font-weight: 900; letter-spacing: -.035em;
font-size: 26px; color: var(--ink);
}
body.site .plugins-publish p { margin: 12px 0 0; color: var(--dim); font-size: 13px; line-height: 1.75; max-width: 70ch; }
body.site .plugins-publish-actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; }
/* ── blog index ── */
body.site .posts { border-bottom: 1px solid var(--line2); }
body.site .posts a {
display: grid; grid-template-columns: 132px minmax(0, 1fr) auto;
gap: 0 30px; align-items: baseline; text-decoration: none;
padding: 24px var(--gut); border-bottom: 1px solid var(--line);
}
body.site .posts a:last-child { border-bottom: 0; }
body.site .posts a:hover { background: color-mix(in srgb, var(--spot) 5%, transparent); }
body.site .posts time { color: var(--faint); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
body.site .posts h2 {
margin: 0; font-family: var(--disp); font-weight: 800; letter-spacing: -.03em;
font-size: 21px; color: var(--ink);
}
body.site .posts p { margin: 8px 0 0; color: var(--dim); font-size: 12.5px; line-height: 1.7; max-width: 74ch; }
body.site .posts .go-arrow { color: var(--faint); font-size: 15px; }
body.site .posts a:hover .go-arrow { color: var(--spot); }
/* ── article ── */
/* The measure column is capped at the body's own 76ch. Left as 1fr it ate every
spare pixel and parked the sections list against the far edge, with the gap
between them growing without limit on a wide display. */
body.site .article {
display: grid; grid-template-columns: minmax(0, 76ch) 224px; gap: 60px;
justify-content: center;
padding: 54px var(--gut) 76px; border-bottom: 1px solid var(--line2); align-items: start;
}
/* No sidebar means no second column, and the measure centres rather than
hugging the left gutter with the whole frame empty beside it. Elsewhere on
the site a short measure sits left because something else occupies the
right; here nothing does. */
body.site .article-solo { grid-template-columns: minmax(0, 1fr); }
body.site .article-solo .article-body { margin-inline: auto; }
body.site .article-body { min-width: 0; max-width: 76ch; }
body.site .article-meta {
display: flex; gap: 16px; align-items: baseline;
color: var(--faint); font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
}
body.site .article-meta a { color: var(--spot); text-decoration: none; }
body.site .article h1 {
font-family: var(--disp); font-weight: 900; letter-spacing: -.045em; line-height: .95;
font-size: clamp(30px, 3.6vw, 52px); margin: 18px 0 0; color: var(--ink);
}
body.site .article-lede { margin: 20px 0 0; color: var(--dim); font-size: 15px; line-height: 1.75; }
body.site .article-rule { margin: 34px 0 0; border: 0; border-top: 1px solid var(--line2); }
/* prose: mono body, display headings, hairlines instead of boxes */
body.site .prose { margin-top: 34px; color: var(--dim); font-size: 13.5px; line-height: 1.85; }
body.site .prose > * + * { margin-top: 20px; }
body.site .prose h2 {
font-family: var(--disp); font-weight: 800; letter-spacing: -.03em; font-size: 25px;
color: var(--ink); margin-top: 46px; padding-top: 18px; border-top: 1px solid var(--line);
}
body.site .prose h3 { font-family: var(--disp); font-weight: 700; font-size: 18px; color: var(--ink); margin-top: 32px; }
body.site .prose a { color: var(--spot); text-underline-offset: 3px; }
body.site .prose strong { color: var(--ink); font-weight: 500; }
body.site .prose code {
color: var(--ink); background: var(--panel);
border: 1px solid var(--line2); padding: 1px 6px; font-size: 12px;
}
body.site .prose pre {
background: var(--panel); border: 1px solid var(--line2);
padding: 16px 18px; overflow-x: auto; font-size: 12.5px; line-height: 1.7;
}
body.site .prose pre code { background: none; border: 0; padding: 0; font-size: inherit; }
body.site .prose ul, body.site .prose ol { padding-left: 22px; }
body.site .prose li + li { margin-top: 8px; }
body.site .prose blockquote {
border-left: 2px solid var(--spot); padding-left: 18px; margin-left: 0; color: var(--faint);
}
body.site .prose img { max-width: 100%; border: 1px solid var(--line2); }
body.site .prose table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
body.site .prose th, body.site .prose td { text-align: left; padding: 9px 12px; border: 1px solid var(--line); }
body.site .prose th { color: var(--ink); font-weight: 500; }
/* ── post figures ──
Diagrams are built from the site's own tokens rather than exported as PNGs.
They switch with ink and paper, stay sharp at any density, and a diagram of
the sidebar stops being a lie the next time the sidebar changes. */
body.site .pf { margin: 34px 0 0; }
body.site .pf-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
body.site .pf-panel { border: 1px solid var(--line2); background: var(--panel); min-width: 0; }
body.site .pf-label {
display: flex; justify-content: space-between; gap: 10px;
padding: 8px 12px; border-bottom: 1px solid var(--line2);
color: var(--faint); font-family: var(--mono); font-size: 10px;
letter-spacing: .16em; text-transform: uppercase;
}
body.site .pf-label b { color: var(--dim); font-weight: 400; }
body.site .pf-cap { margin: 12px 0 0; color: var(--faint); font-size: 11.5px; line-height: 1.6; }
/* agent rows */
body.site .pf-row {
display: grid; grid-template-columns: 14px minmax(0, 1fr); gap: 9px; align-items: baseline;
padding: 7px 12px; font-family: var(--mono); font-size: 12px;
}
body.site .pf-row + .pf-row { border-top: 1px solid var(--line); }
body.site .pf-row strong { color: var(--ink); font-weight: 400; }
body.site .pf-row small { display: block; color: var(--faint); font-size: 10.5px; }
body.site .pf-mark { line-height: 1; }
body.site .pf-row.working .pf-mark, body.site .pf-row.working small { color: var(--st-working); }
body.site .pf-row.blocked .pf-mark, body.site .pf-row.blocked small { color: var(--st-blocked); }
body.site .pf-row.done .pf-mark, body.site .pf-row.done small { color: var(--st-done); }
body.site .pf-row.idle .pf-mark, body.site .pf-row.idle small { color: var(--st-idle); }
/* The left panel animates because the argument is the motion. Four braille
frames at 8fps is what 0.7.5 actually did, per working pane, forever. */
body.site .pf-spin { position: relative; display: inline-block; width: 1ch; }
body.site .pf-spin i { position: absolute; left: 0; top: 0; font-style: normal; opacity: 0; animation: pf-frame .5s steps(1, end) infinite; }
body.site .pf-spin i:nth-child(2) { animation-delay: .125s; }
body.site .pf-spin i:nth-child(3) { animation-delay: .25s; }
body.site .pf-spin i:nth-child(4) { animation-delay: .375s; }
@keyframes pf-frame { 0%, 24.9% { opacity: 1; } 25%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
body.site .pf-spin i { animation: none; }
body.site .pf-spin i:first-child { opacity: 1; }
}
/* flow steps */
body.site .pf-flow { padding: 14px 12px; font-family: var(--mono); font-size: 11.5px; }
body.site .pf-step { color: var(--dim); padding: 5px 0; }
body.site .pf-step b { color: var(--ink); font-weight: 400; }
body.site .pf-step s { display: block; color: var(--faint); text-decoration: none; font-size: 10.5px; padding-top: 2px; }
body.site .pf-arrow { color: var(--line2); padding: 1px 0; }
/* the two outcomes are alternatives, not the next two steps, so they get a
fork rather than another length of the same connector */
body.site .pf-fork em { color: var(--faint); font-style: normal; margin-right: 8px; }
body.site .pf-fork s { padding-left: 34px; }
body.site .pf-stop { color: var(--st-done); }
body.site .pf-cost { color: var(--st-blocked); }
/* bars */
body.site .pf-bars { padding: 16px 14px; font-family: var(--mono); font-size: 11px; }
body.site .pf-bar-row + .pf-bar-row { margin-top: 16px; }
body.site .pf-bar-name { color: var(--dim); }
body.site .pf-bar { display: grid; grid-template-columns: 46px minmax(0, 1fr) 58px; gap: 8px; align-items: center; margin-top: 5px; }
body.site .pf-bar span:first-child { color: var(--faint); font-size: 10px; }
body.site .pf-bar i { display: block; height: 9px; min-width: 1px; }
body.site .pf-bar em { color: var(--faint); font-style: normal; text-align: right; }
body.site .pf-bar.old i { background: var(--line2); }
body.site .pf-bar.new i { background: var(--spot); }
body.site .pf-bar.new em { color: var(--dim); }
@media (max-width: 560px) {
body.site .pf-2 { grid-template-columns: minmax(0, 1fr); }
body.site .pf-bar { grid-template-columns: 42px minmax(0, 1fr) 52px; }
}
/* sticky section index */
body.site .toc { position: sticky; top: 30px; }
body.site .toc p { margin: 0 0 12px; color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase; }
body.site .toc nav { display: flex; flex-direction: column; }
body.site .toc a {
color: var(--dim); text-decoration: none; font-size: 12px; line-height: 1.5;
padding: 7px 0 7px 13px; border-left: 1px solid var(--line2);
}
body.site .toc a:hover { color: var(--ink); }
body.site .toc a[aria-current] { color: var(--spot); border-left-color: var(--spot); }
/* ── close ── */
body.site .end { position: relative; overflow: hidden; padding: 96px var(--gut) 104px; border-bottom: 1px solid var(--line2); }
/* The bleed is a fraction of the mark, not of the section. As a percentage it
resolved against .end's height, so the taller the copy wrapped the further
the ram sank — on a phone only its crown cleared the divider. */
body.site .end .ram {
--ram-size: min(560px, 54vw);
position: absolute; right: -6%; bottom: calc(var(--ram-size) * -0.22);
width: var(--ram-size); height: var(--ram-size); color: var(--mass); z-index: 0;
}
body.site .end .in { position: relative; z-index: 1; }
body.site .end h2 { font-family: var(--disp); font-weight: 900; letter-spacing: -.05em; font-size: clamp(36px, 5.4vw, 70px); line-height: .93; margin: 0; max-width: 13ch; color: var(--ink); }
body.site .end p { margin: 20px 0 0; color: var(--dim); font-size: 13.5px; max-width: 52ch; line-height: 1.8; }
/* ── footer ──
The drafting title block that used to sit here is gone. Three of its five
cells were marketing in a spec costume, it was identical on every page so it
said nothing after the first view, and its version was hardcoded in two
files. The two facts worth keeping now sit in the footer line, with the
version read from latest.json at build. */
body.site .foot { padding: 22px var(--gut); border-top: 1px solid var(--line2); display: flex; gap: 22px; align-items: center; flex-wrap: wrap; color: var(--faint); font-size: 11.5px; }
body.site .foot a { color: var(--dim); text-decoration: none; }
body.site .foot a:hover { color: var(--ink); }
body.site .foot .ram { width: 15px; height: 15px; color: var(--faint); }
body.site .foot .fact { color: var(--faint); font-variant-numeric: tabular-nums; }
/* the outbound cluster sits right: site links and provenance on one side, ways
to reach the project on the other. Its own gap is tighter than the row's, so
two 14px marks read as a pair, not as two more items in the list. */
body.site .foot .out { margin-left: auto; display: flex; align-items: center; gap: 16px; }
body.site .foot .ico { display: inline-flex; color: var(--faint); }
body.site .foot .ico svg { width: 14px; height: 14px; fill: currentColor; }
body.site .foot .ico:hover { color: var(--ink); }
@media (max-width: 900px) {
:root { --gut: 20px; }
body.site { background-size: 40px 40px; padding-bottom: 40px; }
/* One row can't hold the lockup, the count, Install and three links at
390px — those are 450px of cells in 350px. The links move into the sheet,
which also carries what phone width otherwise drops silently. */
body.site .hd-nav a.lnk { display: none; }
/* the toggle is 103px of the bar and the sheet now carries it. Left in, it
overflowed the nav at 430560px, which widened the document past the
viewport and left a dead strip down the right of every section. */
body.site .hd-nav .mode { display: none; }
body.site .hd-nav .menu { display: inline-flex; }
body.site .sheet.open { display: block; }
/* two up: the column divider alone left the rows floating together, so the
four cells read as one block rather than a grid. The bottom row keeps its
edge from the strip's own border. */
body.site .strip { grid-template-columns: 1fr 1fr; }
body.site .strip .stat { border-bottom: 1px solid var(--line); }
body.site .strip .stat:nth-last-child(-n + 2) { border-bottom: 0; }
body.site .strip .stat:nth-child(2n) { border-right: 0; }
/* the evidence drops under the text rather than competing for width */
body.site .caps > .cap { grid-template-columns: 54px minmax(0, 1fr); gap: 0 18px; }
body.site .caps .n > span:first-child { font-size: 30px; }
/* stacked, the card butted straight into the last line of the paragraph —
the 20px was inside its border, so it padded the card rather than
separating it. Margin instead, and the interior goes back to even. */
body.site .caps .ev { grid-column: 2; margin-top: 20px; }
body.site .fig { padding: 26px var(--gut) 30px; }
body.site .plate { aspect-ratio: auto; }
body.site .hd-hero { padding: 60px var(--gut) 52px; }
/* 224px of sections beside a phone measure left the title breaking every two
words. A sticky jump list is worth least on the screen it costs most, so
the post falls back to the same single column a heading-less post uses. */
body.site .article { grid-template-columns: minmax(0, 1fr); }
body.site .article .toc { display: none; }
}
/* Below this the nav's cells stop fitting, so they come off in order of how
little they're worth on a phone. Measured at the 24px gap: lock 88 + github
88 + x 30 + toggle 103 + install 86 is 515px of cells, and a 480px viewport
leaves 440px. X goes first — it's the one thing the footer also carries. */
@media (max-width: 560px) {
body.site .hd-nav { gap: 14px; }
body.site .hd-nav .ico[href*="x.com"] { display: none; }
/* the hint stops competing for the same line as the title and becomes the
line under it — right-aligned, it read as a second ragged column */
body.site .fighd { flex-wrap: wrap; gap: 6px 16px; }
body.site .fighd .r { margin-left: 0; text-align: left; }
/* the close is mostly the watermark's room at this width */
body.site .end { padding: 56px var(--gut) 64px; }
/* nothing to push against on one column: the cluster hung alone on its own
line while the rest of the footer sat left */
body.site .foot .out { margin-left: 0; }
/* the mark carries no wordmark of its own, so once the row wrapped it read as
a stray glyph leading a list rather than as a lockup. The identity is at the
top of the page; here it was only costing the row its first cell. */
body.site .foot .ram { display: none; }
/* the 132px date column left the title about 120px to live in, so every
headline broke every two words. The date goes above the entry instead. */
body.site .posts a { grid-template-columns: minmax(0, 1fr) auto; gap: 8px 16px; }
body.site .posts time { grid-column: 1 / -1; }
/* the count column took half the head and squeezed the copy into a
three-words-per-line ribbon; stacked, it becomes the head's footer */
body.site .ph-split { grid-template-columns: minmax(0, 1fr); gap: 30px; }
body.site .ph-side { text-align: left; }
body.site .ph-side b { font-size: 44px; }
}
@media (max-width: 430px) {
/* 44 characters at 13.5px ran to a third line with "sh" alone on it. At 12px
the only break opportunity — the space after -fsSL — lands mid-line and the
URL stays whole. */
body.site .hd-cmd code { padding: 12px 14px; font-size: 12px; }
body.site .hd-cmd button { padding: 12px 13px; }
}
/* The narrow end of the range — SE-class phones and folded covers, 320px up.
Four cells and three gaps came to 334px against 324px of bar at 364, so the
nav overflowed again and the document went wider than the screen. The count
is the widest thing that can go and still leave a working link: the mark
keeps the destination, and the same number is one screen down in the strip. */
@media (max-width: 380px) {
:root { --gut: 16px; }
body.site .hd-nav { gap: 10px; }
body.site .hd-nav .ico.wide { width: 30px; gap: 0; padding: 0; }
body.site .hd-nav .ico.wide b { display: none; }
/* the button's 60px was the difference between two lines and a third with
"sh" on it, so it becomes the row under the command instead of the cell
beside it — the same shape the plugins list uses for its foot */
body.site .hd-cmd { flex-wrap: wrap; }
body.site .hd-cmd code { flex: 1 1 100%; }
body.site .hd-cmd button {
flex: 1 1 100%; text-align: left; padding: 11px 14px;
border-left: 0; border-top: 1px solid var(--line2);
}
}
/* The mock scales on its own width — .hero-product-shot is the container.
Sidebar left, tabs above the pane: the same layout as the wide plate, just
smaller. It used to reflow into stacked strips, which is a shape herdr
never renders and which overflowed sideways anyway. Type is the thing that
shrinks, so the terminal stays crisp rather than being transform-scaled. */
@container (max-width: 900px) {
body.site .herdr-ui-mock {
aspect-ratio: auto; height: 74vh; min-height: 460px;
/* the sidebar keeps enough width for a branch name under the workspace */
grid-template-columns: minmax(84px, 27%) minmax(0, 1fr);
}
body.site .mock-space, body.site .mock-agent { padding: 6px 8px; }
/* "grouped" is the sort mode; at this width it collides with the heading */
body.site .mock-agent-title span:last-child { display: none; }
body.site .mock-main { min-height: 0; min-width: 0; }
body.site .mock-view, body.site .mock-pane { min-width: 0; }
/* splits stack out of view rather than halving an already narrow pane */
body.site .mock-panes { grid-template-columns: minmax(0, 1fr) !important; min-width: 0; }
body.site .mock-panes .mock-pane:not(.focused) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
body.site .strip i.live { animation: none; }
}
/* ──────────────────────────────────────────────────────────────
Typography split — readability first.
Prose inherits --body from body.site. Everything below is put back
into mono because mono is doing a real job there: identifiers and
repo paths, commands, uppercase labels, and figures with units.
Display headings stay Archivo and are set where they're declared.
────────────────────────────────────────────────────────────── */
body.site .hd-nav a.lnk,
body.site .hd-nav .btn,
body.site .hd-eyebrow,
body.site .meta,
body.site .stat span,
body.site .fighd,
body.site .caps .n,
body.site .foot,
body.site .ph-side span,
body.site .posts time,
body.site .article-meta,
body.site .toc p,
body.site .plugins-search,
body.site .plugins-sort,
body.site .plugins-sort-label,
body.site .plugins-meta,
body.site .plugins-status,
body.site .plugin-title,
body.site .plugin-meta-row,
body.site .btn-solid,
body.site .btn-ghost,
body.site code,
body.site pre {
font-family: var(--mono);
}
/* prose sizes: comfortable rather than compact */
body.site .lede { font-size: 16px; line-height: 1.7; max-width: 56ch; }
body.site .ph p { font-size: 15.5px; line-height: 1.7; max-width: 62ch; }
body.site .caps p { font-size: 14.5px; line-height: 1.7; color: var(--dim); }
body.site .end p { font-size: 15px; line-height: 1.75; }
body.site .figcap { font-size: 13.5px; line-height: 1.7; }
body.site .posts p { font-size: 14px; line-height: 1.65; }
body.site .article-lede { font-size: 16.5px; line-height: 1.7; }
body.site .prose { font-size: 16px; line-height: 1.75; }
body.site .prose code, body.site .prose pre { font-size: 13.5px; }
body.site .toc a { font-family: var(--body); font-size: 13px; line-height: 1.5; }
body.site .plugins-trust { font-size: 13.5px; line-height: 1.7; }
body.site .plugin-desc { font-size: 14px; line-height: 1.6; min-height: calc(2 * 1.6em); }
body.site .plugin-title { font-size: 14.5px; }
+465 -949
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@ const name = 'Can Celik';
const role = 'Building Herdr';
const photo = '/assets/author-can.jpeg';
const x = 'https://x.com/lumendriada';
const email = 'ogulcancelik@gmail.com';
const email = 'can@herdr.dev';
---
<aside class="author-card" aria-label="About the author">
+2 -4
View File
@@ -13,8 +13,8 @@ const {
description,
canonicalPath,
ogType = 'website',
ogImage = '/assets/og-card-v8.png',
ogImageAlt = 'Herdr — One terminal. The whole herd.',
ogImage = '/assets/og-card-v9.png',
ogImageAlt = 'Herdr — run them anywhere, leave them running.',
} = Astro.props;
const canonicalUrl = new URL(canonicalPath, 'https://herdr.dev').toString();
const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
@@ -77,7 +77,6 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
<a href="/">Home</a>
<a href="/compare/">Compare</a>
<a href="/plugins/">Plugins</a>
<a href="/stats/">Stats</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
@@ -90,7 +89,6 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
<a href="/">Home</a>
<a href="/compare/">Compare</a>
<a href="/plugins/">Plugins</a>
<a href="/stats/">Stats</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
+219
View File
@@ -0,0 +1,219 @@
---
/* The chassis every non-docs page shares: one dark ground, the framed grid,
the same nav and footer as the homepage. Pages supply their own <main>.
Docs keep Starlight's shell and are themed through src/styles/starlight.css. */
interface Props {
title: string;
description: string;
canonicalPath: string;
ogType?: 'website' | 'article';
ogImage?: string;
ogImageAlt?: string;
}
const {
title,
description,
canonicalPath,
ogType = 'website',
ogImage = '/assets/og-card-v9.png',
ogImageAlt = 'Herdr — run them anywhere, leave them running.',
} = Astro.props;
import { fmt, releaseVersion, siteStats } from '../site-data';
const canonicalUrl = new URL(canonicalPath, 'https://herdr.dev').toString();
const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
const year = new Date().getFullYear();
/* Same build-time source the homepage uses, so the nav count and the footer
version agree across every page without being written down twice. */
const { starCount } = await siteStats();
const version = await releaseVersion();
const facts = version ? `v${version} · Apache 2.0` : 'Apache 2.0';
---
<!doctype html>
<html lang="en" data-palette="catppuccin" data-mode="ink">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="theme-color" content="#17171a" />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content="Herdr" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={ogImageAlt} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageUrl} />
<meta name="twitter:image:alt" content={ogImageAlt} />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon.png?v=14" />
<link rel="apple-touch-icon" href="/assets/logo.png" />
<link rel="preload" href="/assets/jbm.woff2?v=2" as="font" type="font/woff2" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<script is:inline>
/* Ground selection, shared with the docs — see index.html for the
reasoning. Starlight's `starlight-theme` key is the source of truth;
`auto` is treated as dark so the default never follows the OS. */
window.herdrGround = {
read: function () {
try {
return localStorage.getItem('starlight-theme') === 'light' ? 'paper' : 'ink';
} catch (e) {
return 'ink';
}
},
apply: function (mode) {
var root = document.documentElement;
root.setAttribute('data-mode', mode);
root.setAttribute('data-palette', mode === 'paper' ? 'solarized-light' : 'catppuccin');
return mode;
},
save: function (mode) {
try {
localStorage.setItem('starlight-theme', mode === 'paper' ? 'light' : 'dark');
} catch (e) {}
},
};
window.herdrGround.apply(window.herdrGround.read());
</script>
<link rel="stylesheet" href="/css/style.css?v=59" />
<link rel="stylesheet" href="/css/site.css?v=60" />
</head>
<body class="site">
<div class="hd-frame">
<nav class="hd-nav">
<a class="lock" href="/"><span class="ram"></span><b>herdr</b></a>
<div class="sp"></div>
<a class="lnk" href="/docs/">Docs</a>
<a class="lnk" href="/plugins/">Plugins</a>
<a class="lnk" href="/blog/">Blog</a>
<a class="ico wide" href="https://github.com/herdrdev/herdr" aria-label="Herdr on GitHub — star count">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.64 7.64 0 0 1 8 3.87c.68 0 1.36.09 2 .26 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" /></svg>
<b data-stars>{fmt(starCount)}</b>
</a>
<a class="ico" href="https://x.com/herdrdev" aria-label="Herdr on X">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117Z" /></svg>
</a>
<div class="mode">
<button type="button" data-ground="ink" aria-pressed="true">ink</button>
<button type="button" data-ground="paper" aria-pressed="false">paper</button>
</div>
<a class="btn" href="/docs/install/">Install</a>
<button type="button" class="menu" id="nav-menu" aria-expanded="false" aria-controls="nav-sheet" aria-label="Menu">
<span></span><span></span><span></span>
</button>
</nav>
<div class="sheet" id="nav-sheet">
<a href="/docs/">Docs</a>
<a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="https://x.com/herdrdev">@herdrdev</a>
<div class="sheet-ground">
<span>Ground</span>
<div class="mode">
<button type="button" data-ground="ink" aria-pressed="true">ink</button>
<button type="button" data-ground="paper" aria-pressed="false">paper</button>
</div>
</div>
</div>
<slot />
<footer class="foot">
<span class="ram"></span>
<a href="/docs/">docs</a>
<a href="/plugins/">plugins</a>
<a href="/blog/">blog</a>
<span>built in the terminal · {year}</span>
<span class="fact">{facts}</span>
<span class="out">
<a class="ico" href="https://github.com/herdrdev/herdr" aria-label="Herdr on GitHub">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.64 7.64 0 0 1 8 3.87c.68 0 1.36.09 2 .26 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" /></svg>
</a>
<a class="ico" href="https://x.com/herdrdev" aria-label="Herdr on X">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117Z" /></svg>
</a>
<a href="mailto:hey@herdr.dev">hey@herdr.dev</a>
</span>
</footer>
</div>
<script is:inline>
(function () {
var buttons = document.querySelectorAll('[data-ground]');
function paint(mode) {
window.herdrGround.apply(mode);
buttons.forEach(function (b) {
b.setAttribute('aria-pressed', String(b.dataset.ground === mode));
});
}
buttons.forEach(function (b) {
b.addEventListener('click', function () {
window.herdrGround.save(b.dataset.ground);
paint(b.dataset.ground);
});
});
paint(window.herdrGround.read());
})();
/* nav sheet — the links, the account and the ground toggle that don't
fit the bar on a phone */
(function () {
var btn = document.getElementById('nav-menu');
var sheet = document.getElementById('nav-sheet');
if (!btn || !sheet) return;
function set(open) {
sheet.classList.toggle('open', open);
btn.setAttribute('aria-expanded', String(open));
}
btn.addEventListener('click', function (event) {
event.stopPropagation();
set(!sheet.classList.contains('open'));
});
/* the ground buttons live in the sheet, so a tap inside must not close
it — only taps on the page behind do */
document.addEventListener('click', function (event) {
if (!sheet.contains(event.target)) set(false);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') set(false);
});
})();
/* The nav count is baked at build, which can be a deploy behind. Refresh
it from the same snapshot so the number is current on every page. No
count-up here — this is chrome, not the argument the homepage makes. */
fetch('https://assets.herdr.dev/stats/stats.json', { headers: { accept: 'application/json' } })
.then(function (r) {
return r.ok ? r.json() : null;
})
.then(function (d) {
if (!d || !d.stars || typeof d.stars.total !== 'number') return;
var text = d.stars.total.toLocaleString('en-US');
document.querySelectorAll('[data-stars]').forEach(function (el) {
el.textContent = text;
});
})
.catch(function () {});
</script>
</body>
</html>
+3
View File
@@ -13,6 +13,9 @@ export const collections = {
description: z.string(),
date: z.coerce.date(),
draft: z.boolean().default(false),
/* description is always used for meta and OG; set false when it should
not also print as a subtitle under the title */
lede: z.boolean().default(true),
ogImage: z.string().optional(),
}),
}),
@@ -3,7 +3,6 @@ title: Coding agents are becoming runtimes
description: Coding agent CLIs are becoming developer infrastructure. Project instructions, lifecycle state, hooks, and terminal runtimes need shared contracts.
date: 2026-06-10
draft: false
ogImage: /assets/og-blog-coding-agents-runtimes.png
---
Coding agents have crossed an important line.
@@ -2,7 +2,6 @@
title: Live updates without killing your terminal processes
description: Herdr can replace its server during an update while keeping pane processes alive.
date: 2026-05-27
ogImage: /assets/og-blog-live-updates.png
---
Live handoff shipped in Herdr 0.6.3 as an experimental, opt-in update path. See the [live handoff docs](/docs/session-state/#live-handoff) for the user-facing behavior.
@@ -0,0 +1,163 @@
---
title: Ten agents, three clients, 95% less CPU
description: Herdr's sidebar stopped animating, hidden panes stopped drawing, and mouse movement stopped counting as a change. The saving compounds with panes and clients, so the busiest sessions gained the most.
date: 2026-08-03
ogImage: /assets/og-blog-frames-v1.png
draft: false
---
Herdr did not get faster.
That distinction is worth keeping, because the honest version is more interesting. No renderer in Herdr became ten times quicker. Herdr stopped asking the renderer to draw things that nobody was going to look at. In the workloads where that work dominated, total CPU across the server and every attached client fell by 89 to 95 percent.
The saving also compounds, which is the part worth knowing. One agent with one client attached improved 91 percent. Ten agents with background output and three clients attached improved 95, because a frame that is never produced is also never serialized, never transmitted, and never applied by any of them. The busier the session, the more there is to not do.
Three changes got us there, and none of them were found in a profiler. The first started as a complaint, and the rule that came out of it was that motion should mean something changed.
## The spinner was a complaint before it was a cost
Herdr's job is to tell you which agent needs you. You run five, ten, twenty of them, and the sidebar is how you know where to look.
So every working agent got a spinner. It seemed obvious at the time: motion means alive, and an agent that is thinking should look like it is thinking.
Run twenty agents and you find out what that actually builds. Twenty things moving at once, all saying the same thing, none of them saying it is the one that needs you. The workspace list already carried state as coloured dots, and the sidebar was the noisy half of the same information. People told us. We agreed.
The first fix was a performance fix. Instead of rebuilding pane contents for a spinner tick, the server reused the client's retained frame, redrew only the status rectangle, patched it, and sent the smaller result. It worked. It made an animation that should not exist about as cheap as an animation can be.
It was still twenty things moving. So we removed them.
<figure class="pf">
<div class="pf-2">
<div class="pf-panel">
<div class="pf-label"><span>0.7.5</span><b>3 spinners, 8fps</b></div>
<div class="pf-row working"><span class="pf-mark pf-spin"><i>⠋</i><i>⠙</i><i>⠹</i><i>⠸</i></span><div><strong>herdr</strong><small>working</small></div></div>
<div class="pf-row working"><span class="pf-mark pf-spin"><i>⠋</i><i>⠙</i><i>⠹</i><i>⠸</i></span><div><strong>web-dashboard</strong><small>working</small></div></div>
<div class="pf-row blocked"><span class="pf-mark">◉</span><div><strong>llm-proxy</strong><small>blocked, needs you</small></div></div>
<div class="pf-row working"><span class="pf-mark pf-spin"><i>⠋</i><i>⠙</i><i>⠹</i><i>⠸</i></span><div><strong>data-pipeline</strong><small>working</small></div></div>
<div class="pf-row done"><span class="pf-mark">●</span><div><strong>docs</strong><small>done</small></div></div>
</div>
<div class="pf-panel">
<div class="pf-label"><span>0.8.0</span><b>nothing moves</b></div>
<div class="pf-row working"><span class="pf-mark">●</span><div><strong>herdr</strong><small>working</small></div></div>
<div class="pf-row working"><span class="pf-mark">●</span><div><strong>web-dashboard</strong><small>working</small></div></div>
<div class="pf-row blocked"><span class="pf-mark">◉</span><div><strong>llm-proxy</strong><small>blocked, needs you</small></div></div>
<div class="pf-row working"><span class="pf-mark">●</span><div><strong>data-pipeline</strong><small>working</small></div></div>
<div class="pf-row done"><span class="pf-mark">●</span><div><strong>docs</strong><small>done</small></div></div>
</div>
</div>
<figcaption class="pf-cap">Both panels carry the same information. Only one of them lets you find the blocked agent without reading. The left panel is animating in your browser right now, which is the entire argument.</figcaption>
</figure>
Working state is now a static coloured mark. Entering the state still renders immediately, so a change catches your eye the moment it happens. Staying in it costs nothing, and looks like nothing.
The performance consequence was larger than the design one. Animating that sidebar meant waking on a timer, rendering the status surface, comparing the frame, preparing it, serializing it, sending it to every attached client, and having each client receive and apply it. Roughly eight times a second, for as long as any agent was working, whether or not one other thing on screen had changed.
Removing the spinner removed all of it: the timer, the animation deadlines, the scans asking whether any workspace contained a working pane, the animation-only render causes, the frame patching. Master produces zero scheduled animation frames. One agent working with nothing else on screen went from 1.467 percent CPU to 0.133 on Linux, and from 3.280 to 0.265 on macOS.
We removed spinners because they were noise, not because they were inefficient.
## The same rule, applied to output you cannot see
Once motion means a change, the question becomes which things are actually changes.
When a pane produced output, the PTY reader set a shared flag meaning something happened. It coalesced wakeups efficiently and threw away the one fact that mattered, which was which pane. So when the server picked that flag up it could not tell whether the pane was even on screen. A background agent writing to a tab you were not looking at went into rendering anyway, and with several clients attached, produced frames for all of them.
<figure class="pf">
<div class="pf-2">
<div class="pf-panel">
<div class="pf-label"><span>0.7.5</span><b>a pane writes</b></div>
<div class="pf-flow">
<div class="pf-step"><b>dirty flag set</b><s>something changed, we do not know what</s></div>
<div class="pf-arrow">│</div>
<div class="pf-step"><b>render</b><s>no way to rule it out</s></div>
<div class="pf-arrow">│</div>
<div class="pf-step"><b>serialize, transmit</b><s>once per attached client</s></div>
<div class="pf-arrow">│</div>
<div class="pf-step pf-cost"><b>3 clients apply a frame</b><s>for a tab none of them are showing</s></div>
</div>
</div>
<div class="pf-panel">
<div class="pf-label"><span>0.8.0</span><b>a pane writes</b></div>
<div class="pf-flow">
<div class="pf-step"><b>request_pty(pane 7)</b><s>the source is kept</s></div>
<div class="pf-arrow">│</div>
<div class="pf-step"><b>can any client see pane 7?</b><s>tabs, zoom, popups, direct attaches</s></div>
<div class="pf-fork">
<div class="pf-step pf-stop"><em>├─ no</em><b>stop here</b><s>bytes already parsed, nothing drawn</s></div>
<div class="pf-step"><em>└─ yes</em><b>render as before</b><s>visible output is still visible output</s></div>
</div>
</div>
</div>
</div>
<figcaption class="pf-cap">Render requests now carry their source. Writes from one pane coalesce into a single entry, while simultaneous writes from different panes each keep their identity.</figcaption>
</figure>
Skipping loses nothing. The bytes have already been parsed, so terminal state, scrollback, cursor, OSC metadata and agent detection all stay current. Switching to that tab renders from that state normally. Where the mapping is unknown, the pane is treated as visible rather than suppressed, because a wrong skip costs you a stale screen and a wrong render costs some CPU. Those are not the same kind of mistake.
## And to input that changes nothing
Every mouse event reported that the view had changed. Pointer movement over a pane is forwarded to the application inside it, but Herdr's own frame is usually identical before and after. At 60 motion events per second, that meant roughly 60 server renders and 60 client frames per second, to draw the same picture sixty times.
Herdr now knows which of its own modes respond to hover, which is the global menu, context menus and the navigator. Outside those, movement alone repaints nothing, while the events themselves still route through to the pane exactly as before.
The benchmark counted both ends. In every measured round, on both builds, 1,680 motion packets were sent and all 1,680 arrived in the pane. Version 0.7.5 emitted around 56 to 60 frames per second doing it. Master emitted none. Linux CPU fell from 9.450 percent to 0.667.
## Why three changes became ninety percent
A render avoided at the server is not one saving. It is the frame preparation, the serialization, the transmission and the frame handling in every attached client, none of which happen.
That is why the numbers grow with clients rather than shrinking. Ten silent working panes with one client improved 78 percent. The same panes with three clients improved 91.
<figure class="pf">
<div class="pf-panel">
<div class="pf-label"><span>total CPU, server plus every attached client</span><b>linux</b></div>
<div class="pf-bars">
<div class="pf-bar-row">
<div class="pf-bar-name">one silent working pane</div>
<div class="pf-bar old"><span>0.7.5</span><i style="width:11.3%"></i><em>1.467%</em></div>
<div class="pf-bar new"><span>0.8.0</span><i style="width:1%"></i><em>0.133%</em></div>
</div>
<div class="pf-bar-row">
<div class="pf-bar-name">ten silent working panes, three clients</div>
<div class="pf-bar old"><span>0.7.5</span><i style="width:33.2%"></i><em>4.316%</em></div>
<div class="pf-bar new"><span>0.8.0</span><i style="width:3.1%"></i><em>0.400%</em></div>
</div>
<div class="pf-bar-row">
<div class="pf-bar-name">ten panes, nine hidden writers, three clients</div>
<div class="pf-bar old"><span>0.7.5</span><i style="width:100%"></i><em>13.014%</em></div>
<div class="pf-bar new"><span>0.8.0</span><i style="width:4.7%"></i><em>0.617%</em></div>
</div>
<div class="pf-bar-row">
<div class="pf-bar-name">passive mouse motion at 60Hz</div>
<div class="pf-bar old"><span>0.7.5</span><i style="width:72.6%"></i><em>9.450%</em></div>
<div class="pf-bar new"><span>0.8.0</span><i style="width:5.1%"></i><em>0.667%</em></div>
</div>
<div class="pf-bar-row">
<div class="pf-bar-name">forty-nine hidden writers at 60Hz</div>
<div class="pf-bar old"><span>0.7.5</span><i style="width:44.6%"></i><em>5.800%</em></div>
<div class="pf-bar new"><span>0.8.0</span><i style="width:34.1%"></i><em>4.433%</em></div>
</div>
</div>
</div>
<figcaption class="pf-cap">The last row is the one to look at. macOS followed the same shape, from 22.958 percent to 1.673 on the hidden output case and from 14.857 to 13.498 on the last one.</figcaption>
</figure>
On 0.7.5, adding hidden background output to a session cost 8.7 CPU points on Linux and 12.7 on macOS. On master the same activity costs 0.2 and 0.6, which is roughly the price of reading and parsing the bytes and nothing else.
## Where it does not help
Fifty panes, forty-nine of them writing at 60Hz, is about 2,940 terminal updates per second. Every one of them still has to be read from the PTY and parsed into terminal state, because that is what makes the pane correct when you switch to it. Suppressing frames cannot suppress terminal emulation.
That case improved 23.6 percent on Linux and 9.1 on macOS. Visible output at 60Hz, a pane you are actually watching, improved 33.9 percent on Linux and 2.7 on macOS, which is to say it did not regress. Idle sat at the measurement floor on both builds and deserves no percentage claim at all.
None of those are the headline. All of them are the point. The gains are large exactly where the work was unnecessary and small exactly where it was not.
## How this was measured
Official 0.7.5 release binaries against hashed master builds, on separate Linux and macOS machines, giving 54 isolated observations per operating system. Every run used a fresh named session, real attached clients in fixed 86 by 47 terminals, a five second warmup and twenty one second samples. The figures are total CPU for the Herdr server plus every attached client.
Every headline result also has a behavioural counter behind it rather than only a CPU total: scheduled frames per second for the sidebar, hidden source skips for background output, exact packet delivery counts for mouse motion. A CPU number on its own can be explained by a dozen things. A CPU number with a matching mechanism counter is harder to argue with.
One limit worth stating plainly. This compares a shipped release against master, so intervening changes and build environments are part of what is being compared. The profiler evidence matches the mechanisms closely, but not every CPU point can be assigned to one commit.
None of this started in a profiler. It started with people saying the sidebar was too busy, and with bug reports and performance reports from people running far stranger sessions than we test. Keep them coming. Thank you to everyone helping make Herdr better.
+115 -9
View File
@@ -1,11 +1,13 @@
{
"schemaVersion": 1,
"generatedAt": "2026-06-20T12:00:00.000Z",
"pluginCount": 10,
"repositoryCount": 7,
"source": {
"provider": "github",
"query": "topic:herdr-plugin",
"totalCount": 7,
"collectedCount": 7,
"totalCount": 8,
"collectedCount": 8,
"truncated": false
},
"plugins": [
@@ -25,7 +27,18 @@
"updatedAt": "2026-06-19T19:58:26Z",
"pushedAt": "2026-06-19T19:58:26Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "herdr-plugin.toml",
"id": "github-start",
"name": "GitHub Start",
"version": "1.4.0",
"minHerdrVersion": "0.7.0",
"description": "Start an agent from a GitHub issue, PR, or discussion.",
"platforms": ["linux", "macos"]
}
]
},
{
"id": 223456789,
@@ -43,7 +56,36 @@
"updatedAt": "2026-06-18T08:10:00Z",
"pushedAt": "2026-06-18T08:10:00Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "agent-telegram-notify/herdr-plugin.toml",
"id": "agent-telegram-notify",
"name": "Telegram Notify",
"version": "0.3.1",
"minHerdrVersion": "0.7.0",
"description": "Send a Telegram message when an agent blocks or finishes.",
"platforms": ["linux", "macos"]
},
{
"path": "link-preview/herdr-plugin.toml",
"id": "link-preview",
"name": "Link Preview",
"version": "0.2.0",
"minHerdrVersion": "0.7.0",
"description": "Preview a URL from pane output without leaving the terminal.",
"platforms": ["linux", "macos"]
},
{
"path": "worktree-bootstrap/herdr-plugin.toml",
"id": "worktree-bootstrap",
"name": "Worktree Bootstrap",
"version": "0.5.0",
"minHerdrVersion": "0.7.0",
"description": "Create a worktree, install dependencies, and open a layout in one step.",
"platforms": ["linux", "macos", "windows"]
}
]
},
{
"id": 323456789,
@@ -61,7 +103,18 @@
"updatedAt": "2026-06-17T14:02:00Z",
"pushedAt": "2026-06-17T14:02:00Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "herdr-plugin.toml",
"id": "lindqvist.board",
"name": "Board",
"version": "2.1.0",
"minHerdrVersion": "0.7.5",
"description": "A live project board pane that tracks agent tasks across worktrees.",
"platforms": ["linux", "macos"]
}
]
},
{
"id": 423456789,
@@ -79,7 +132,18 @@
"updatedAt": "2026-06-15T09:30:00Z",
"pushedAt": "2026-06-15T09:30:00Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "herdr-plugin.toml",
"id": "layout-presets",
"name": "Layout Presets",
"version": "0.9.2",
"minHerdrVersion": "0.7.0",
"description": "Apply opinionated split-pane layouts per project with one keybinding.",
"platforms": ["linux", "macos"]
}
]
},
{
"id": 523456789,
@@ -97,7 +161,27 @@
"updatedAt": "2026-06-10T11:45:00Z",
"pushedAt": "2026-06-10T11:45:00Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "plugin/herdr-plugin.toml",
"id": "jira-bridge",
"name": "Jira Bridge",
"version": "0.4.0",
"minHerdrVersion": "0.7.0",
"description": "Open Jira tickets as workspaces and push agent summaries back as comments.",
"platforms": ["linux", "macos"]
},
{
"path": "plugin-cloud/herdr-plugin.toml",
"id": "jira-bridge-cloud",
"name": "Jira Bridge Cloud",
"version": "0.4.0",
"minHerdrVersion": "0.7.0",
"description": "The Jira Cloud variant of the bridge.",
"platforms": ["linux", "macos"]
}
]
},
{
"id": 623456789,
@@ -115,7 +199,18 @@
"updatedAt": "2026-06-08T16:20:00Z",
"pushedAt": "2026-06-08T16:20:00Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "herdr-plugin.toml",
"id": "notify",
"name": "Notify",
"version": "1.0.0",
"minHerdrVersion": "0.7.0",
"description": "Desktop and mobile notifications when an agent finishes, blocks, or needs input.",
"platforms": ["linux", "macos", "windows"]
}
]
},
{
"id": 723456789,
@@ -133,7 +228,18 @@
"updatedAt": "2026-06-14T10:00:00Z",
"pushedAt": "2026-06-14T10:00:00Z",
"archived": false,
"fork": false
"fork": false,
"manifests": [
{
"path": "herdr-plugin.toml",
"id": "snippet-runner",
"name": "snippet-runner",
"version": "0.1.0",
"minHerdrVersion": "0.7.0",
"description": null,
"platforms": ["linux"]
}
]
}
]
}
+36 -33
View File
@@ -1,18 +1,16 @@
---
import { getCollection, render } from 'astro:content';
import MarketingLayout from '../../components/MarketingLayout.astro';
import SiteLayout from '../../components/SiteLayout.astro';
import AuthorCard from '../../components/AuthorCard.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
/* dev only, see blog/index.astro */
return posts
.filter((post) => !post.data.draft)
.filter((post) => import.meta.env.DEV || !post.data.draft)
.map((post) => {
const slug = post.id.replace(/\.(md|mdx)$/i, '');
return {
params: { slug },
props: { post, slug },
};
return { params: { slug }, props: { post, slug } };
});
}
@@ -21,7 +19,7 @@ const { Content, headings } = await render(post);
const sectionHeadings = headings.filter((heading) => heading.depth === 2);
---
<MarketingLayout
<SiteLayout
title={`${post.data.title} — Herdr blog`}
description={post.data.description}
canonicalPath={`/blog/${slug}/`}
@@ -29,9 +27,33 @@ const sectionHeadings = headings.filter((heading) => heading.depth === 2);
ogImage={post.data.ogImage}
ogImageAlt={post.data.ogImage ? post.data.title : undefined}
>
<main class="content-shell">
<div class="article-layout section">
<aside class="article-toc" aria-label="Article sections">
{/* a post with no h2s renders no sidebar, so the grid must stop reserving
a column for it */}
<div class:list={['article', sectionHeadings.length === 0 && 'article-solo']}>
<article class="article-body">
<p class="article-meta">
<a href="/blog/">← Blog</a>
<time datetime={post.data.date.toISOString()}>
{post.data.date.toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
</p>
<h1>{post.data.title}</h1>
{post.data.lede && post.data.description && (
<p class="article-lede">{post.data.description}</p>
)}
<hr class="article-rule" />
<div class="prose">
<Content />
</div>
<AuthorCard />
</article>
{sectionHeadings.length > 0 && (
<aside class="toc" aria-label="Article sections">
<p>Sections</p>
<nav>
{sectionHeadings.map((heading) => (
@@ -39,30 +61,11 @@ const sectionHeadings = headings.filter((heading) => heading.depth === 2);
))}
</nav>
</aside>
<article class="article-shell">
<header class="article-header">
<p class="post-meta">
<a href="/blog/">Blog</a>
<time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}</time>
</p>
<h1>{post.data.title}</h1>
<p>{post.data.description}</p>
</header>
<div class="article-prose">
<Content />
</div>
<AuthorCard />
</article>
</div>
</main>
)}
</div>
<script>
const headings = Array.from(document.querySelectorAll('.article-prose h2[id]'));
const headings = Array.from(document.querySelectorAll('.prose h2[id]'));
const links = new Map(
Array.from(document.querySelectorAll('[data-heading-link]')).map((link) => [
link.getAttribute('data-heading-link'),
@@ -89,4 +92,4 @@ const sectionHeadings = headings.filter((heading) => heading.depth === 2);
for (const heading of headings) observer.observe(heading);
}
</script>
</MarketingLayout>
</SiteLayout>
+30 -26
View File
@@ -1,38 +1,42 @@
---
import { getCollection } from 'astro:content';
import MarketingLayout from '../../components/MarketingLayout.astro';
import SiteLayout from '../../components/SiteLayout.astro';
/* Drafts render locally and never in a build, so an unpublished post can be
reviewed on the dev server without being one deploy away from going live. */
const posts = (await getCollection('blog'))
.filter((post) => !post.data.draft)
.filter((post) => import.meta.env.DEV || !post.data.draft)
.map((post) => ({ ...post, slug: post.id.replace(/\.(md|mdx)$/i, '') }))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const stamp = (date) =>
date.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' });
---
<MarketingLayout
<SiteLayout
title="Herdr blog"
description="Notes on terminal-native agent workflows and the engineering behind Herdr."
description="Notes on the agent runtime — product decisions, implementation writeups, and what we learned running agents on machines nobody is watching."
canonicalPath="/blog/"
>
<main class="content-shell">
<section class="page-hero page-hero-compact">
<p class="eyebrow"><span class="status-dot dot-working"></span>Blog</p>
<h1>Notes from Herdr.</h1>
<p class="hero-lede">
Product notes and implementation writeups for the terminal-native agent multiplexer.
</p>
</section>
<header class="ph">
<div class="hd-eyebrow"><s></s>the blog</div>
<h1>Notes from the herd.</h1>
<p>
Product decisions, implementation writeups, and what we learn running agents on
machines nobody is watching.
</p>
</header>
<section class="section section-narrow" aria-label="Blog posts">
<div class="post-list">
{posts.map((post) => (
<article class="post-card">
<a class="post-card-link" href={`/blog/${post.slug}/`}>
<h2>{post.data.title}</h2>
<span aria-hidden="true">→</span>
</a>
</article>
))}
</div>
</section>
</main>
</MarketingLayout>
<div class="posts">
{posts.map((post) => (
<a href={`/blog/${post.slug}/`}>
<time datetime={post.data.date.toISOString()}>{stamp(post.data.date)}</time>
<div>
<h2>{post.data.title}</h2>
{post.data.description && <p>{post.data.description}</p>}
</div>
<span class="go-arrow" aria-hidden="true">→</span>
</a>
))}
</div>
</SiteLayout>
+4 -4
View File
@@ -19,10 +19,10 @@
content="Herdr combines tmux-style persistence, real terminal panes, agent state, direct attach, SSH workflows, and an API agents can drive."
/>
<meta property="og:url" content="https://herdr.dev/compare/" />
<meta property="og:image" content="https://herdr.dev/assets/og-card-v8.png" />
<meta property="og:image" content="https://herdr.dev/assets/og-card-v9.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Herdr — One terminal. The whole herd." />
<meta property="og:image:alt" content="Herdr — run them anywhere, leave them running." />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Compare Herdr — terminal-native agent multiplexer" />
@@ -30,8 +30,8 @@
name="twitter:description"
content="Compare Herdr with tmux, Zellij, cmux, Warp, Solo, Conductor, Emdash, and Superset."
/>
<meta name="twitter:image" content="https://herdr.dev/assets/og-card-v8.png" />
<meta name="twitter:image:alt" content="Herdr — One terminal. The whole herd." />
<meta name="twitter:image" content="https://herdr.dev/assets/og-card-v9.png" />
<meta name="twitter:image:alt" content="Herdr — run them anywhere, leave them running." />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon.png?v=14" />
<link rel="apple-touch-icon" href="/assets/logo.png" />
+27 -1
View File
@@ -1,7 +1,33 @@
---
import { readFile } from 'node:fs/promises';
import { agentCount, fmt, releaseVersion, siteStats } from '../site-data';
/* The numbers in the nav and hero strip are baked here at build time from the
snapshots and latest.json, so the page ships with real values, renders
correctly without JS, and can never be more than one deploy stale. A small
script on the page refreshes the counts live afterwards. */
const homepage = await readFile(new URL('../../index.html', import.meta.url), 'utf8');
const { starCount, installCount, pluginCount } = await siteStats();
const version = await releaseVersion();
const facts = version ? `v${version} · Apache 2.0` : 'Apache 2.0';
/* the line drops out entirely rather than showing a placeholder if the
manifests can't be read — a missing sentence beats a wrong number */
const agents = await agentCount();
const agentsNote = agents ? `${agents} agents detected out of the box` : '';
/* the strip carried a hardcoded 19; same source as row 04 now */
const agentsCount = agents ? String(agents) : '—';
/* replaceAll, not replace: the star count appears in the nav as well as the
strip, and replace() would fill only the first of them. */
const page = homepage
.replaceAll('<!--N_STARS-->', fmt(starCount))
.replaceAll('<!--N_INSTALLS-->', fmt(installCount))
.replaceAll('<!--N_PLUGINS-->', fmt(pluginCount))
.replaceAll('<!--FACTS-->', facts)
.replaceAll('<!--AGENTS_NOTE-->', agentsNote)
.replaceAll('<!--N_AGENTS-->', agentsCount);
---
<Fragment set:html={homepage} />
<Fragment set:html={page} />
+60
View File
@@ -0,0 +1,60 @@
<!doctype html>
<html lang="en" class="og">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>herdr OG card — source</title>
<meta name="robots" content="noindex" />
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/og-card.css?v=3" />
<style>
/* Source of truth for /assets/og-card-v9.png.
Regenerate: open this page, capture the full document, crop to the
leftmost 1200x630 (the document is pinned to exactly the card size, but
the browser viewport may be wider). Bump the filename when the artwork
changes — Slack, X and LinkedIn cache unfurls by URL for weeks. */
/* centre the headline in the band below the head rather than in the whole
card, so a four-line headline can't ride up into the lockup */
.in { display: flex; flex-direction: column; justify-content: center; padding-top: 118px; }
h1 {
font-family: var(--disp);
font-weight: 900;
letter-spacing: -0.055em;
line-height: 0.86;
font-size: 104px;
margin: 30px 0 0;
max-width: 15ch;
color: var(--ink);
}
h1 em { font-style: normal; color: var(--spot); }
/* X, LinkedIn and Slack all draw their own attribution over the bottom of an
unfurl, so the lockup and provenance sit at the top and the bottom band is
left deliberately empty. */
.head {
position: absolute;
left: 62px;
right: 62px;
top: 48px;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.head .sub { color: var(--faint); font-size: 21px; letter-spacing: 0.1em; }
.head .sub b { color: var(--dim); font-weight: 400; }
</style>
</head>
<body class="og">
<div class="card">
<span class="ram mass"></span>
<div class="head">
<div class="lockup"><span class="ram"></span><b>herdr</b></div>
<div class="sub"><b>herdr.dev</b> &nbsp;·&nbsp; Apache 2.0</div>
</div>
<div class="in pad">
<div class="eyebrow"><s></s>the agent runtime</div>
<h1>Run them <em>anywhere.</em> Leave them running.</h1>
</div>
</div>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
<!doctype html>
<html lang="en" class="og">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>herdr performance OG card — source</title>
<meta name="robots" content="noindex" />
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/og-card.css?v=3" />
<style>
/* Source of truth for /assets/og-blog-frames-v1.png. Same capture recipe as
card-b: open the page, capture the full document, it is already 1200x630.
A share card is read at thumbnail size in a timeline, so it carries the
headline and the one measurement that backs it. The caveats live in the
post, where there is room to read them. */
.head {
position: absolute; left: 62px; right: 62px; top: 48px; z-index: 1;
display: flex; align-items: center; justify-content: space-between;
}
.head .sub { color: var(--faint); font-size: 21px; letter-spacing: 0.1em; }
.head .sub b { color: var(--dim); font-weight: 400; }
.in { display: flex; flex-direction: column; justify-content: center; padding: 118px 62px 54px; }
h1 {
font-family: var(--disp); font-weight: 900; letter-spacing: -0.055em;
line-height: 0.9; font-size: 96px; margin: 0; color: var(--ink);
}
h1 em { font-style: normal; color: var(--spot); }
.meas {
display: flex; align-items: baseline; gap: 22px;
margin-top: 42px; padding-top: 22px; border-top: 1px solid var(--line2);
color: var(--faint); font-size: 22px; letter-spacing: 0.04em;
}
.meas b { color: var(--ink); font-weight: 400; }
.meas .r { margin-left: auto; color: var(--dim); }
</style>
</head>
<body class="og">
<div class="card">
<span class="ram mass"></span>
<div class="head">
<div class="lockup"><span class="ram"></span><b>herdr</b></div>
<div class="sub"><b>herdr.dev</b> &nbsp;·&nbsp; Apache 2.0</div>
</div>
<div class="in">
<h1>Ten agents,<br />three clients,<br /><em>95% less CPU.</em></h1>
<div class="meas">
<span>total cpu <b>13.014% → 0.617%</b></span>
<span class="r">v0.7.5 → v0.8.0</span>
</div>
</div>
</div>
</body>
</html>
+156 -33
View File
@@ -1,5 +1,5 @@
---
import MarketingLayout from '../components/MarketingLayout.astro';
import SiteLayout from '../components/SiteLayout.astro';
import fixture from '../data/plugins-fixture.json';
const snapshotUrl = 'https://assets.herdr.dev/plugins/index.json';
@@ -17,29 +17,45 @@ try {
}
const initialData = liveData ?? (import.meta.env.DEV ? fixture : null);
const initialIsFixture = !liveData && import.meta.env.DEV;
// A card is a repository; a plugin is a validated manifest inside it. A monorepo
// with three manifests is one card and three plugins, so the two counts differ
// and both are stated. `source.totalCount` is the raw GitHub topic search and
// still includes blacklisted and manifest-less repositories, so it is not used.
const repos = initialData?.plugins ?? [];
const repositoryCount = initialData?.repositoryCount ?? repos.length;
const pluginCount =
initialData?.pluginCount ??
repos.reduce((n, repo) => n + (repo.manifests?.length || 1), 0);
const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
---
<MarketingLayout
<SiteLayout
title="Herdr plugins: community plugin marketplace"
description="Discover community Herdr plugins published on GitHub. An automatic index of public repositories tagged with the herdr-plugin topic."
canonicalPath="/plugins/"
>
<main class="plugins-page">
<section class="section plugins-hero">
<p class="section-kicker"># marketplace</p>
<h2>Plugins built on Herdr.</h2>
<p class="plugins-lede">
Community plugins published on GitHub. Install any of them with
<header class="ph ph-split">
<div>
<div class="hd-eyebrow"><s></s>the marketplace</div>
<h1>Built by the herd.</h1>
<p>
Community plugins published on GitHub, discovered automatically from the
<code>herdr-plugin</code> topic. Install any of them with
<code>herdr plugin install</code>, or publish your own and get listed.
</p>
<p class="plugins-trust">
<span class="status-dot dot-idle"></span>
Listings are discovered automatically from the GitHub topic
<code>herdr-plugin</code>. They are not reviewed by Herdr, so install at
your own discretion. <a href="/docs/plugins/#trust-and-security">Trust guidance</a>.
Listings aren't reviewed by Herdr, so install at your own discretion.
<a href="/docs/plugins/#trust-and-security">Trust guidance →</a>
</p>
</section>
</div>
<aside class="ph-side">
<b>{pluginCount.toLocaleString('en-US')}</b>
<span>plugins across {repositoryCount.toLocaleString('en-US')} repositories</span>
<a href="/docs/plugins/">Publish yours →</a>
</aside>
</header>
<section class="section plugins-controls">
<div class="plugins-search-wrap">
@@ -48,7 +64,7 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
id="plugins-search"
type="search"
class="plugins-search"
placeholder="search plugins, owners, languages…"
placeholder="search plugins, repositories, owners, languages…"
autocomplete="off"
spellcheck="false"
aria-label="Search plugins"
@@ -63,6 +79,9 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
<option value="name">Name</option>
</select>
</label>
</section>
<section class="section plugins-metabar">
<p id="plugins-meta" class="plugins-meta" aria-live="polite"></p>
</section>
@@ -98,8 +117,8 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
const moreButton = document.getElementById("plugins-more");
// 24 fills complete rows in both the 3-column and 2-column grid layouts.
const SHOW_LIMIT = 24;
let showAll = false;
const PAGE_SIZE = 24;
let visibleLimit = PAGE_SIZE;
const LANG_COLORS = {
TypeScript: "#3178c6",
@@ -113,7 +132,8 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
Ruby: "#701516",
};
let plugins = [];
// One entry per repository. Its `manifests` are the plugins it ships.
let repos = [];
let usingFallback = false;
function timeAgo(iso) {
@@ -188,23 +208,58 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
meta.appendChild(lang);
}
// Nearly every repository ships exactly one plugin whose name restates the
// repository name, so only its version earns a place in the meta row.
const manifests = manifestsOf(p);
if (manifests.length === 1 && manifests[0].version) {
meta.appendChild(el("span", "plugin-stat plugin-version", "v" + manifests[0].version));
}
const updated = timeAgo(p.pushedAt);
if (updated) meta.appendChild(el("span", "plugin-stat plugin-updated", "updated " + updated));
card.appendChild(meta);
// A monorepo is the case the card title can't answer on its own, so it
// gets the extra row: how many plugins, and which.
if (manifests.length > 1) {
const ships = el("div", "plugin-ships");
ships.appendChild(el("span", "plugin-ship-count", manifests.length + " plugins"));
ships.appendChild(
el("span", "plugin-ship-list", manifests.map(manifestLabel).join(" · ")),
);
card.appendChild(ships);
}
return card;
}
function manifestsOf(p) {
return Array.isArray(p.manifests) ? p.manifests : [];
}
function manifestLabel(m) {
return m.name || m.id || m.path || "plugin";
}
function matches(p, q) {
if (!q) return true;
const hay = [p.fullName, p.owner, p.name, p.description, p.language, ...(p.topics || [])]
const hay = [
p.fullName,
p.owner,
p.name,
p.description,
p.language,
...(p.topics || []),
...manifestsOf(p).flatMap((m) => [m.name, m.id, m.description]),
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return q.split(/\s+/).every((term) => hay.includes(term));
}
function sortPlugins(list, mode) {
function sortRepos(list, mode) {
const by = [...list];
const t = (v) => new Date(v || 0).getTime() || 0;
if (mode === "recent") by.sort((a, b) => t(b.pushedAt) - t(a.pushedAt));
@@ -219,16 +274,18 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
function render() {
const q = searchInput.value.trim().toLowerCase();
const visible = sortPlugins(plugins.filter((p) => matches(p, q)), sortSelect.value);
const shown = showAll ? visible : visible.slice(0, SHOW_LIMIT);
const visible = sortRepos(repos.filter((p) => matches(p, q)), sortSelect.value);
const shown = visible.slice(0, visibleLimit);
grid.replaceChildren();
for (const p of shown) grid.appendChild(buildCard(p));
const hiddenCount = visible.length - shown.length;
moreButton.hidden = hiddenCount === 0;
if (hiddenCount > 0) {
moreButton.textContent = "show " + hiddenCount + " more";
// Grow a page at a time, and name the real remainder on the last step so
// the button never promises 24 and delivers 5.
const remaining = visible.length - shown.length;
moreButton.hidden = remaining === 0;
if (remaining > 0) {
moreButton.textContent = "show " + Math.min(PAGE_SIZE, remaining) + " more";
}
if (visible.length === 0) {
@@ -241,23 +298,28 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
}
}
function setMeta(generatedAt) {
function setMeta(data) {
const parts = [];
parts.push(plugins.length + (plugins.length === 1 ? " plugin" : " plugins"));
const ago = timeAgo(generatedAt);
const repoCount = (data && data.repositoryCount) ?? repos.length;
const total =
(data && data.pluginCount) ??
repos.reduce((n, repo) => n + (manifestsOf(repo).length || 1), 0);
parts.push(total + (total === 1 ? " plugin" : " plugins"));
parts.push(repoCount + (repoCount === 1 ? " repository" : " repositories"));
const ago = timeAgo(data && data.generatedAt);
if (ago) parts.push("updated " + ago);
if (usingFallback) parts.push("preview data");
metaEl.textContent = parts.join(" · ");
}
function apply(data) {
plugins = Array.isArray(data && data.plugins) ? data.plugins : [];
setMeta(data && data.generatedAt);
repos = Array.isArray(data && data.plugins) ? data.plugins : [];
setMeta(data);
render();
}
function showError() {
plugins = [];
repos = [];
metaEl.textContent = "";
grid.replaceChildren();
moreButton.hidden = true;
@@ -288,10 +350,16 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
}
}
searchInput.addEventListener("input", render);
sortSelect.addEventListener("change", render);
// A new query or order is a new list, so paging starts over.
function resetAndRender() {
visibleLimit = PAGE_SIZE;
render();
}
searchInput.addEventListener("input", resetAndRender);
sortSelect.addEventListener("change", resetAndRender);
moreButton.addEventListener("click", () => {
showAll = true;
visibleLimit += PAGE_SIZE;
render();
});
load();
@@ -300,6 +368,7 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
<style is:global>
.plugins-page {
padding-bottom: 5rem;
--plugins-clear-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.5 1.5l9 9M10.5 1.5l-9 9' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
}
/* Reuse .section only for max-width centering, not its 5rem padding + top divider. */
@@ -416,6 +485,23 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
border-color: var(--accent);
}
/* Chromium paints the native clear button in the OS accent colour, which
belongs to no ground the site defines. Redraw it from our own tokens. */
.plugins-search::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
width: 0.7rem;
height: 0.7rem;
cursor: pointer;
background: var(--muted-2);
-webkit-mask: var(--plugins-clear-icon) center / contain no-repeat;
mask: var(--plugins-clear-icon) center / contain no-repeat;
}
.plugins-search::-webkit-search-cancel-button:hover {
background: var(--ink);
}
.plugins-sort-wrap {
display: inline-flex;
align-items: center;
@@ -566,6 +652,43 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
color: var(--muted-2);
}
/* What the repository ships. Sits below the repo stats, separated by a rule,
so the repository stays the primary identity of the card. */
.plugin-ships {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.45rem;
padding-top: 0.7rem;
border-top: 1px solid var(--line);
font-family: var(--mono);
font-size: 0.78rem;
line-height: 1.5;
min-width: 0;
}
.plugin-version {
color: var(--muted-2);
}
.plugin-ship-count {
flex: 0 0 auto;
padding: 0.05rem 0.4rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
color: var(--muted-2);
}
.plugin-ship-list {
align-self: stretch;
min-width: 0;
color: var(--muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.plugins-status {
padding: 3rem 1rem;
text-align: center;
@@ -626,4 +749,4 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
}
}
</style>
</MarketingLayout>
</SiteLayout>
-270
View File
@@ -1,270 +0,0 @@
---
import MarketingLayout from '../components/MarketingLayout.astro';
---
<MarketingLayout
title="Herdr stats: live public numbers"
description="Live public stats for Herdr — GitHub stars, downloads, and website traffic, pulled from publicly verifiable sources."
canonicalPath="/stats/"
>
<main class="stats-page">
<section class="hero stats-hero">
<div class="hero-left">
<div class="hero-copy">
<p class="eyebrow">
<span class="status-dot dot-working"></span>
build in public · live numbers
</p>
<div class="hero-stat-value" id="star-count">—</div>
<div class="hero-stat-label"><span class="live-dot" aria-hidden="true"></span> GitHub stars · counting live</div>
<p class="hero-lede">
Live public stats for herdr: stars, installs, and traffic. Everything on this
page comes from sources you can check yourself, refreshed continuously.
</p>
<div class="hero-actions">
<a class="button button-primary" href="https://github.com/herdrdev/herdr">★ Star on GitHub</a>
<a class="button button-secondary" href="/docs/install/">Get herdr</a>
</div>
<p class="hero-note" id="hero-note"></p>
</div>
</div>
<div class="hero-right">
<div class="chart-card">
<div class="chart-card-title">
<span>github stars · all time</span>
<span class="when" id="stars-since"></span>
</div>
<div id="chart-stars" class="chart-slot"></div>
</div>
</div>
</section>
<div class="stats-main">
<section class="kpi-row" aria-label="Headline numbers">
<div class="kpi">
<div class="label">total installs</div>
<div class="value" id="kpi-installs">—</div>
<div class="sub" id="kpi-installs-sub"></div>
</div>
<div class="kpi">
<div class="label">daily visitors</div>
<div class="value" id="kpi-visitors">—</div>
<div class="sub">herdr.dev uniques · 7-day avg · cloudflare</div>
</div>
<div class="kpi">
<div class="label">releases shipped</div>
<div class="value" id="kpi-releases">—</div>
<div class="sub">stable + preview channels</div>
</div>
<div class="kpi">
<div class="label">days old</div>
<div class="value" id="kpi-age">—</div>
<div class="sub" id="kpi-age-sub"></div>
</div>
</section>
<section class="stats-section">
<h2>Week by week</h2>
<p class="section-sub">Weekly totals. The current week is still filling up, shown dimmed.</p>
<div class="velocity-grid">
<div class="chart-card">
<div class="chart-card-title"><span>stars / week</span></div>
<div id="chart-stars-week" class="chart-slot"></div>
</div>
<div class="chart-card">
<div class="chart-card-title"><span>downloads / week</span></div>
<div id="chart-downloads-week" class="chart-slot"></div>
</div>
<div class="chart-card">
<div class="chart-card-title"><span>avg daily visitors / week</span></div>
<div id="chart-visitors-week" class="chart-slot"></div>
</div>
</div>
</section>
<section class="stats-section">
<h2>Downloads</h2>
<p class="section-sub" id="downloads-sub"></p>
<div class="chart-card">
<div class="chart-card-title">
<span>release downloads · cumulative</span>
<span class="when">github api</span>
</div>
<div id="chart-downloads" class="chart-slot"></div>
</div>
</section>
<section class="stats-section">
<h2>Where the herd browses from</h2>
<p class="section-sub" id="traffic-sub"></p>
<div class="traffic-grid">
<div class="chart-card">
<div class="chart-card-title"><span>top countries</span><span class="when">visits</span></div>
<div id="rank-countries"></div>
</div>
<div class="chart-card">
<div class="chart-card-title"><span>top referrers</span><span class="when">visits</span></div>
<div id="rank-referrers"></div>
</div>
<div class="chart-card">
<div class="chart-card-title"><span>top pages</span><span class="when">page views</span></div>
<div id="rank-paths"></div>
</div>
</div>
</section>
<section class="stats-section">
<h2>Timeline</h2>
<p class="section-sub">From first commit to today.</p>
<div id="timeline"></div>
</section>
<p class="sources-note" id="sources-note">
Every number on this page comes from publicly verifiable sources: the
<a href="https://api.github.com/repos/herdrdev/herdr">GitHub API</a> for stars and release downloads, and
<a href="https://formulae.brew.sh/api/analytics/install/365d.json">Homebrew analytics</a> for brew installs.
Website traffic is measured by Cloudflare (unique visitors per day, herdr.dev zone).
<span id="updated-note"></span>
</p>
</div>
<script>
import { initStatsPage } from '../scripts/stats-page';
initStatsPage();
</script>
</main>
</MarketingLayout>
<style is:global>
.stats-hero { min-height: auto; }
.stats-hero .hero-left { padding: 3.5rem 3rem 3.5rem max(2.5rem, calc((100vw - var(--max)) / 2)); }
.stats-hero .hero-right { padding: 3rem max(2.5rem, calc((100vw - var(--max)) / 2)) 3rem 2.5rem; }
.stats-hero .chart-card { width: 100%; position: relative; z-index: 1; }
.hero-stat-value {
font-family: var(--mono);
font-weight: 600;
font-size: clamp(3.2rem, 7vw, 5.2rem);
line-height: 1;
letter-spacing: -0.04em;
color: var(--ink);
min-height: 1em;
}
.hero-stat-label {
display: flex; align-items: center; gap: 0.55rem;
margin-top: 0.55rem;
font-family: var(--mono); font-size: 0.95rem; color: var(--muted);
}
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: live-pulse 2.4s ease-in-out infinite; }
@keyframes live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }
.stats-main { max-width: var(--max); margin: 0 auto; padding: 0 1.5rem 4rem; }
.stats-section { margin-top: 3.5rem; }
.stats-section > h2 { font-family: var(--mono); font-weight: 400; letter-spacing: -0.02em; font-size: 1.45rem; margin: 0 0 0.35rem; color: var(--ink); }
.stats-section > .section-sub { color: var(--muted); font-size: 0.95rem; margin: 0 0 1.4rem; max-width: 62ch; }
.chart-card {
border: 1px solid var(--line); border-radius: var(--radius-lg);
background: var(--bg-elevated); padding: 1rem 1.1rem 0.8rem; position: relative;
}
.chart-card-title {
display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
font-family: var(--mono); font-size: 0.85rem; color: var(--ink-soft); margin-bottom: 0.5rem;
}
.chart-card-title .when { color: var(--muted-2); font-size: 0.78rem; }
.chart { position: relative; }
.chart svg { display: block; width: 100%; height: auto; }
.chart svg:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.chart .grid { stroke: var(--line); stroke-width: 1; }
.chart .grid.strong { stroke: var(--line-strong); }
.chart .tick { fill: var(--muted-2); font-family: var(--mono); font-size: 11px; }
.chart .line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chart .area { opacity: 0.1; }
.chart .dot { stroke: var(--bg-elevated); stroke-width: 2; }
.chart .event-dot { fill: var(--bg-elevated); stroke-width: 2; }
.chart .milestone-label { fill: var(--muted); font-family: var(--mono); font-size: 11.5px; font-weight: 600; }
.chart .milestone-leader { stroke: var(--line-strong); stroke-width: 1; }
.chart .cap-label { fill: var(--muted); font-family: var(--mono); font-size: 11.5px; font-weight: 600; }
.chart .crosshair { stroke: var(--line-strong); stroke-width: 1; }
.chart .hover-dot { stroke: var(--bg-elevated); stroke-width: 2; }
.chart .bar .hit { fill: transparent; }
.chart .bar.partial .fill { opacity: 0.45; }
.chart .bar:hover .fill, .chart .bar.active .fill { filter: brightness(1.12); }
.chart-tip {
position: absolute; pointer-events: none; z-index: 4;
background: var(--ink); color: var(--bg);
font-family: var(--mono); font-size: 0.78rem; line-height: 1.4;
padding: 0.35rem 0.55rem; border-radius: var(--radius-md);
white-space: nowrap; transform: translate(-50%, calc(-100% - 10px));
}
.chart-tip strong { font-size: 0.88rem; }
.chart-empty { color: var(--muted); font-family: var(--mono); font-size: 0.85rem; padding: 2rem 0; text-align: center; }
.chart-table { margin-top: 0.4rem; font-family: var(--mono); font-size: 0.78rem; }
.chart-table summary { color: var(--muted-2); cursor: pointer; padding: 0.25rem 0; }
.chart-table summary:hover { color: var(--muted); }
.chart-table-scroll {
max-height: 250px; overflow-y: auto; margin-top: 0.4rem;
scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent;
}
.chart-table-scroll::-webkit-scrollbar { width: 8px; }
.chart-table-scroll::-webkit-scrollbar-track { background: transparent; }
.chart-table-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; }
/* the site stylesheet styles tables globally (min-width, accent last column) — reset inside cards */
.chart-table table { width: 100%; min-width: 0; border-collapse: collapse; font-size: 0.78rem; }
.chart-table th, .chart-table td { text-align: left; padding: 0.28rem 0.6rem 0.28rem 0; border-bottom: 1px solid var(--line); color: var(--ink-soft); font-variant-numeric: tabular-nums; font-size: 0.78rem; }
.chart-table th { color: var(--muted); font-weight: 600; letter-spacing: 0.04em; }
.chart-table th:last-child, .chart-table td:last-child { background: none; color: var(--ink-soft); font-weight: 600; text-align: right; padding-right: 0; }
.chart-table tr:last-child td { border-bottom: 0; }
.kpi-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.9rem; margin-top: 3rem; }
.kpi {
border: 1px solid var(--line); border-radius: var(--radius-lg);
background: var(--bg-elevated); padding: 1rem 1.1rem;
}
.kpi .label { font-family: var(--mono); font-size: 0.78rem; color: var(--muted); }
.kpi .value { font-family: var(--mono); font-weight: 600; font-size: 1.9rem; letter-spacing: -0.02em; color: var(--ink); margin: 0.25rem 0 0.15rem; }
.kpi .sub { font-family: var(--mono); font-size: 0.75rem; color: var(--muted-2); }
.kpi .sub .up { color: var(--green); font-weight: 600; }
.velocity-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.9rem; }
.traffic-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.9rem; }
.rank-list { display: flex; flex-direction: column; gap: 0.15rem; margin-top: 0.3rem; }
.rank-row {
display: grid; grid-template-columns: minmax(0, 45%) 1fr auto;
gap: 0.7rem; align-items: center; padding: 0.28rem 0;
}
.rank-name { font-family: var(--mono); font-size: 0.8rem; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rank-bar { height: 8px; border-radius: 2px; background: color-mix(in srgb, var(--yellow) 14%, transparent); overflow: hidden; }
.rank-fill { display: block; height: 100%; border-radius: 2px; background: var(--yellow); }
.rank-value { font-family: var(--mono); font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.milestone-term {
background: var(--term-bg); border: 1px solid var(--term-border); border-radius: var(--radius-lg);
font-family: var(--mono); font-size: 0.92rem; padding: 1.1rem 1.3rem; max-width: 560px;
}
.term-line { display: flex; gap: 1.2rem; padding: 0.22rem 0; align-items: baseline; }
.term-cmd-line { margin-bottom: 0.5rem; gap: 0.6rem; }
.term-prompt { color: var(--term-prompt); }
.term-cmd { color: var(--term-text); }
.term-date { color: var(--term-dim); flex: 0 0 4.4em; }
.term-text { color: var(--term-text); flex: 1; }
.term-note { color: var(--term-muted); }
.term-line:last-child .term-text { color: var(--term-green-bright); font-weight: 600; }
.term-line:last-child .term-note { color: var(--term-green-bright); }
.sources-note { margin-top: 3.5rem; padding-top: 1.4rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 0.88rem; max-width: 72ch; }
.sources-note a { color: var(--accent); }
.stats-stale {
margin-top: 1rem; font-family: var(--mono); font-size: 0.8rem; color: var(--yellow);
}
@media (max-width: 900px) {
.kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.velocity-grid, .traffic-grid { grid-template-columns: minmax(0, 1fr); }
}
</style>
-742
View File
@@ -1,742 +0,0 @@
// Client renderer for /stats/. Fetches the public stats snapshot from R2 and
// renders every number and chart client-side, so the static page never goes stale.
const STATS_URL = "https://assets.herdr.dev/stats/stats.json";
const REPO_API_URL = "https://api.github.com/repos/herdrdev/herdr";
const LINE_H = 320;
const BAR_W = 420;
const BAR_H = 240;
const STAR_MILESTONES = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000];
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
type SeriesPoint = { d: string; v: number };
type StatsJson = {
schemaVersion: number;
generatedAt: string;
repo: string;
bornDay: string;
stars: { total: number; daily: SeriesPoint[] };
downloads: { github: number; brew: number; daily: SeriesPoint[] };
releases: { shipped: number };
traffic: {
windowDays: number;
daily: SeriesPoint[];
topCountries: Array<{ code: string; visits: number }>;
topReferrers: Array<{ host: string; visits: number }>;
topPaths: Array<{ path: string; views: number }>;
};
timeline: Array<{ d: string; label: string; note?: string; badge?: string }>;
};
type WeekPoint = { weekStart: string; value: number; partial: boolean; daysIn: number };
type Milestone = { day: string; value: number; label: string };
// ---------- formatting ----------
function fmtInt(value: number): string {
return Math.round(value).toLocaleString("en-US");
}
function fmtCompact(value: number): string {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value % 1_000_000 === 0 ? 0 : 1)}M`;
if (value >= 10_000) return `${Math.round(value / 1_000)}k`;
if (value >= 1_000) {
const scaled = value / 1_000;
return `${Number.isInteger(scaled) ? scaled : scaled.toFixed(1)}k`;
}
return String(Math.round(value));
}
function fmtDay(day: string): string {
const date = new Date(`${day}T00:00:00Z`);
return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
}
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
// ---------- date/series math ----------
function dayEpoch(day: string): number {
return Date.parse(`${day}T00:00:00Z`);
}
function daysBetween(fromDay: string, toDay: string): number {
return Math.round((dayEpoch(toDay) - dayEpoch(fromDay)) / 86_400_000);
}
function todayUtc(): string {
return new Date().toISOString().slice(0, 10);
}
function weekStartOf(day: string): string {
const date = new Date(`${day}T00:00:00Z`);
const sinceMonday = (date.getUTCDay() + 6) % 7;
return new Date(date.getTime() - sinceMonday * 86_400_000).toISOString().slice(0, 10);
}
function weekMeta(weekStart: string, lastDataDay: string | null): { partial: boolean; daysIn: number } {
if (!lastDataDay) return { partial: false, daysIn: 7 };
const weekEnd = new Date(dayEpoch(weekStart) + 6 * 86_400_000).toISOString().slice(0, 10);
const partial = lastDataDay < weekEnd || weekEnd >= todayUtc();
const daysIn = partial ? Math.min(daysBetween(weekStart, lastDataDay) + 1, 7) : 7;
return { partial, daysIn };
}
function deltasFromCumulative(daily: SeriesPoint[]): SeriesPoint[] {
const deltas: SeriesPoint[] = [];
for (let index = 1; index < daily.length; index += 1) {
deltas.push({ d: daily[index].d, v: Math.max(daily[index].v - daily[index - 1].v, 0) });
}
return deltas;
}
function weeklySums(days: SeriesPoint[], lastDataDay: string | null): WeekPoint[] {
const byWeek = new Map<string, number>();
for (const point of days) {
const week = weekStartOf(point.d);
byWeek.set(week, (byWeek.get(week) ?? 0) + point.v);
}
return [...byWeek.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([weekStart, value]) => ({ weekStart, value, ...weekMeta(weekStart, lastDataDay) }));
}
function weeklyAverages(days: SeriesPoint[], lastDataDay: string | null): WeekPoint[] {
const byWeek = new Map<string, { sum: number; count: number }>();
for (const point of days) {
const week = weekStartOf(point.d);
const bucket = byWeek.get(week) ?? { sum: 0, count: 0 };
bucket.sum += point.v;
bucket.count += 1;
byWeek.set(week, bucket);
}
return [...byWeek.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([weekStart, bucket]) => ({
weekStart,
value: Math.round(bucket.sum / bucket.count),
...weekMeta(weekStart, lastDataDay),
}));
}
function starMilestones(daily: SeriesPoint[]): Milestone[] {
const milestones: Milestone[] = [];
let next = 0;
for (const point of daily) {
while (next < STAR_MILESTONES.length && point.v >= STAR_MILESTONES[next]) {
const value = STAR_MILESTONES[next];
milestones.push({ day: point.d, value, label: value >= 1_000 ? `${value / 1_000}k` : String(value) });
next += 1;
}
}
return milestones;
}
function niceTicks(max: number, count = 4): number[] {
if (max <= 0) return [0];
const rawStep = max / count;
const power = 10 ** Math.floor(Math.log10(rawStep));
const step = [1, 2, 2.5, 5, 10].map((mult) => mult * power).find((candidate) => candidate >= rawStep) ?? rawStep;
const ticks: number[] = [];
for (let tick = 0; tick <= max; tick += step) ticks.push(tick);
return ticks;
}
function monthStartsBetween(firstDay: string, lastDay: string): string[] {
const starts: string[] = [];
const last = dayEpoch(lastDay);
const cursor = new Date(`${firstDay.slice(0, 7)}-01T00:00:00Z`);
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
while (cursor.getTime() <= last) {
starts.push(cursor.toISOString().slice(0, 10));
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
}
return starts;
}
// ---------- chart builders (SVG strings) ----------
type LineChartOptions = {
points: SeriesPoint[];
colorVar: string;
unit: string;
width: number;
milestones?: Milestone[];
events?: Array<{ day: string; label: string }>;
};
function lineChart(options: LineChartOptions): string {
const { points, width } = options;
if (points.length < 2) return `<p class="chart-empty">not enough data yet</p>`;
const margin = { top: 34, right: 26, bottom: 30, left: 54 };
const innerW = width - margin.left - margin.right;
const innerH = LINE_H - margin.top - margin.bottom;
const x0 = dayEpoch(points[0].d);
const x1 = dayEpoch(points[points.length - 1].d);
const yMaxData = Math.max(...points.map((point) => point.v));
const ticks = niceTicks(yMaxData);
const yMax = Math.max(ticks[ticks.length - 1], yMaxData) * 1.04;
const xFor = (day: string): number => margin.left + ((dayEpoch(day) - x0) / (x1 - x0)) * innerW;
const yFor = (value: number): number => margin.top + innerH - (value / yMax) * innerH;
const coords = points.map((point) => ({ x: xFor(point.d), y: yFor(point.v), day: point.d, value: point.v }));
const linePath = coords.map((c, i) => `${i === 0 ? "M" : "L"}${c.x.toFixed(1)},${c.y.toFixed(1)}`).join("");
const baseline = margin.top + innerH;
const areaPath = `${linePath}L${coords[coords.length - 1].x.toFixed(1)},${baseline}L${coords[0].x.toFixed(1)},${baseline}Z`;
const grid = ticks
.map((tick) => {
const y = yFor(tick).toFixed(1);
return `<line class="grid" x1="${margin.left}" y1="${y}" x2="${width - margin.right}" y2="${y}"/>` +
`<text class="tick" x="${margin.left - 8}" y="${y}" dy="0.32em" text-anchor="end">${fmtCompact(tick)}</text>`;
})
.join("");
const monthLabels = monthStartsBetween(points[0].d, points[points.length - 1].d)
.map((day) => {
const x = xFor(day).toFixed(1);
return `<text class="tick" x="${x}" y="${baseline + 20}" text-anchor="middle">${MONTHS[new Date(`${day}T00:00:00Z`).getUTCMonth()]}</text>`;
})
.join("");
const milestoneMarks = (options.milestones ?? [])
.map((milestone) => {
const x = xFor(milestone.day);
const y = yFor(milestone.value);
return (
`<line class="milestone-leader" x1="${x.toFixed(1)}" y1="${(y - 9).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(y - 18).toFixed(1)}"/>` +
`<circle class="dot" cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="4.5" style="fill:var(${options.colorVar})"/>` +
`<text class="milestone-label" x="${x.toFixed(1)}" y="${(y - 23).toFixed(1)}" text-anchor="middle">${milestone.label}</text>`
);
})
.join("");
const events = (options.events ?? []).map((event) => {
const x = xFor(event.day);
const nearest = coords.reduce((best, c) => (Math.abs(c.x - x) < Math.abs(best.x - x) ? c : best), coords[0]);
return { ...event, dotX: nearest.x, dotY: nearest.y };
});
const eventBaseX = Math.min(...events.map((event) => event.dotX), Infinity) - 12;
const eventMarks = events
.map((event, index) => {
const labelY = margin.top + 14 + index * 16;
return (
`<line class="milestone-leader" x1="${(eventBaseX + 4).toFixed(1)}" y1="${(labelY + 4).toFixed(1)}" x2="${(event.dotX - 2).toFixed(1)}" y2="${(event.dotY - 6).toFixed(1)}"/>` +
`<circle class="event-dot" cx="${event.dotX.toFixed(1)}" cy="${event.dotY.toFixed(1)}" r="4" style="stroke:var(${options.colorVar})"/>` +
`<text class="milestone-label" x="${eventBaseX.toFixed(1)}" y="${labelY.toFixed(1)}" text-anchor="end">${escapeHtml(event.label)}</text>`
);
})
.join("");
const end = coords[coords.length - 1];
const endDot = `<circle class="dot" cx="${end.x.toFixed(1)}" cy="${end.y.toFixed(1)}" r="4.5" style="fill:var(${options.colorVar})"/>`;
// escape < so the JSON can never terminate the wrapping <script> tag
const data = JSON.stringify({
unit: options.unit,
width,
points: coords.map((c) => ({ x: Number(c.x.toFixed(1)), y: Number(c.y.toFixed(1)), day: c.day, value: c.value })),
}).replace(/</g, "\\u003c");
return `
<div class="chart">
<svg viewBox="0 0 ${width} ${LINE_H}" role="img" aria-label="${options.unit} over time" tabindex="0">
${grid}
${monthLabels}
<path class="area" d="${areaPath}" style="fill:var(${options.colorVar})"/>
<path class="line" d="${linePath}" style="stroke:var(${options.colorVar})"/>
${milestoneMarks}
${eventMarks}
${endDot}
<line class="crosshair" y1="${margin.top}" y2="${baseline}" x1="-10" x2="-10" hidden></line>
<circle class="hover-dot" r="4.5" cx="-10" cy="-10" style="fill:var(${options.colorVar})" hidden></circle>
</svg>
<div class="chart-tip" hidden></div>
<script type="application/json">${data}</script>
</div>`;
}
type BarChartOptions = {
weeks: WeekPoint[];
colorVar: string;
unit: string;
maxWeeks?: number;
};
function barChart(options: BarChartOptions): string {
const weeks = options.weeks.slice(-(options.maxWeeks ?? 14));
if (weeks.length === 0) return `<p class="chart-empty">not enough data yet</p>`;
const margin = { top: 26, right: 12, bottom: 30, left: 44 };
const innerW = BAR_W - margin.left - margin.right;
const innerH = BAR_H - margin.top - margin.bottom;
const yMaxData = Math.max(...weeks.map((week) => week.value));
const ticks = niceTicks(yMaxData, 3);
const yMax = Math.max(ticks[ticks.length - 1], yMaxData) * 1.06;
const yFor = (value: number): number => margin.top + innerH - (value / yMax) * innerH;
const baseline = margin.top + innerH;
const band = innerW / weeks.length;
const barWidth = Math.min(24, Math.max(band - 2, 4));
const grid = ticks
.filter((tick) => tick > 0)
.map((tick) => {
const y = yFor(tick).toFixed(1);
return `<line class="grid" x1="${margin.left}" y1="${y}" x2="${BAR_W - margin.right}" y2="${y}"/>` +
`<text class="tick" x="${margin.left - 7}" y="${y}" dy="0.32em" text-anchor="end">${fmtCompact(tick)}</text>`;
})
.join("");
const maxValue = Math.max(...weeks.filter((week) => !week.partial).map((week) => week.value), 0);
const labelStep = Math.ceil(weeks.length / 5);
const bars = weeks
.map((week, index) => {
const xCenter = margin.left + band * index + band / 2;
const x = xCenter - barWidth / 2;
const yTop = yFor(week.value);
const r = Math.min(4, barWidth / 2, Math.max(baseline - yTop, 0));
const path =
`M${x.toFixed(1)},${baseline}` +
`L${x.toFixed(1)},${(yTop + r).toFixed(1)}` +
`Q${x.toFixed(1)},${yTop.toFixed(1)} ${(x + r).toFixed(1)},${yTop.toFixed(1)}` +
`L${(x + barWidth - r).toFixed(1)},${yTop.toFixed(1)}` +
`Q${(x + barWidth).toFixed(1)},${yTop.toFixed(1)} ${(x + barWidth).toFixed(1)},${(yTop + r).toFixed(1)}` +
`L${(x + barWidth).toFixed(1)},${baseline}Z`;
const capLabel = !week.partial && week.value === maxValue && week.value > 0
? `<text class="cap-label" x="${xCenter.toFixed(1)}" y="${(yTop - 7).toFixed(1)}" text-anchor="middle">${fmtCompact(week.value)}</text>`
: "";
const axisLabel = index % labelStep === 0
? `<text class="tick" x="${xCenter.toFixed(1)}" y="${baseline + 20}" text-anchor="middle">${fmtDay(week.weekStart)}</text>`
: "";
const tipText = week.partial
? `week of ${fmtDay(week.weekStart)} · ${fmtInt(week.value)} ${options.unit} · day ${week.daysIn}/7`
: `week of ${fmtDay(week.weekStart)} · ${fmtInt(week.value)} ${options.unit}`;
return (
`<g class="bar${week.partial ? " partial" : ""}" data-tip="${escapeHtml(tipText)}">` +
`<rect class="hit" x="${(margin.left + band * index).toFixed(1)}" y="${margin.top}" width="${band.toFixed(1)}" height="${innerH}"></rect>` +
`<path class="fill" d="${path}" style="fill:var(${options.colorVar})"/>` +
`</g>${capLabel}${axisLabel}`
);
})
.join("");
return `
<div class="chart">
<svg viewBox="0 0 ${BAR_W} ${BAR_H}" role="img" aria-label="${options.unit} per week">
${grid}
<line class="grid strong" x1="${margin.left}" y1="${baseline}" x2="${BAR_W - margin.right}" y2="${baseline}"/>
${bars}
</svg>
<div class="chart-tip" hidden></div>
</div>`;
}
// ---------- tables, rank lists, timeline ----------
function detailsTable(headers: [string, string], rows: string): string {
return `
<details class="chart-table">
<summary>view as table</summary>
<div class="chart-table-scroll">
<table><thead><tr><th>${headers[0]}</th><th>${headers[1]}</th></tr></thead><tbody>${rows}</tbody></table>
</div>
</details>`;
}
function weeklyTable(weeks: WeekPoint[], unit: string): string {
const rows = [...weeks]
.slice(-14)
.reverse()
.map(
(week) =>
`<tr><td>week of ${fmtDay(week.weekStart)}</td><td>${fmtInt(week.value)}${week.partial ? ` (day ${week.daysIn}/7)` : ""}</td></tr>`,
)
.join("");
return detailsTable(["week", unit], rows);
}
function cumulativeTable(points: SeriesPoint[], unit: string): string {
const byMonth = new Map<string, SeriesPoint>();
for (const point of points) byMonth.set(point.d.slice(0, 7), point);
const rows = [...byMonth.values()]
.map((point) => `<tr><td>${fmtDay(point.d)}</td><td>${fmtInt(point.v)}</td></tr>`)
.join("");
return detailsTable(["end of month", unit], rows);
}
function flagEmoji(code: string): string {
if (!/^[A-Z]{2}$/.test(code)) return "";
return String.fromCodePoint(...[...code].map((char) => 0x1f1a5 + char.charCodeAt(0)));
}
function countryName(code: string): string {
try {
return new Intl.DisplayNames(["en"], { type: "region" }).of(code) ?? code;
} catch {
return code;
}
}
function rankList(rows: Array<{ label: string; value: number }>, unit: string): string {
if (rows.length === 0) return `<p class="chart-empty">no data yet</p>`;
const max = Math.max(...rows.map((row) => row.value), 1);
const items = rows
.map(
(row) =>
`<div class="rank-row" title="${escapeHtml(row.label)} · ${fmtInt(row.value)} ${unit}">` +
`<span class="rank-name">${escapeHtml(row.label)}</span>` +
`<span class="rank-bar"><span class="rank-fill" style="width:${((row.value / max) * 100).toFixed(1)}%"></span></span>` +
`<span class="rank-value">${fmtCompact(row.value)}</span>` +
`</div>`,
)
.join("");
return `<div class="rank-list">${items}</div>`;
}
function timelineTerminal(stats: StatsJson, milestones: Milestone[]): string {
const lines: Array<{ day: string; text: string; note: string }> = [];
for (const entry of stats.timeline) {
const note = entry.note ?? `day ${daysBetween(stats.bornDay, entry.d)}`;
lines.push({ day: entry.d, text: escapeHtml(entry.label), note: escapeHtml(note) });
}
let previousDay = stats.timeline.find((entry) => entry.label.includes("first star"))?.d ?? stats.bornDay;
for (const milestone of milestones) {
lines.push({ day: milestone.day, text: `${fmtInt(milestone.value)}`, note: `+${daysBetween(previousDay, milestone.day)} days` });
previousDay = milestone.day;
}
lines.sort((a, b) => a.day.localeCompare(b.day));
lines.push({ day: todayUtc(), text: `${fmtInt(stats.stars.total)}`, note: "today" });
const body = lines
.map(
(line) =>
`<div class="term-line"><span class="term-date">${fmtDay(line.day)}</span><span class="term-text">${line.text}</span><span class="term-note">${line.note}</span></div>`,
)
.join("");
return `
<div class="milestone-term" role="img" aria-label="herdr timeline">
<div class="term-line term-cmd-line"><span class="term-prompt">$</span><span class="term-cmd">herdr stats --timeline</span></div>
${body}
</div>`;
}
// ---------- interaction ----------
function attachChartInteractions(root: ParentNode): void {
root.querySelectorAll<HTMLElement>(".chart").forEach((chart) => {
const dataEl = chart.querySelector<HTMLScriptElement>("script[type='application/json']");
const tip = chart.querySelector<HTMLElement>(".chart-tip");
const svg = chart.querySelector<SVGSVGElement>("svg");
if (!svg || !tip) return;
if (dataEl?.textContent) {
const data = JSON.parse(dataEl.textContent) as {
unit: string;
width: number;
points: Array<{ x: number; y: number; day: string; value: number }>;
};
const crosshair = svg.querySelector<SVGLineElement>(".crosshair");
const hoverDot = svg.querySelector<SVGCircleElement>(".hover-dot");
if (!crosshair || !hoverDot) return;
const points = data.points;
let active = -1;
const showIndex = (index: number): void => {
if (index < 0 || index >= points.length) return;
active = index;
const point = points[index];
crosshair.setAttribute("x1", String(point.x));
crosshair.setAttribute("x2", String(point.x));
crosshair.hidden = false;
hoverDot.setAttribute("cx", String(point.x));
hoverDot.setAttribute("cy", String(point.y));
hoverDot.hidden = false;
tip.innerHTML = "";
const strong = document.createElement("strong");
strong.textContent = `${point.value.toLocaleString("en-US")} ${data.unit}`;
tip.appendChild(strong);
tip.appendChild(document.createElement("br"));
tip.appendChild(document.createTextNode(fmtDay(point.day)));
const rect = svg.getBoundingClientRect();
const scale = rect.width / data.width;
tip.style.left = `${point.x * scale}px`;
tip.style.top = `${point.y * scale}px`;
tip.hidden = false;
};
const hide = (): void => {
active = -1;
crosshair.hidden = true;
hoverDot.hidden = true;
tip.hidden = true;
};
svg.addEventListener("pointermove", (event) => {
const rect = svg.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * data.width;
let best = 0;
let bestDist = Infinity;
for (let i = 0; i < points.length; i += 1) {
const dist = Math.abs(points[i].x - x);
if (dist < bestDist) {
bestDist = dist;
best = i;
}
}
showIndex(best);
});
svg.addEventListener("pointerleave", hide);
svg.addEventListener("focus", () => showIndex(points.length - 1));
svg.addEventListener("blur", hide);
svg.addEventListener("keydown", (event) => {
if (event.key === "ArrowLeft") {
showIndex(Math.max(active - 1, 0));
event.preventDefault();
}
if (event.key === "ArrowRight") {
showIndex(Math.min(active + 1, points.length - 1));
event.preventDefault();
}
});
}
chart.querySelectorAll<SVGGElement>(".bar").forEach((bar) => {
bar.addEventListener("pointerenter", () => {
const hit = bar.querySelector(".hit");
if (!hit) return;
const svgRect = svg.getBoundingClientRect();
const barRect = hit.getBoundingClientRect();
tip.textContent = bar.getAttribute("data-tip");
tip.style.left = `${barRect.left - svgRect.left + barRect.width / 2}px`;
tip.style.top = `${barRect.top - svgRect.top + 18}px`;
tip.hidden = false;
bar.classList.add("active");
});
bar.addEventListener("pointerleave", () => {
tip.hidden = true;
bar.classList.remove("active");
});
});
});
}
// ---------- live star counter ----------
function startLiveStars(initial: number): void {
const hero = document.getElementById("star-count");
if (!hero) return;
let current = initial;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const renderCount = (value: number): void => {
hero.textContent = Math.round(value).toLocaleString("en-US");
};
renderCount(current);
const animateTo = (target: number): void => {
if (reduceMotion || target <= current) {
current = Math.max(current, target);
renderCount(current);
return;
}
const from = current;
const start = performance.now();
const duration = 900;
const frame = (now: number): void => {
const t = Math.min((now - start) / duration, 1);
const eased = 1 - (1 - t) ** 3;
renderCount(from + (target - from) * eased);
if (t < 1) requestAnimationFrame(frame);
else current = target;
};
requestAnimationFrame(frame);
};
const poll = (): void => {
fetch(REPO_API_URL)
.then((res) => (res.ok ? res.json() : null))
.then((repo) => {
if (repo && typeof repo.stargazers_count === "number") animateTo(repo.stargazers_count);
})
.catch(() => {
// keep last known number; the counter must never break the page
});
};
poll();
setInterval(poll, 60_000);
}
// ---------- page assembly ----------
function setText(id: string, text: string): void {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
function setHtml(id: string, html: string): void {
const el = document.getElementById(id);
if (el) el.innerHTML = html;
}
function render(stats: StatsJson): void {
const starsDaily = stats.stars.daily;
const lastStarDay = starsDaily.at(-1)?.d ?? null;
const milestones = starMilestones(starsDaily);
const badges = stats.timeline
.filter((entry) => entry.badge)
.map((entry) => ({ day: entry.d, label: entry.badge ?? "" }));
// hero
const last7d = (() => {
const last = starsDaily.at(-1);
if (!last) return null;
const weekAgoDay = new Date(dayEpoch(last.d) - 7 * 86_400_000).toISOString().slice(0, 10);
const anchor = [...starsDaily].reverse().find((point) => point.d <= weekAgoDay);
return anchor ? stats.stars.total - anchor.v : null;
})();
if (last7d !== null) setText("hero-note", `+${fmtInt(last7d)} stars in the last 7 days`);
if (starsDaily[0]) setText("stars-since", `since ${fmtDay(starsDaily[0].d)}`);
setHtml(
"chart-stars",
lineChart({ points: starsDaily, colorVar: "--accent", unit: "stars", width: 640, milestones, events: badges }) +
cumulativeTable(starsDaily, "stars"),
);
// KPIs
const downloadsDeltas = deltasFromCumulative(stats.downloads.daily);
const lastDownloadDay = stats.downloads.daily.at(-1)?.d ?? null;
const downloadsWeekly = weeklySums(downloadsDeltas, lastDownloadDay);
const lastFullWeek = [...downloadsWeekly].reverse().find((week) => !week.partial);
setText("kpi-installs", fmtInt(stats.downloads.github + stats.downloads.brew));
setHtml(
"kpi-installs-sub",
`${lastFullWeek ? `<span class="up">+${fmtInt(lastFullWeek.value)}</span> last week · ` : ""}releases + homebrew`,
);
const trafficDaily = stats.traffic.daily;
const lastSevenTraffic = trafficDaily.slice(-7);
const visitorsAvg = lastSevenTraffic.length
? Math.round(lastSevenTraffic.reduce((sum, point) => sum + point.v, 0) / lastSevenTraffic.length)
: null;
setText("kpi-visitors", visitorsAvg === null ? "—" : fmtCompact(visitorsAvg));
setText("kpi-releases", fmtInt(stats.releases.shipped));
setText("kpi-age", fmtInt(daysBetween(stats.bornDay, todayUtc())));
setText("kpi-age-sub", `born ${fmtDay(stats.bornDay)}, ${stats.bornDay.slice(0, 4)}`);
// weekly velocity
const starsDeltas = deltasFromCumulative(starsDaily);
setHtml(
"chart-stars-week",
barChart({ weeks: weeklySums(starsDeltas, lastStarDay), colorVar: "--accent", unit: "stars" }) +
weeklyTable(weeklySums(starsDeltas, lastStarDay), "stars"),
);
setHtml(
"chart-downloads-week",
barChart({ weeks: downloadsWeekly, colorVar: "--green", unit: "downloads" }) + weeklyTable(downloadsWeekly, "downloads"),
);
const visitorsWeekly = weeklyAverages(trafficDaily, trafficDaily.at(-1)?.d ?? null);
setHtml(
"chart-visitors-week",
barChart({ weeks: visitorsWeekly, colorVar: "--yellow", unit: "avg daily visitors" }) +
weeklyTable(visitorsWeekly, "avg daily visitors"),
);
// downloads cumulative
const trackedSince = stats.downloads.daily[0]?.d;
setText(
"downloads-sub",
`GitHub release downloads, cumulative${trackedSince ? `, tracked since ${fmtDay(trackedSince)}` : ""}. ` +
`Homebrew adds ${fmtInt(stats.downloads.brew)} installs on top (not shown in the curve).`,
);
setHtml(
"chart-downloads",
lineChart({ points: stats.downloads.daily, colorVar: "--green", unit: "downloads", width: 960 }) +
cumulativeTable(stats.downloads.daily, "downloads"),
);
// traffic
setText("traffic-sub", `herdr.dev traffic over the last ${stats.traffic.windowDays} days, measured by Cloudflare.`);
setHtml(
"rank-countries",
rankList(
stats.traffic.topCountries.map((row) => ({ label: `${flagEmoji(row.code)} ${countryName(row.code)}`.trim(), value: row.visits })),
"visits",
),
);
setHtml(
"rank-referrers",
rankList(stats.traffic.topReferrers.map((row) => ({ label: row.host, value: row.visits })), "visits"),
);
setHtml(
"rank-paths",
rankList(stats.traffic.topPaths.map((row) => ({ label: row.path, value: row.views })), "page views"),
);
// timeline + freshness
setHtml("timeline", timelineTerminal(stats, milestones));
const generated = new Date(stats.generatedAt);
const ageHours = (Date.now() - generated.getTime()) / 3_600_000;
const stampText = `Data updated ${fmtDay(stats.generatedAt.slice(0, 10))}, ${stats.generatedAt.slice(11, 16)} UTC.`;
setText("updated-note", stampText);
if (ageHours > 6) {
const note = document.createElement("p");
note.className = "stats-stale";
note.textContent = `note: snapshot is ${Math.round(ageHours)} hours old — the star counter above is still live.`;
document.getElementById("sources-note")?.after(note);
}
attachChartInteractions(document);
startLiveStars(stats.stars.total);
}
function assertStatsShape(stats: StatsJson): void {
const ok =
stats.schemaVersion === 1 &&
typeof stats.generatedAt === "string" &&
typeof stats.bornDay === "string" &&
typeof stats.stars?.total === "number" &&
Array.isArray(stats.stars?.daily) &&
typeof stats.downloads?.github === "number" &&
typeof stats.downloads?.brew === "number" &&
Array.isArray(stats.downloads?.daily) &&
typeof stats.releases?.shipped === "number" &&
Array.isArray(stats.traffic?.daily) &&
Array.isArray(stats.traffic?.topCountries) &&
Array.isArray(stats.traffic?.topReferrers) &&
Array.isArray(stats.traffic?.topPaths) &&
Array.isArray(stats.timeline);
if (!ok) throw new Error("stats snapshot has unexpected shape");
}
export function initStatsPage(): void {
fetch(STATS_URL, { headers: { accept: "application/json" } })
.then((res) => {
if (!res.ok) throw new Error(`stats fetch failed: ${res.status}`);
return res.json() as Promise<StatsJson>;
})
.then((stats) => {
assertStatsShape(stats);
render(stats);
})
.catch((error) => {
console.error("stats page render failed", error);
setText("hero-note", "stats are temporarily unavailable, try again in a minute");
startLiveStars(0);
});
}
+103
View File
@@ -0,0 +1,103 @@
import { readdir, readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
/* The numbers the site's chrome shows, resolved once per build.
Stars, installs and plugin counts come from the same R2 snapshots /stats/
and /plugins/ read; the version comes from latest.json, which release CI
owns. Nothing here is hand-maintained, so no figure on the site can go
stale independently of a deploy.
Memoised because SiteLayout renders on every blog post, and a build that
renders many pages should still fetch once. */
export type SiteStats = {
starCount?: number;
installCount?: number;
pluginCount?: number;
};
async function snapshot(url: string): Promise<any | null> {
try {
const res = await fetch(url);
return res.ok ? await res.json() : null;
} catch {
return null;
}
}
async function load(): Promise<SiteStats> {
const [stats, plugins] = await Promise.all([
snapshot('https://assets.herdr.dev/stats/stats.json'),
snapshot('https://assets.herdr.dev/plugins/index.json'),
]);
return {
starCount: stats?.stars?.total,
installCount: stats?.downloads
? (stats.downloads.github || 0) + (stats.downloads.brew || 0)
: undefined,
/* `pluginCount` counts validated manifests, which is what the marketplace
lists. `source.totalCount` is the raw GitHub topic search and still
includes blacklisted repositories and ones with no manifest. */
pluginCount: plugins?.pluginCount ?? plugins?.plugins?.length,
};
}
let statsPromise: Promise<SiteStats> | null = null;
export function siteStats(): Promise<SiteStats> {
statsPromise ??= load();
return statsPromise;
}
/* latest.json carries the full release notes, so read and pick rather than
importing it and bundling all of that into the page. */
async function readVersion(): Promise<string | null> {
try {
const raw = await readFile(new URL('../latest.json', import.meta.url), 'utf8');
return JSON.parse(raw).version ?? null;
} catch {
return null;
}
}
let versionPromise: Promise<string | null> | null = null;
export function releaseVersion(): Promise<string | null> {
versionPromise ??= readVersion();
return versionPromise;
}
/* How many agents Herdr detects out of the box, counted from the bundled
manifests. The site is built from the same repo as the binary, so the number
tracks the code and cannot drift the way a written-down one would — add a
manifest and the homepage says so on the next deploy. */
async function countAgentManifests(): Promise<number | null> {
/* Resolved from cwd, not import.meta.url: once Vite bundles this module for
the build the module URL no longer points at the source file, so a
relative URL silently found nothing and the line vanished from dist while
still rendering in dev. Both candidates cover running the build from
website/ or from the repo root. */
for (const rel of ['../src/detect/manifests', 'src/detect/manifests']) {
try {
const files = await readdir(resolve(process.cwd(), rel));
const count = files.filter((f) => f.endsWith('.toml')).length;
if (count > 0) return count;
} catch {
/* try the next candidate */
}
}
return null;
}
let agentCountPromise: Promise<number | null> | null = null;
export function agentCount(): Promise<number | null> {
agentCountPromise ??= countAgentManifests();
return agentCountPromise;
}
/* A fetch that failed at build shows an em dash. A stale hardcoded number
would look authoritative and be wrong, which is worse. */
export const fmt = (n: unknown): string =>
typeof n === 'number' ? n.toLocaleString('en-US') : '—';
+60 -41
View File
@@ -16,47 +16,65 @@
--sl-sidebar-width: 18.5rem;
}
/* Docs share the site's ground and mauve. Prose stays in a reading sans
rather than the marketing mono — docs is a tool, not a statement.
The ramp is the marketing scale, not a separate one: the same near-neutral
ground and the same text triad, so the two surfaces cannot drift apart on
the next palette change. Chroma is drained for the reason it was drained
there — saturated blue at low luminance softens the edges of light text,
and a side-by-side read easier without it. Surfaces are luminance-matched
to what they replace; text is taken straight from the marketing scale,
which lands every step at or above its old contrast except gray-3, and
that one stays clear of AA. Contrast on the ground: white 14.7, gray-1
11.9, gray-2 9.3, gray-3 5.3, gray-4 4.2, accent 8.8. */
:root,
:root[data-theme="dark"] {
--sl-color-accent-low: #26314f;
--sl-color-accent: #7aa2f7;
--sl-color-accent-high: #a9c2ff;
--sl-color-white: #d5dcff;
--sl-color-gray-1: #c0caf5;
--sl-color-gray-2: #a9b1d6;
--sl-color-gray-3: #737aa2;
--sl-color-gray-4: #565f89;
--sl-color-gray-5: #414868;
--sl-color-gray-6: #2f3549;
--sl-color-gray-7: #24283b;
--sl-color-black: #1a1b26;
--sl-color-bg: #1a1b26;
--sl-color-bg-nav: #202331;
--sl-color-bg-sidebar: #202331;
--sl-color-bg-inline-code: #24283b;
--sl-color-hairline: #2f3549;
--sl-color-hairline-light: #414868;
--sl-color-accent-low: #3f364b;
--sl-color-accent: #cba6f7;
--sl-color-accent-high: #ddc4fb;
--sl-color-white: #eae8ee;
--sl-color-gray-1: #d5d1dd;
--sl-color-gray-2: #bdb9c9;
--sl-color-gray-3: #8e8a98;
--sl-color-gray-4: #7c7887;
--sl-color-gray-5: #4a4851;
--sl-color-gray-6: #35353d;
--sl-color-gray-7: #26262b;
--sl-color-black: #17171a;
--sl-color-bg: #17171a;
--sl-color-bg-nav: #1e1e22;
--sl-color-bg-sidebar: #1e1e22;
--sl-color-bg-inline-code: #26262b;
--sl-color-hairline: #26262b;
--sl-color-hairline-light: #35353d;
}
/* the brand display face, same pairing as the marketing pages:
Archivo for the branded headings, mono for everything code-adjacent */
:root { --sl-font-display: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif; }
/* paper — the same ground the marketing pages use, with the mauve dropped to
a value that holds on a light background */
:root[data-theme="light"] {
--sl-color-accent-low: #c4c8da;
--sl-color-accent: #2e7de9;
--sl-color-accent-high: #1f2f66;
--sl-color-white: #1f2f66;
--sl-color-gray-1: #3760bf;
--sl-color-gray-2: #68709a;
--sl-color-gray-3: #8990b3;
--sl-color-gray-4: #a8aecb;
--sl-color-gray-5: #b6bbd1;
--sl-color-gray-6: #c4c8da;
--sl-color-gray-7: #d8d9e0;
--sl-color-black: #e1e2e7;
--sl-color-bg: #e1e2e7;
--sl-color-bg-nav: #d8d9e0;
--sl-color-bg-sidebar: #d8d9e0;
--sl-color-bg-inline-code: #c4c8da;
--sl-color-hairline: #b6bbd1;
--sl-color-hairline-light: #a8aecb;
--sl-color-accent-low: #ded3f2;
--sl-color-accent: #8839ef;
--sl-color-accent-high: #5b2ba3;
--sl-color-white: #15140f;
--sl-color-gray-1: #26241d;
--sl-color-gray-2: #55534a;
--sl-color-gray-3: #6f6c60;
--sl-color-gray-4: #86826f;
--sl-color-gray-5: #a8a493;
--sl-color-gray-6: #cbc5b6;
--sl-color-gray-7: #e2ded4;
--sl-color-black: #efece5;
--sl-color-bg: #efece5;
--sl-color-bg-nav: #e7e3da;
--sl-color-bg-sidebar: #e7e3da;
--sl-color-bg-inline-code: #ddd8cc;
--sl-color-hairline: #e2ded4;
--sl-color-hairline-light: #cbc5b6;
}
.site-title {
@@ -103,16 +121,17 @@
.hero h1 {
letter-spacing: -0.03em;
line-height: 1.1;
font-weight: 400;
font-weight: 900;
font-size: clamp(2rem, 5vw, 3.2rem);
font-family: var(--sl-font-mono);
font-family: var(--sl-font-display);
letter-spacing: -0.045em;
}
.sl-markdown-content h2 {
margin-top: 2.4rem;
letter-spacing: -0.02em;
font-family: var(--sl-font-mono);
font-weight: 400;
letter-spacing: -0.03em;
font-family: var(--sl-font-display);
font-weight: 800;
}
.sl-markdown-content h3 {