maintain-partners — LovStudio 合作伙伴板块维护
Maintains the configured website repo. Resolve the path from --repo,
LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, or the shared user profile. The partners
strip usually lives in app/(main)/(home)/PartnersGrid.tsx as a PARTNERS: Partner[] array; older sites may still keep it in
app/(main)/(home)/WorkshopDispatch.tsx. Logos live in
public/partners/<slug>/logo.png; taglines in
src/i18n/messages/{zh-CN,en,ja,th}.json under dispatch.partner*Tagline.
User Configuration
Before touching files, resolve:
SKILL_ROOT="${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}"
SKILL_DIR="${SKILL_DIR:-$SKILL_ROOT/sgc-maintain-partners}"
WEB_ROOT="${LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT:?Set this or pass --repo}"
PARTNERS_TSX="${LOVSTUDIO_MAINTAIN_PARTNERS_FILE:-app/(main)/(home)/PartnersGrid.tsx}"
Use this precedence for the website root:
- Explicit
--repo <path> on add_partner.py / audit_partners.py.
LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT.
- Shared profile JSON at
${LOVSTUDIO_SKILLS_PROFILE:-$HOME/.lovstudio/skills/profile.json}.
LOVSTUDIO_WEB_ROOT and PARTNERS_SITE_ROOT are accepted as legacy aliases,
but should not be the public contract for reusable skills.
Use this precedence for the partners TSX file:
- Explicit
--partners-file <path>.
LOVSTUDIO_MAINTAIN_PARTNERS_FILE.
- Shared profile keys
sites.partners_file, lovstudio.partners_file,
partners.file, or workspace.partners_file.
app/(main)/(home)/PartnersGrid.tsx, then legacy
app/(main)/(home)/WorkshopDispatch.tsx.
LOVSTUDIO_PARTNERS_FILE and PARTNERS_FILE are accepted as legacy aliases,
but should not be the public contract for reusable skills.
For details and supported profile keys, read references/user-config.md.
Skill Dependencies
sgc-find-logo is required for all logo discovery. This skill must
not scrape homepages itself or keep a separate fallback crawler.
- Use the
depends_on frontmatter field to declare skill-level dependencies.
This mirrors the depends_on field in sgc-general-skills/skills.yaml;
unknown frontmatter keys are expected to be ignored by agents that do not
consume dependency metadata.
When to Use
- User asks to add one or more new partners (with or without a logo URL).
- User asks to standardize / normalize a logo (sizing wrong, white-on-white, etc.).
- User provides a local file and asks to replace an existing partner's logo.
- User asks to audit the partners section before a release.
Standards
- Logo canvas: 80px content height for the website partners strip
(light grayscale, CSS
height: 32px ≈ 2.5× density, sharp enough),
240px for event posters or any retina export at scale: 2 or higher.
- For white-on-transparent logos: invert (full or selective) so they show on
the light grayscale strip.
- For icon-only logos < ~40px wide after normalization: pass
--show-name
when adding so the brand name renders next to the icon.
- Tagline format:
<品牌名> · <一句话定位> in Chinese; mirror style in en/ja/th.
Workflow
Op 1: Add a new partner
- Ask the user for the brand name + homepage URL via
AskUserQuestion.
- Collect the logo with
sgc-find-logo:python3 "$SKILL_ROOT/sgc-find-logo/scripts/find_logo.py" \
--name "<显示名>" --url <URL> --slug <slug> --json
Use the archived primary asset under
~/.lovstudio/logo-collection/<slug>/logo.<ext>. If find_logo.py returns
no candidates, stop and ask the user for a better official URL / press-kit
URL, then rerun find_logo.py. Do not call a local scraper from this skill.
- Visually verify the archived primary asset before normalizing.
- If the primary asset is SVG, rasterize it before normalization:
rsvg-convert -h 240 ~/.lovstudio/logo-collection/<slug>/logo.svg \
-o /tmp/<slug>-raw.png
Use the rasterized /tmp/<slug>-raw.png as --src. For non-SVG sources,
use the archived primary asset directly.
- Normalize:
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src <archived-or-rasterized-logo> \
--dst "$WEB_ROOT/public/partners/<slug>/logo.png" \
--invert auto
- Read the normalized PNG to confirm it's visible (not white-on-white).
- Append to PARTNERS + all 4 locale JSONs:
python3 "$SKILL_DIR/scripts/add_partner.py" \
--repo "$WEB_ROOT" \
--partners-file "$PARTNERS_TSX" \
--name "<显示名>" --href "<URL>" \
--logo "/partners/<slug>/logo.png" \
--key partner<Slug>Tagline \
--category community \
--zh "..." --en "..." --ja "..." --th "..." \
[--show-name]
Op 2: Normalize an existing logo
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src public/partners/<slug>/logo.png \
--dst public/partners/<slug>/logo.png \
--invert auto
Re-read after to verify.
Op 3: Replace logo from a user-provided file
Ask for the source file path directly, or read it from the user's configured
workspace/profile. Do not assume a private partners folder.
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src "<user-provided path>" \
--dst "$WEB_ROOT/public/partners/<slug>/logo.png" \
--invert auto
JPEG inputs auto-strip near-white background to transparent before crop.
Op 4: Audit
python3 "$SKILL_DIR/scripts/audit_partners.py" \
--repo "$WEB_ROOT" \
--partners-file "$PARTNERS_TSX"
# add --probe to also HTTP-check every href (slow, requires proxy)
Reports: missing logo files, missing i18n keys per locale, dead URLs.
Op 5: Align a row of partner logos (cross-asset visual height parity)
When: putting 3+ partner logos in a single horizontal strip and they look
different sizes despite having the same CSS height. Common in event posters,
hero sections, "联办 / co-host" rows.
Root cause: each source file has different internal padding (designer
canvas margin), so two PNGs both set to height: 24px render at different
visible heights because their content occupies different fractions of the
canvas. Per-logo CSS height tweaks based on eyeballed content ratios are
unstable—different displays / scaling will diverge again.
Reliable fix — trim at file level, uniform CSS box:
Normalize every logo to identical content height. Default raster file
target is 240px (3× density for retina poster export at scale: 2;
80px gives only 1.7× and looks soft after PNG export). Use --invert off
if the source is already light-on-transparent (don't double-invert):
for f in lujiazui juanyi citic-bookstore citic-thinker-lab; do
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src "<configured-partners-source>/<brand>/<file>.png" \
--dst <event-assets>/partners/$f.png \
--height 240 --invert auto
done
Always normalize from the original source, never from a previously
normalized 80px file (upscaling = blurry — burned by this on juanyi).
For SVG sources, rasterize first. normalize_logo.py operates on
raster pixels and cannot crop SVG viewBox padding. Without this step
an SVG always renders smaller than rasterized PNG siblings:
rsvg-convert -h 720 brand.svg -o /tmp/brand-raw.png # 3× of 240
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src /tmp/brand-raw.png --dst <event-assets>/partners/brand.png \
--height 240 --invert off
rsvg-convert ships with librsvg (brew install librsvg).
For SVG with embedded background rect (icon wrapped in a black/colored
rounded square — common in app-icon-style SVGs from find-logo), strip
the background before rasterizing, otherwise filter brightness(0) invert(1) flattens it into a solid white block that hides the icon:
# Drop the outer <rect fill="#000"...> wrapper
sed -E 's|<rect[^/]*fill="#0+"[^/]*/>||' brand.svg > /tmp/brand-clean.svg
rsvg-convert -h 720 /tmp/brand-clean.svg -o /tmp/brand-raw.png
Wrap each logo in a fixed-size box (recommended over auto-width flex):
<span class="ps-logo-box"><img src="..." class="ps-logo"></span>
.ps-logo-box {
width: 96px; height: 30px; /* fixed grid cell */
display: inline-flex;
align-items: center; justify-content: center;
border: 1px solid rgba(255,255,255,0.10);
border-radius: 4px;
padding: 3px 6px;
box-sizing: border-box;
}
.ps-logo { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }
Fixed boxes give a stable matrix look — narrow logos (icon-only) and wide
logos (icon + wordmark) all occupy the same footprint, with the asset
scaled to fit. Auto-width flex (the older recipe) makes per-row total
widths unpredictable as logos get added/removed.
Dark-background unification — when the row sits on a dark canvas
(e.g. event poster), most brand logos are designed for white BG and look
inconsistent (some have black text, some have brand-colored marks). The
stable recipe:
.ps-logo { filter: brightness(0) invert(1) opacity(0.88); }
/* logos already white-on-transparent — opt out of inversion */
.ps-logo.ps-logo-original { filter: opacity(0.88); }
brightness(0) flattens all colors to black, then invert(1) produces
uniform white at the configured opacity. The .ps-logo-original escape
hatch is for source files that are already white-on-transparent (white
SVG variants from a brand kit) so you don't double-process them into
invisible black-on-dark.
Icon-only SVG → composite icon + wordmark — if the brand SVG only
has an icon (no "BrandName" wordmark beside it), don't ship just the icon
in a 96×30 box (it'll look like an unidentified mark). Compose the
wordmark with PIL using the brand's own font when possible:
from PIL import Image, ImageDraw, ImageFont, ImageOps
# 1. rasterize cleaned SVG, invert white→black so default filter works
icon = Image.open('/tmp/brand-icon.png').convert('RGBA')
r, g, b, a = icon.split()
inv = Image.merge('RGB', (ImageOps.invert(r), ImageOps.invert(g), ImageOps.invert(b)))
icon = Image.merge('RGBA', (*inv.split(), a))
icon = icon.crop(icon.getbbox())
target_h = 240
icon = icon.resize((int(icon.width * target_h / icon.height), target_h), Image.LANCZOS)
# 2. render wordmark in brand font (find-logo bundles fonts/ when found)
font = ImageFont.truetype('partners/<brand>/fonts/<Family>.ttf', 150)
# 3. compose icon + gap + text on transparent canvas
The PNG goes through the same brightness(0) invert(1) filter as raster
logos — match colors with all other entries automatically. Use the brand's
own font (often shipped under <brand>/fonts/ by the find-logo skill);
fall back to system SF / Helvetica only if no brand font is available.
Anti-pattern — do not try to fix alignment by setting per-logo
heights like .ps-logo-juanyi { height: 26px }. It's brittle (every new
logo needs another magic number), unstable across browsers, and breaks
the moment a designer reships the source asset with different padding.
CLI Reference
normalize_logo.py
| Flag |
Default |
Notes |
--src |
required |
input image (PNG/JPG/rasterized SVG) |
--dst |
required |
output PNG path; parent dirs auto-created |
--height |
80 |
target content height. Use 240 for retina poster export (scale: 2) — 80 looks soft after 2× downscale. |
--invert |
auto |
auto / off / full / selective (selective preserves colored icons) |
add_partner.py
| Flag |
Notes |
--repo |
website repo root; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, profile JSON, or legacy LOVSTUDIO_WEB_ROOT / PARTNERS_SITE_ROOT |
--partners-file |
PARTNERS TSX file; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_FILE, profile JSON, legacy LOVSTUDIO_PARTNERS_FILE / PARTNERS_FILE, PartnersGrid.tsx, or WorkshopDispatch.tsx |
--name |
display name (CJK ok) |
--href |
brand URL |
--logo |
path under /public, e.g. /partners/foo/logo.png |
--key |
i18n key, e.g. partnerFooTagline |
--category |
compute / peer / invest / media / community; default community |
--zh / --en / --ja / --th |
tagline strings (all required) |
--show-name |
render name next to icon for narrow logos |
audit_partners.py
| Flag |
Notes |
--repo |
website repo root; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, profile JSON, or legacy LOVSTUDIO_WEB_ROOT / PARTNERS_SITE_ROOT |
--partners-file |
PARTNERS TSX file; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_FILE, profile JSON, legacy LOVSTUDIO_PARTNERS_FILE / PARTNERS_FILE, PartnersGrid.tsx, or WorkshopDispatch.tsx |
--probe |
HTTP-probe every href (slow, needs proxy env vars) |
Network proxy
Sandbox child processes don't inherit the system ClashX proxy. Before
fetching logos with sgc-find-logo or probing partner URLs, export:
export https_proxy=http://127.0.0.1:7890 \
http_proxy=http://127.0.0.1:7890 \
all_proxy=socks5://127.0.0.1:7891
audit_partners.py already injects these for curl invocations.
Dependencies
git clone https://github.com/lovstudio/find-logo-skill \
"${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}/sgc-find-logo"
python3 -m pip install Pillow
brew install librsvg # for SVG logo sources
1---2name: sgc-maintain-partners3description: Maintain the LovStudio website's partners section AND align partner logo rows on event posters / hero strips: reuse sgc-find-logo for brand logo discovery, normalize collected logos to a 240px-tall content canvas (retina-ready), rasterize SVGs via rsvg-convert before normalizing (so SVG viewBox padding gets cropped), strip embedded background rects from icon-style SVGs, composite icon + wordmark when only an icon is available (using brand fonts), wrap logos in a fixed-size grid box (96×30 with subtle border) for stable matrix layouts, replace existing logos with user-provided files, append new partners to the PARTNERS array with i18n taglines across zh-CN/en/ja/th, and audit the section for dead URLs / missing files / missing translations. Also handles cross-asset visual height parity (multi-logo strips on dark backgrounds, "logo 不等高", unified-color filter recipe). Trigger when the user mentions "合作伙伴", "partners", "trusted by", "新增 logo", "标准化 logo", "替换 logo", "审计合作伙伴", "维护合作伙伴", "logo 不一样高", "logo 对齐", "lo4license: MIT5---67# maintain-partners — LovStudio 合作伙伴板块维护89Maintains the configured website repo. Resolve the path from `--repo`,10`LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT`, or the shared user profile. The partners11strip usually lives in `app/(main)/(home)/PartnersGrid.tsx` as a `PARTNERS:12Partner[]` array; older sites may still keep it in13`app/(main)/(home)/WorkshopDispatch.tsx`. Logos live in14`public/partners/<slug>/logo.png`; taglines in15`src/i18n/messages/{zh-CN,en,ja,th}.json` under `dispatch.partner*Tagline`.1617## User Configuration1819Before touching files, resolve:2021```bash22SKILL_ROOT="${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}"23SKILL_DIR="${SKILL_DIR:-$SKILL_ROOT/sgc-maintain-partners}"24WEB_ROOT="${LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT:?Set this or pass --repo}"25PARTNERS_TSX="${LOVSTUDIO_MAINTAIN_PARTNERS_FILE:-app/(main)/(home)/PartnersGrid.tsx}"26```2728Use this precedence for the website root:29301. Explicit `--repo <path>` on `add_partner.py` / `audit_partners.py`.312. `LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT`.323. Shared profile JSON at33 `${LOVSTUDIO_SKILLS_PROFILE:-$HOME/.lovstudio/skills/profile.json}`.3435`LOVSTUDIO_WEB_ROOT` and `PARTNERS_SITE_ROOT` are accepted as legacy aliases,36but should not be the public contract for reusable skills.3738Use this precedence for the partners TSX file:39401. Explicit `--partners-file <path>`.412. `LOVSTUDIO_MAINTAIN_PARTNERS_FILE`.423. Shared profile keys `sites.partners_file`, `lovstudio.partners_file`,43 `partners.file`, or `workspace.partners_file`.444. `app/(main)/(home)/PartnersGrid.tsx`, then legacy45 `app/(main)/(home)/WorkshopDispatch.tsx`.4647`LOVSTUDIO_PARTNERS_FILE` and `PARTNERS_FILE` are accepted as legacy aliases,48but should not be the public contract for reusable skills.4950For details and supported profile keys, read `references/user-config.md`.5152## Skill Dependencies5354- `sgc-find-logo` is required for all logo discovery. This skill must55 not scrape homepages itself or keep a separate fallback crawler.56- Use the `depends_on` frontmatter field to declare skill-level dependencies.57 This mirrors the `depends_on` field in `sgc-general-skills/skills.yaml`;58 unknown frontmatter keys are expected to be ignored by agents that do not59 consume dependency metadata.6061## When to Use6263- User asks to **add** one or more new partners (with or without a logo URL).64- User asks to **standardize / normalize** a logo (sizing wrong, white-on-white, etc.).65- User provides a local file and asks to **replace** an existing partner's logo.66- User asks to **audit** the partners section before a release.6768## Standards6970- Logo canvas: **80px** content height for the website partners strip71 (light grayscale, CSS `height: 32px` ≈ 2.5× density, sharp enough),72 **240px** for event posters or any retina export at `scale: 2` or higher.73- For white-on-transparent logos: invert (full or selective) so they show on74 the light grayscale strip.75- For icon-only logos < ~40px wide after normalization: pass `--show-name`76 when adding so the brand name renders next to the icon.77- Tagline format: `<品牌名> · <一句话定位>` in Chinese; mirror style in en/ja/th.7879## Workflow8081### Op 1: Add a new partner82831. Ask the user for the brand name + homepage URL via `AskUserQuestion`.842. Collect the logo with `sgc-find-logo`:85 ```bash86 python3 "$SKILL_ROOT/sgc-find-logo/scripts/find_logo.py" \87 --name "<显示名>" --url <URL> --slug <slug> --json88 ```89 Use the archived primary asset under90 `~/.lovstudio/logo-collection/<slug>/logo.<ext>`. If `find_logo.py` returns91 no candidates, stop and ask the user for a better official URL / press-kit92 URL, then rerun `find_logo.py`. Do not call a local scraper from this skill.933. Visually verify the archived primary asset before normalizing.944. If the primary asset is SVG, rasterize it before normalization:95 ```bash96 rsvg-convert -h 240 ~/.lovstudio/logo-collection/<slug>/logo.svg \97 -o /tmp/<slug>-raw.png98 ```99 Use the rasterized `/tmp/<slug>-raw.png` as `--src`. For non-SVG sources,100 use the archived primary asset directly.1015. Normalize:102 ```bash103 python3 "$SKILL_DIR/scripts/normalize_logo.py" \104 --src <archived-or-rasterized-logo> \105 --dst "$WEB_ROOT/public/partners/<slug>/logo.png" \106 --invert auto107 ```1086. Read the normalized PNG to confirm it's visible (not white-on-white).1097. Append to PARTNERS + all 4 locale JSONs:110 ```bash111 python3 "$SKILL_DIR/scripts/add_partner.py" \112 --repo "$WEB_ROOT" \113 --partners-file "$PARTNERS_TSX" \114 --name "<显示名>" --href "<URL>" \115 --logo "/partners/<slug>/logo.png" \116 --key partner<Slug>Tagline \117 --category community \118 --zh "..." --en "..." --ja "..." --th "..." \119 [--show-name]120 ```121122### Op 2: Normalize an existing logo123124```bash125python3 "$SKILL_DIR/scripts/normalize_logo.py" \126 --src public/partners/<slug>/logo.png \127 --dst public/partners/<slug>/logo.png \128 --invert auto129```130131Re-read after to verify.132133### Op 3: Replace logo from a user-provided file134135Ask for the source file path directly, or read it from the user's configured136workspace/profile. Do not assume a private partners folder.137138```bash139python3 "$SKILL_DIR/scripts/normalize_logo.py" \140 --src "<user-provided path>" \141 --dst "$WEB_ROOT/public/partners/<slug>/logo.png" \142 --invert auto143```144145JPEG inputs auto-strip near-white background to transparent before crop.146147### Op 4: Audit148149```bash150python3 "$SKILL_DIR/scripts/audit_partners.py" \151 --repo "$WEB_ROOT" \152 --partners-file "$PARTNERS_TSX"153# add --probe to also HTTP-check every href (slow, requires proxy)154```155156Reports: missing logo files, missing i18n keys per locale, dead URLs.157158### Op 5: Align a row of partner logos (cross-asset visual height parity)159160**When**: putting 3+ partner logos in a single horizontal strip and they look161different sizes despite having the same CSS `height`. Common in event posters,162hero sections, "联办 / co-host" rows.163164**Root cause**: each source file has different internal padding (designer165canvas margin), so two PNGs both set to `height: 24px` render at different166*visible* heights because their content occupies different fractions of the167canvas. Per-logo CSS height tweaks based on eyeballed content ratios are168unstable—different displays / scaling will diverge again.169170**Reliable fix — trim at file level, uniform CSS box**:1711721. **Normalize every logo** to identical content height. Default raster file173 target is **240px** (3× density for retina poster export at `scale: 2`;174 80px gives only 1.7× and looks soft after PNG export). Use `--invert off`175 if the source is already light-on-transparent (don't double-invert):176 ```bash177 for f in lujiazui juanyi citic-bookstore citic-thinker-lab; do178 python3 "$SKILL_DIR/scripts/normalize_logo.py" \179 --src "<configured-partners-source>/<brand>/<file>.png" \180 --dst <event-assets>/partners/$f.png \181 --height 240 --invert auto182 done183 ```184 **Always normalize from the original source**, never from a previously185 normalized 80px file (upscaling = blurry — burned by this on juanyi).1861872. **For SVG sources, rasterize first**. `normalize_logo.py` operates on188 raster pixels and **cannot crop SVG viewBox padding**. Without this step189 an SVG always renders smaller than rasterized PNG siblings:190 ```bash191 rsvg-convert -h 720 brand.svg -o /tmp/brand-raw.png # 3× of 240192 python3 "$SKILL_DIR/scripts/normalize_logo.py" \193 --src /tmp/brand-raw.png --dst <event-assets>/partners/brand.png \194 --height 240 --invert off195 ```196 `rsvg-convert` ships with `librsvg` (`brew install librsvg`).1971983. **For SVG with embedded background rect** (icon wrapped in a black/colored199 rounded square — common in app-icon-style SVGs from `find-logo`), strip200 the background before rasterizing, otherwise filter `brightness(0)201 invert(1)` flattens it into a solid white block that hides the icon:202 ```bash203 # Drop the outer <rect fill="#000"...> wrapper204 sed -E 's|<rect[^/]*fill="#0+"[^/]*/>||' brand.svg > /tmp/brand-clean.svg205 rsvg-convert -h 720 /tmp/brand-clean.svg -o /tmp/brand-raw.png206 ```2072084. **Wrap each logo in a fixed-size box** (recommended over auto-width flex):209 ```html210 <span class="ps-logo-box"><img src="..." class="ps-logo"></span>211 ```212 ```css213 .ps-logo-box {214 width: 96px; height: 30px; /* fixed grid cell */215 display: inline-flex;216 align-items: center; justify-content: center;217 border: 1px solid rgba(255,255,255,0.10);218 border-radius: 4px;219 padding: 3px 6px;220 box-sizing: border-box;221 }222 .ps-logo { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }223 ```224 Fixed boxes give a stable matrix look — narrow logos (icon-only) and wide225 logos (icon + wordmark) all occupy the same footprint, with the asset226 scaled to fit. Auto-width flex (the older recipe) makes per-row total227 widths unpredictable as logos get added/removed.2282295. **Dark-background unification** — when the row sits on a dark canvas230 (e.g. event poster), most brand logos are designed for white BG and look231 inconsistent (some have black text, some have brand-colored marks). The232 stable recipe:233 ```css234 .ps-logo { filter: brightness(0) invert(1) opacity(0.88); }235 /* logos already white-on-transparent — opt out of inversion */236 .ps-logo.ps-logo-original { filter: opacity(0.88); }237 ```238 `brightness(0)` flattens all colors to black, then `invert(1)` produces239 uniform white at the configured opacity. The `.ps-logo-original` escape240 hatch is for source files that are already white-on-transparent (white241 SVG variants from a brand kit) so you don't double-process them into242 invisible black-on-dark.2432446. **Icon-only SVG → composite icon + wordmark** — if the brand SVG only245 has an icon (no "BrandName" wordmark beside it), don't ship just the icon246 in a 96×30 box (it'll look like an unidentified mark). Compose the247 wordmark with PIL using the brand's own font when possible:248249 ```python250 from PIL import Image, ImageDraw, ImageFont, ImageOps251 # 1. rasterize cleaned SVG, invert white→black so default filter works252 icon = Image.open('/tmp/brand-icon.png').convert('RGBA')253 r, g, b, a = icon.split()254 inv = Image.merge('RGB', (ImageOps.invert(r), ImageOps.invert(g), ImageOps.invert(b)))255 icon = Image.merge('RGBA', (*inv.split(), a))256 icon = icon.crop(icon.getbbox())257 target_h = 240258 icon = icon.resize((int(icon.width * target_h / icon.height), target_h), Image.LANCZOS)259 # 2. render wordmark in brand font (find-logo bundles fonts/ when found)260 font = ImageFont.truetype('partners/<brand>/fonts/<Family>.ttf', 150)261 # 3. compose icon + gap + text on transparent canvas262 ```263 The PNG goes through the same `brightness(0) invert(1)` filter as raster264 logos — match colors with all other entries automatically. Use the brand's265 own font (often shipped under `<brand>/fonts/` by the find-logo skill);266 fall back to system SF / Helvetica only if no brand font is available.2672687. **Anti-pattern — do not** try to fix alignment by setting per-logo269 heights like `.ps-logo-juanyi { height: 26px }`. It's brittle (every new270 logo needs another magic number), unstable across browsers, and breaks271 the moment a designer reships the source asset with different padding.272273## CLI Reference274275### normalize_logo.py276| Flag | Default | Notes |277|---|---|---|278| `--src` | required | input image (PNG/JPG/rasterized SVG) |279| `--dst` | required | output PNG path; parent dirs auto-created |280| `--height` | `80` | target content height. **Use 240 for retina poster export** (`scale: 2`) — 80 looks soft after 2× downscale. |281| `--invert` | `auto` | `auto` / `off` / `full` / `selective` (selective preserves colored icons) |282283### add_partner.py284| Flag | Notes |285|---|---|286| `--repo` | website repo root; defaults to `LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT`, profile JSON, or legacy `LOVSTUDIO_WEB_ROOT` / `PARTNERS_SITE_ROOT` |287| `--partners-file` | PARTNERS TSX file; defaults to `LOVSTUDIO_MAINTAIN_PARTNERS_FILE`, profile JSON, legacy `LOVSTUDIO_PARTNERS_FILE` / `PARTNERS_FILE`, PartnersGrid.tsx, or WorkshopDispatch.tsx |288| `--name` | display name (CJK ok) |289| `--href` | brand URL |290| `--logo` | path under `/public`, e.g. `/partners/foo/logo.png` |291| `--key` | i18n key, e.g. `partnerFooTagline` |292| `--category` | `compute` / `peer` / `invest` / `media` / `community`; default `community` |293| `--zh / --en / --ja / --th` | tagline strings (all required) |294| `--show-name` | render name next to icon for narrow logos |295296### audit_partners.py297| Flag | Notes |298|---|---|299| `--repo` | website repo root; defaults to `LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT`, profile JSON, or legacy `LOVSTUDIO_WEB_ROOT` / `PARTNERS_SITE_ROOT` |300| `--partners-file` | PARTNERS TSX file; defaults to `LOVSTUDIO_MAINTAIN_PARTNERS_FILE`, profile JSON, legacy `LOVSTUDIO_PARTNERS_FILE` / `PARTNERS_FILE`, PartnersGrid.tsx, or WorkshopDispatch.tsx |301| `--probe` | HTTP-probe every href (slow, needs proxy env vars) |302303## Network proxy304305Sandbox child processes don't inherit the system ClashX proxy. Before306fetching logos with `sgc-find-logo` or probing partner URLs, export:307308```bash309export https_proxy=http://127.0.0.1:7890 \310 http_proxy=http://127.0.0.1:7890 \311 all_proxy=socks5://127.0.0.1:7891312```313314`audit_partners.py` already injects these for `curl` invocations.315316## Dependencies317318```bash319git clone https://github.com/lovstudio/find-logo-skill \320 "${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}/sgc-find-logo"321python3 -m pip install Pillow322brew install librsvg # for SVG logo sources323```