diff --git a/brand/bytefolk/README.md b/brand/bytefolk/README.md
index 439b86f..437b0e2 100644
--- a/brand/bytefolk/README.md
+++ b/brand/bytefolk/README.md
@@ -1,40 +1,41 @@
-# ByteFolk brand mark
+# ByteFolk organization avatar
-The ByteFolk organization mark is the minimal one-color **horned B** approved in
-[issue #18](https://github.com/bytefolk/.github/issues/18). It combines a capital `B`,
-two modular horns, and an extended lower bowl that hints at the digital-employee mascot without
-turning the logo into an illustration.
+The current organization avatar uses the **Open Herd icon without the ByteFolk wordmark**,
+as specified in [issue #18, R2](https://github.com/bytefolk/.github/issues/18).
+Its only graphic source is `g#open-herd-mark` in
+[`bytefolk-concept-c-open-herd.svg`](bytefolk-concept-c-open-herd.svg).
+The mark-only SVG preserves that group's four paths, five rectangles, and colors exactly;
+it only removes the horizontal lockup's translation and wordmark and fits the icon canvas.
+R2 supersedes the earlier R1 one-color horned B avatar direction.
## Assets
| File | Use |
| --- | --- |
-| `symbol.svg` | Primary mark on white or very light surfaces |
-| `symbol-reversed.svg` | White mark on black or dark surfaces |
-| `lockup.svg` | Horizontal symbol and `ByteFolk` wordmark |
-| `avatar-1024.png` | Exact opaque upload artifact for the GitHub organization profile |
+| `bytefolk-concept-c-open-herd.svg` | Unchanged original Open Herd horizontal lockup and source group |
+| `bytefolk-concept-c-open-herd-mark.svg` | Exact icon-only extraction; the current avatar export source |
+| `avatar-1024.png` | Opaque 1024×1024 Open Herd upload artifact for the GitHub organization profile |
+| `symbol.svg` | Historical R1 black horned B; not an avatar source |
+| `symbol-reversed.svg` | Historical R1 white horned B; not an avatar source |
+| `lockup.svg` | Historical R1 horned B lockup; not an avatar source |
-The full-body mascot is a separate expressive brand character. It must not replace the symbol in
-organization avatars, favicons, repository ownership marks, or compact product navigation.
+Historical R1 SVGs remain unchanged for reference. Their presence or earlier approval must not
+be used to restore the black B organization avatar. The full-body mascot is also a separate asset.
## Usage
-- Primary ink: `#141414`.
-- Reversed ink: `#FFFFFF`.
-- Keep clear space of at least one horn-stem width around the symbol.
-- Keep every visible avatar pixel (any grayscale value below 250) inside the 496 px-radius circle
+- Preserve the source palette: blue `#1677ff`, purple `#722ed1`, ink `#141414`, and white `#ffffff`.
+- Use the icon without visible text for organization avatars and compact navigation.
+- Keep every visible avatar pixel (any color channel below 250) inside the 496 px-radius circle
centered at `(511.5, 511.5)` on the 1024 px canvas. This leaves 16 px inside GitHub's 512 px
circular crop.
-- Use the standalone symbol at 16 px or larger.
-- Use the horizontal lockup at 120 px wide or larger.
+- Use the standalone icon at 16 px or larger. Reserve the full lockup for wider brand surfaces.
- Upload `avatar-1024.png` without recropping or recoloring it. The image already includes padding
and a validated circular safe area for GitHub's presentation.
-- On dark surfaces, use `symbol-reversed.svg`; do not place the primary black mark directly on a
- dark background.
+- Preserve the opaque white avatar background in both GitHub light and dark themes.
-Do not add gradients, shadows, outlines, background frames, eyes, nostrils, mane tiles, extra
-colors, text inside the mark, or product-specific decoration. Do not stretch, rotate, crop, or
-rearrange the horns and B geometry.
+Do not redraw, recolor, stretch, rotate, crop, or rearrange the source geometry. Do not add
+gradients, shadows, outlines, text, or decoration. Preserve the existing white details and seams.
## Reproduce the GitHub avatar
@@ -43,7 +44,14 @@ ImageMagick is required for deterministic raster export:
```bash
brand/bytefolk/render-avatar.sh
brand/bytefolk/render-avatar.sh --check
+ruby scripts/validate-brand-assets.rb
```
-The first command rewrites `avatar-1024.png` from `symbol.svg`. The second renders to a temporary
-file and verifies byte-for-byte equality with the tracked artifact.
+The first command renders the mark-only SVG at density 768, scales proportionally to fit
+900×900, centers it on an opaque white 1024×1024 canvas, and writes `avatar-1024.png`.
+The second renders to a temporary file and verifies byte-for-byte equality with the tracked
+artifact. The validator checks the approved source, icon geometry and palette, and avatar pixels.
+
+The tracked export was produced with ImageMagick 7.1.2-22. Other renderer versions may produce
+different antialiasing or PNG bytes; do not replace the approved artifact without verifying the
+source and visual result. Upload the tracked PNG, then verify GitHub's served pixels separately.
diff --git a/brand/bytefolk/avatar-1024.png b/brand/bytefolk/avatar-1024.png
index 82e530b..a023ee1 100644
Binary files a/brand/bytefolk/avatar-1024.png and b/brand/bytefolk/avatar-1024.png differ
diff --git a/brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg b/brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg
new file mode 100644
index 0000000..4f95076
--- /dev/null
+++ b/brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg
@@ -0,0 +1,13 @@
+
diff --git a/brand/bytefolk/bytefolk-concept-c-open-herd.svg b/brand/bytefolk/bytefolk-concept-c-open-herd.svg
new file mode 100644
index 0000000..6e4e928
--- /dev/null
+++ b/brand/bytefolk/bytefolk-concept-c-open-herd.svg
@@ -0,0 +1,35 @@
+
diff --git a/brand/bytefolk/render-avatar.sh b/brand/bytefolk/render-avatar.sh
index 511a90d..a35cd40 100755
--- a/brand/bytefolk/render-avatar.sh
+++ b/brand/bytefolk/render-avatar.sh
@@ -2,7 +2,7 @@
set -euo pipefail
asset_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-source_svg="$asset_dir/symbol.svg"
+source_svg="$asset_dir/bytefolk-concept-c-open-herd-mark.svg"
tracked_png="$asset_dir/avatar-1024.png"
mode="${1:-write}"
@@ -15,9 +15,12 @@ render() {
local destination="$1"
magick \
-background '#ffffff' \
+ -density 768 \
"$source_svg" \
- -filter box \
- -resize '1024x1024!' \
+ -resize 900x900 \
+ -gravity center \
+ -background '#ffffff' \
+ -extent 1024x1024 \
-alpha remove \
-alpha off \
-colorspace sRGB \
@@ -40,7 +43,7 @@ case "$mode" in
echo "avatar-1024.png is stale; run brand/bytefolk/render-avatar.sh" >&2
exit 1
fi
- echo "ByteFolk avatar matches symbol.svg"
+ echo "ByteFolk avatar matches the Open Herd mark-only source"
;;
*)
echo "Usage: brand/bytefolk/render-avatar.sh [write|--check]" >&2
diff --git a/scripts/validate-brand-assets.rb b/scripts/validate-brand-assets.rb
index beda490..7ea98ac 100644
--- a/scripts/validate-brand-assets.rb
+++ b/scripts/validate-brand-assets.rb
@@ -3,10 +3,18 @@
require "pathname"
require "rexml/document"
require "zlib"
+require "digest"
ROOT = Pathname.new(File.expand_path("..", __dir__)).freeze
ASSET_DIR = ROOT.join("brand", "bytefolk").freeze
-SVG_NAMES = %w[symbol.svg symbol-reversed.svg lockup.svg].freeze
+LEGACY_SVG_NAMES = %w[symbol.svg symbol-reversed.svg lockup.svg].freeze
+SOURCE_NAME = "bytefolk-concept-c-open-herd.svg"
+MARK_NAME = "bytefolk-concept-c-open-herd-mark.svg"
+SVG_NAMES = (LEGACY_SVG_NAMES + [SOURCE_NAME, MARK_NAME]).freeze
+# Issue #18 R2: preserve the owner's original source and approved rendered upload.
+SOURCE_SHA256 = "160c6f108e25d5f6d2a06ae68bd3cc11715f4580c0a0d9d02060d016a4526e49"
+AVATAR_SHA256 = "aefe9f3494bff30ff93809f929b04f709f5f36eedbfabdb5f5764efe78578461"
+PALETTE = { "--bf-blue" => "#1677ff", "--bf-purple" => "#722ed1", "--bf-ink" => "#141414", "--bf-paper" => "#ffffff" }.freeze
PROHIBITED_ELEMENTS = %w[filter foreignObject image linearGradient radialGradient script].freeze
CIRCULAR_SAFETY_MARGIN = 16.0
INK_THRESHOLD = 250
@@ -14,6 +22,7 @@
errors = []
circular_metrics = nil
geometry_signatures = {}
+documents = {}
SVG_NAMES.each do |name|
path = ASSET_DIR.join(name)
@@ -34,6 +43,7 @@
errors << "#{path.relative_path_from(ROOT)}: root element must be svg"
next
end
+ documents[name] = document
errors << "#{path.relative_path_from(ROOT)}: viewBox must be present" if root.attributes["viewBox"].to_s.empty?
%w[width height].each do |attribute|
errors << "#{path.relative_path_from(ROOT)}: root #{attribute} is prohibited" if root.attributes[attribute]
@@ -51,6 +61,8 @@
end
end
+ next unless LEGACY_SVG_NAMES.include?(name)
+
symbol_group = REXML::XPath.first(document, "//*[starts-with(@id, 'bytefolk-symbol')]")
if symbol_group
child_geometry = symbol_group.elements.map do |element|
@@ -63,15 +75,43 @@
end
end
-if geometry_signatures.length == SVG_NAMES.length && geometry_signatures.values.uniq.length != 1
+if geometry_signatures.length == LEGACY_SVG_NAMES.length && geometry_signatures.values.uniq.length != 1
errors << "ByteFolk primary, reversed, and lockup symbol geometry must remain identical"
end
+if documents[SOURCE_NAME] && documents[MARK_NAME]
+ unless Digest::SHA256.file(ASSET_DIR.join(SOURCE_NAME)).hexdigest == SOURCE_SHA256
+ errors << "#{SOURCE_NAME}: original Open Herd source must remain unchanged"
+ end
+ source_group = REXML::XPath.first(documents[SOURCE_NAME], "//*[@id='open-herd-mark']")
+ mark = documents[MARK_NAME].root
+ unless mark.attributes["viewBox"] == "0 0 142 116" &&
+ (mark.attributes.to_a.map(&:name) - %w[xmlns viewBox role aria-label]).empty?
+ errors << "#{MARK_NAME}: preserve the icon canvas and do not add root styling or transforms"
+ end
+ normalize = lambda do |element|
+ attributes = element.attributes.map do |key, value|
+ value = PALETTE.fetch(value[4...-1], value) if value.start_with?("var(")
+ value = "#ffffff" if value == "#fff"
+ [key, value]
+ end.sort
+ [element.name, attributes, element.elements.size]
+ end
+ source_geometry = source_group&.elements&.map { |element| normalize.call(element) }
+ icon_geometry = mark.elements.reject { |element| %w[title desc].include?(element.name) }.map { |element| normalize.call(element) }
+ unless source_geometry && icon_geometry == source_geometry
+ errors << "#{MARK_NAME}: icon geometry and palette must match the original group exactly, without text"
+ end
+end
+
png_path = ASSET_DIR.join("avatar-1024.png")
if !png_path.file?
errors << "#{png_path.relative_path_from(ROOT)}: missing"
else
bytes = png_path.binread
+ unless Digest::SHA256.hexdigest(bytes) == AVATAR_SHA256
+ errors << "#{png_path.relative_path_from(ROOT)}: does not match the approved R2 Open Herd upload"
+ end
signature = "\x89PNG\r\n\x1A\n".b
if bytes.byteslice(0, 8) != signature
errors << "#{png_path.relative_path_from(ROOT)}: invalid PNG signature"
@@ -107,7 +147,7 @@
if raw.bytesize == expected_size
prior = Array.new(stride, 0)
- pixels = []
+ colors = {}
cursor = 0
min_x = width
min_y = height
@@ -147,11 +187,8 @@
end
row.each_slice(3).with_index do |(red, green, blue), x|
- unless red == green && green == blue
- errors << "#{png_path.relative_path_from(ROOT)}: avatar must remain grayscale"
- break
- end
- next if red >= INK_THRESHOLD
+ colors[[red, green, blue]] = true
+ next if [red, green, blue].min >= INK_THRESHOLD
min_x = [min_x, x].min
min_y = [min_y, y].min
@@ -161,12 +198,13 @@
max_ink_radius = [max_ink_radius, ink_radius].max
outside_circle_ink += 1 if ink_radius > safe_radius
end
- pixels.concat(row)
prior = row
end
- darkest = pixels.min
- errors << "#{png_path.relative_path_from(ROOT)}: primary ink is not #141414" unless darkest == 20
+ PALETTE.each_value do |hex|
+ rgb = hex.delete_prefix("#").scan(/../).map { |channel| channel.to_i(16) }
+ errors << "#{png_path.relative_path_from(ROOT)}: missing source color #{hex}" unless colors[rgb]
+ end
margins = [min_x, min_y, width - 1 - max_x, height - 1 - max_y]
if max_x.negative? || margins.any? { |margin| margin < 100 }
errors << "#{png_path.relative_path_from(ROOT)}: insufficient rectangular padding #{margins.inspect}"