diff --git a/bash/functions/browser/set_chrome_profile_appearance.md b/bash/functions/browser/set_chrome_profile_appearance.md
new file mode 100644
index 00000000..a9062d94
--- /dev/null
+++ b/bash/functions/browser/set_chrome_profile_appearance.md
@@ -0,0 +1,97 @@
+---
+name: set_chrome_profile_appearance
+kind: function
+lang: bash
+domain: browser
+version: "1.0.0"
+purity: impure
+signature: "set_chrome_profile_appearance --user-data-dir
--profile [--avatar ] [--color <#rrggbb>] [--dry-run]"
+description: "Personaliza la apariencia visual de un perfil Chrome/Chromium existente: asigna un avatar built-in (índice 0..55) o una imagen PNG/JPG custom, y/o un color de acento (hex #rrggbb). Edita los campos avatar_icon, is_using_default_avatar, profile_highlight_color, profile_color_seed y default_avatar_fill_color en profile.info_cache de Local State. Requiere que Chromium esté cerrado sobre el user-data-dir. Hace backup de Local State antes de escribir y valida el JSON resultante."
+tags: [navegator, chromium, profile, browser, cdp, scraping, appearance, avatar, color]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: []
+tested: false
+tests: []
+test_file_path: ""
+file_path: "bash/functions/browser/set_chrome_profile_appearance.sh"
+params:
+ - name: --user-data-dir
+ desc: "Raíz del user-data-dir de Chrome/Chromium donde vive el perfil. El directorio y Local State deben existir. Obligatorio."
+ - name: --profile
+ desc: "Nombre de la carpeta del perfil dentro de user-data-dir, por ejemplo: Default, Automation, \"Profile 1\". El perfil debe existir previamente en info_cache de Local State. Obligatorio."
+ - name: --avatar
+ desc: "Índice entero 0..55 del avatar built-in de Chrome (56 avatares: animales, objetos, personas) o ruta absoluta/relativa a un archivo PNG/JPG para avatar custom. Con índice: sets avatar_icon=IDR_PROFILE_AVATAR_ e is_using_default_avatar=true. Con imagen: copia el archivo al perfil como 'Google Profile Picture.png' y sets is_using_default_avatar=false. Opcional; al menos uno de --avatar o --color debe darse."
+ - name: --color
+ desc: "Color de acento del perfil en hex #rrggbb, con o sin el '#' inicial. Se convierte internamente a int32 con signo en formato ARGB 0xFFRRGGBB y se aplica a profile_highlight_color, profile_color_seed y default_avatar_fill_color. Opcional; al menos uno de --avatar o --color debe darse."
+ - name: --dry-run
+ desc: "Describe las acciones que se ejecutarían (campos a modificar, conversión de color) sin escribir nada ni verificar si Chromium está corriendo. Emite JSON de resultado con dry_run:true."
+output: "JSON en stdout con los campos resultantes del perfil: {\"profile\":\"\",\"avatar_icon\":\"...\",\"is_using_default_avatar\":true|false,\"profile_highlight_color\":,\"profile_color_seed\":,\"default_avatar_fill_color\":,\"backup\":\"Local State.bak.YYYYMMDD\"}. En dry-run: {\"profile\":\"...\",\"avatar_applied\":true|false,\"color_applied\":true|false,\"dry_run\":true}. Mensajes de diagnóstico a stderr. Exit 0 en éxito."
+---
+
+## Ejemplo
+
+```bash
+source $HOME/fn_registry/bash/functions/browser/set_chrome_profile_appearance.sh
+
+# Asignar avatar built-in #26 (oso panda) y color azul GitHub al perfil Automation
+set_chrome_profile_appearance \
+ --user-data-dir ~/.config/chromium-cdp \
+ --profile Automation \
+ --avatar 26 \
+ --color "#1f6feb"
+# Salida JSON con los valores aplicados
+
+# Solo cambiar color (sin tocar avatar)
+set_chrome_profile_appearance \
+ --user-data-dir ~/.config/chromium-cdp \
+ --profile Default \
+ --color "16a34a"
+
+# Solo cambiar avatar built-in
+set_chrome_profile_appearance \
+ --user-data-dir ~/.config/chromium-cdp \
+ --profile "Profile 1" \
+ --avatar 5
+
+# Avatar custom desde imagen
+set_chrome_profile_appearance \
+ --user-data-dir ~/.config/chromium-cdp \
+ --profile Personal \
+ --avatar /tmp/foto.png \
+ --color "#0ea5e9"
+
+# Dry-run: ver qué se aplicaría sin escribir
+set_chrome_profile_appearance \
+ --user-data-dir ~/.config/chromium-cdp \
+ --profile Automation \
+ --avatar 26 \
+ --color "#1f6feb" \
+ --dry-run
+```
+
+## Cuando usarla
+
+Úsala para diferenciar visualmente los perfiles de un user-data-dir de automatización — un color y avatar distintos por perfil hacen inmediata la identificación en el selector de Chrome. Ejecútala justo después de `create_chrome_profile` (con `--no-launch`) o como paso independiente de personalización batch antes de lanzar sesiones CDP.
+
+## Gotchas
+
+- **Chromium debe estar cerrado**: Chrome reescribe `Local State` completo desde memoria al cerrar; si se ejecuta mientras hay un proceso chromium vivo sobre el mismo user-data-dir, Chrome sobreescribirá los cambios al salir. La función detecta esto con `pgrep -x chromium` filtrando por cmdline y sale con exit 2 antes de modificar nada. Usa `pkill -TERM chromium` para cerrar y espera unos segundos.
+- **El perfil debe existir en info_cache**: esta función personaliza perfiles existentes; no los crea. Usa `create_chrome_profile` primero (con `--no-launch` basta para que aparezca en Local State) y luego `set_chrome_profile_appearance`.
+- **color es int32 con signo en ARGB**: Chrome almacena el color como entero con signo de 32 bits en formato `0xAARRGGBB`. Un color como `#1f6feb` (azul) da ARGB `0xFF1F6FEB` → signed int32 `-14713877`. La función hace la conversión internamente; tú pasas siempre hex `#rrggbb`.
+- **Avatar custom (imagen) es best-effort**: el campo `gaia_picture_file_name` y `is_using_default_avatar=false` se aplican correctamente en Local State y la imagen se copia al directorio del perfil. Sin embargo, Chrome puede ignorar la foto de perfil en perfiles sin sesión Google activa (Chromium sin cuenta). El camino robusto y garantizado es usar el índice built-in (`--avatar 0..55`): 56 avatares (animales, objetos, personas) son más que suficientes para diferenciar perfiles de automatización.
+- **Backup diario**: se crea `Local State.bak.YYYYMMDD` en el user-data-dir antes de cualquier escritura. Si ya existe el backup del día no se sobreescribe. Si el JSON resultante es inválido, se restaura automáticamente el backup.
+- **is_using_default_avatar con índice built-in**: Chrome considera los avatares IDR_PROFILE_AVATAR_* como "avatares por defecto" del sistema, por eso `is_using_default_avatar` permanece `true` con índice numérico. Esto es correcto y es lo que Chrome haría internamente.
+
+## Exit codes
+
+| Código | Significado |
+|--------|------------|
+| 0 | Éxito |
+| 1 | Argumento obligatorio faltante, rango inválido o archivo de imagen no encontrado |
+| 2 | Lock: hay un chromium usando el mismo user-data-dir |
+| 3 | El perfil no existe en info_cache de Local State |
+| 4 | Error editando Local State (JSON inválido tras escritura, restaurado backup) |
diff --git a/bash/functions/browser/set_chrome_profile_appearance.sh b/bash/functions/browser/set_chrome_profile_appearance.sh
new file mode 100644
index 00000000..a07c485c
--- /dev/null
+++ b/bash/functions/browser/set_chrome_profile_appearance.sh
@@ -0,0 +1,298 @@
+#!/usr/bin/env bash
+# set_chrome_profile_appearance — personaliza la apariencia visual de un perfil
+# Chrome/Chromium existente: asigna un avatar built-in (índice 0..55) o una imagen
+# PNG/JPG custom, y/o un color de acento (hex #rrggbb). Edita Local State.
+
+set -euo pipefail
+
+set_chrome_profile_appearance() {
+ # ── defaults ──────────────────────────────────────────────────────────────
+ local _udd=""
+ local _profile_dir=""
+ local _avatar=""
+ local _color=""
+ local _dry_run=0
+
+ # ── parse args ─────────────────────────────────────────────────────────────
+ _usage() {
+ cat >&2 <<'EOF'
+Usage: set_chrome_profile_appearance --user-data-dir --profile
+ [--avatar ] [--color <#rrggbb>] [--dry-run]
+
+ --user-data-dir Raíz del user-data-dir de Chrome/Chromium (obligatorio).
+ --profile Nombre de la carpeta del perfil, ej: Default, Automation,
+ "Profile 1" (obligatorio). El perfil debe existir.
+ --avatar Índice entero 0..55 del avatar built-in de Chrome, o ruta a
+ un archivo PNG/JPG para avatar custom (opcional).
+ --color Color de acento del perfil en formato hex #rrggbb, con o sin
+ el '#' inicial (opcional).
+ --dry-run Describe las acciones sin modificar nada.
+
+Al menos uno de --avatar o --color debe indicarse.
+
+Exit codes:
+ 0 éxito
+ 1 error de argumento o validación
+ 2 lock: hay un chromium corriendo con este user-data-dir
+ 3 el perfil no existe en info_cache de Local State
+ 4 error editando Local State (JSON inválido tras escritura)
+EOF
+ return 1
+ }
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --user-data-dir) _udd="$2"; shift 2 ;;
+ --profile) _profile_dir="$2"; shift 2 ;;
+ --avatar) _avatar="$2"; shift 2 ;;
+ --color) _color="$2"; shift 2 ;;
+ --dry-run) _dry_run=1; shift ;;
+ -h|--help) _usage; return 0 ;;
+ *) echo "set_chrome_profile_appearance: argumento desconocido: $1" >&2; return 1 ;;
+ esac
+ done
+
+ # ── validaciones obligatorias ──────────────────────────────────────────────
+ if [[ -z "$_udd" ]]; then
+ echo "set_chrome_profile_appearance: --user-data-dir es obligatorio" >&2
+ return 1
+ fi
+ if [[ -z "$_profile_dir" ]]; then
+ echo "set_chrome_profile_appearance: --profile es obligatorio" >&2
+ return 1
+ fi
+ if [[ -z "$_avatar" && -z "$_color" ]]; then
+ echo "set_chrome_profile_appearance: al menos --avatar o --color debe indicarse" >&2
+ return 1
+ fi
+
+ # Expandir ~ en el user-data-dir
+ _udd="${_udd/#\~/$HOME}"
+
+ local _local_state="${_udd}/Local State"
+
+ # Verificar que user-data-dir y Local State existen
+ if [[ ! -d "$_udd" ]]; then
+ echo "set_chrome_profile_appearance: user-data-dir no encontrado: ${_udd}" >&2
+ return 1
+ fi
+ if [[ ! -f "$_local_state" ]]; then
+ echo "set_chrome_profile_appearance: Local State no encontrado: ${_local_state}" >&2
+ return 1
+ fi
+
+ # ── validar --avatar ──────────────────────────────────────────────────────
+ local _avatar_index=-1
+ local _avatar_image_path=""
+
+ if [[ -n "$_avatar" ]]; then
+ if [[ "$_avatar" =~ ^[0-9]+$ ]]; then
+ # Índice built-in
+ _avatar_index=$(( _avatar ))
+ if [[ $_avatar_index -lt 0 || $_avatar_index -gt 55 ]]; then
+ echo "set_chrome_profile_appearance: índice de avatar fuera de rango (0..55): ${_avatar}" >&2
+ return 1
+ fi
+ else
+ # Ruta a imagen custom
+ local _img_path="${_avatar/#\~/$HOME}"
+ if [[ ! -f "$_img_path" ]]; then
+ echo "set_chrome_profile_appearance: archivo de imagen no encontrado: ${_img_path}" >&2
+ return 1
+ fi
+ _avatar_image_path="$_img_path"
+ fi
+ fi
+
+ # ── validar --color ───────────────────────────────────────────────────────
+ local _color_hex=""
+ if [[ -n "$_color" ]]; then
+ _color_hex="${_color/#\#/}" # quitar # inicial si lo hay
+ if ! [[ "$_color_hex" =~ ^[0-9a-fA-F]{6}$ ]]; then
+ echo "set_chrome_profile_appearance: color hex inválido (espera rrggbb): ${_color}" >&2
+ return 1
+ fi
+ fi
+
+ # ── guard: ningún chromium debe tener ESTE user-data-dir abierto ──────────
+ # pgrep -x chromium lista solo procesos cuyo comm es exactamente "chromium",
+ # nunca grep/pgrep/bash. Así evitamos auto-matchear el propio script cuando
+ # el path del udd contiene "chromium" (p.ej. ~/.config/chromium-cdp).
+ if [[ $_dry_run -eq 0 ]]; then
+ local _p _busy=0
+ for _p in $(pgrep -x chromium 2>/dev/null); do
+ if tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_udd"; then
+ _busy=1; break
+ fi
+ done
+ if [[ $_busy -eq 1 ]]; then
+ echo "set_chrome_profile_appearance: hay un chromium corriendo con este user-data-dir — ciérralo primero:" >&2
+ echo " pkill -TERM chromium" >&2
+ echo " (Chrome reescribe Local State al cerrar y pierde los cambios)" >&2
+ return 2
+ fi
+ fi
+
+ # ── verificar que el perfil existe en info_cache ──────────────────────────
+ if [[ $_dry_run -eq 0 ]]; then
+ local _profile_exists
+ _profile_exists="$(python3 -c "
+import json, sys
+data = json.load(open(sys.argv[1]))
+ic = data.get('profile', {}).get('info_cache', {})
+print('yes' if sys.argv[2] in ic else 'no')
+" "$_local_state" "$_profile_dir" 2>/dev/null || echo "no")"
+ if [[ "$_profile_exists" != "yes" ]]; then
+ echo "set_chrome_profile_appearance: perfil '${_profile_dir}' no existe en info_cache de Local State" >&2
+ echo " Perfiles disponibles:" >&2
+ python3 -c "
+import json, sys
+data = json.load(open(sys.argv[1]))
+ic = data.get('profile', {}).get('info_cache', {})
+for k in ic: print(' ', k)
+" "$_local_state" >&2 2>/dev/null || true
+ return 3
+ fi
+ fi
+
+ # ── modo dry-run ──────────────────────────────────────────────────────────
+ if [[ $_dry_run -eq 1 ]]; then
+ echo "=== set_chrome_profile_appearance DRY-RUN ===" >&2
+ echo " user-data-dir : ${_udd}" >&2
+ echo " profile : ${_profile_dir}" >&2
+ if [[ $_avatar_index -ge 0 ]]; then
+ echo " avatar : built-in #${_avatar_index} → avatar_icon=chrome://theme/IDR_PROFILE_AVATAR_${_avatar_index}" >&2
+ echo " is_using_default_avatar=true" >&2
+ elif [[ -n "$_avatar_image_path" ]]; then
+ local _dest_img="${_udd}/${_profile_dir}/Google Profile Picture.png"
+ echo " avatar : imagen custom ${_avatar_image_path}" >&2
+ echo " copiaría a ${_dest_img}" >&2
+ echo " is_using_default_avatar=false" >&2
+ echo " gaia_picture_file_name=Google Profile Picture.png" >&2
+ fi
+ if [[ -n "$_color_hex" ]]; then
+ local _signed_preview
+ _signed_preview="$(python3 -c "
+rgb = int('${_color_hex}', 16)
+argb = 0xFF000000 | rgb
+signed = argb - 0x100000000 if argb >= 0x80000000 else argb
+print(signed)
+" 2>/dev/null || echo '?')"
+ echo " color : #${_color_hex} → signed int32 ${_signed_preview}" >&2
+ echo " profile_highlight_color, profile_color_seed, default_avatar_fill_color" >&2
+ fi
+ echo " archivo : ${_local_state}" >&2
+ printf '{"profile":"%s","avatar_applied":%s,"color_applied":%s,"dry_run":true}\n' \
+ "$_profile_dir" \
+ "$([[ -n "$_avatar" ]] && echo 'true' || echo 'false')" \
+ "$([[ -n "$_color_hex" ]] && echo 'true' || echo 'false')"
+ return 0
+ fi
+
+ # ── backup de Local State (no sobreescribir el del mismo día) ────────────
+ local _today
+ _today="$(date +%Y%m%d)"
+ local _backup="${_local_state}.bak.${_today}"
+ if [[ ! -f "$_backup" ]]; then
+ cp "$_local_state" "$_backup"
+ fi
+
+ # ── copiar imagen custom si es necesario ──────────────────────────────────
+ local _copy_image_done=false
+ if [[ -n "$_avatar_image_path" ]]; then
+ local _profile_path="${_udd}/${_profile_dir}"
+ mkdir -p "$_profile_path"
+ cp "$_avatar_image_path" "${_profile_path}/Google Profile Picture.png"
+ _copy_image_done=true
+ fi
+
+ # ── editar Local State con python3 ────────────────────────────────────────
+ if ! python3 - \
+ "$_local_state" \
+ "$_profile_dir" \
+ "${_avatar_index}" \
+ "${_avatar_image_path}" \
+ "${_color_hex}" <<'PY'; then
+import sys, json
+
+ls_path = sys.argv[1]
+prof_dir = sys.argv[2]
+avatar_index = int(sys.argv[3]) # -1 = no cambiar avatar
+avatar_img = sys.argv[4] # "" = no usar imagen
+color_hex = sys.argv[5] # "" = no cambiar color
+
+with open(ls_path, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+profile_section = data.setdefault("profile", {})
+info_cache = profile_section.setdefault("info_cache", {})
+
+# El perfil debe existir (ya validado en bash, pero doble check)
+if prof_dir not in info_cache:
+ print(f"error: perfil '{prof_dir}' no existe en info_cache", file=sys.stderr)
+ sys.exit(1)
+
+entry = info_cache[prof_dir]
+
+# ── Avatar ────────────────────────────────────────────────────────────────────
+if avatar_index >= 0:
+ # Avatar built-in: IDR_PROFILE_AVATAR_
+ entry["avatar_icon"] = f"chrome://theme/IDR_PROFILE_AVATAR_{avatar_index}"
+ entry["is_using_default_avatar"] = True
+elif avatar_img:
+ # Avatar custom imagen: Chrome necesita gaia_picture_file_name
+ entry["avatar_icon"] = "chrome://theme/IDR_PROFILE_AVATAR_0"
+ entry["is_using_default_avatar"] = False
+ entry["gaia_picture_file_name"] = "Google Profile Picture.png"
+
+# ── Color ─────────────────────────────────────────────────────────────────────
+if color_hex:
+ rgb = int(color_hex, 16) # 0xRRGGBB
+ argb = 0xFF000000 | rgb # alpha=FF opaco → 0xFFRRGGBB
+ # Convertir a int32 con signo (Python usa enteros arbitrarios)
+ signed = argb - 0x100000000 if argb >= 0x80000000 else argb
+
+ entry["profile_highlight_color"] = signed
+ entry["profile_color_seed"] = signed
+ entry["default_avatar_fill_color"] = signed
+
+with open(ls_path, "w", encoding="utf-8") as f:
+ json.dump(data, f, separators=(",", ":"))
+PY
+ echo "set_chrome_profile_appearance: error editando Local State con python3" >&2
+ return 4
+ fi
+
+ # ── validar JSON tras escritura ───────────────────────────────────────────
+ if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then
+ echo "set_chrome_profile_appearance: JSON inválido tras escribir Local State; restaurando backup" >&2
+ cp "$_backup" "$_local_state"
+ return 4
+ fi
+
+ # ── leer valores resultantes para el JSON de salida ───────────────────────
+ local _result_json
+ _result_json="$(python3 - "$_local_state" "$_profile_dir" <<'PY'
+import json, sys
+data = json.load(open(sys.argv[1]))
+entry = data.get("profile", {}).get("info_cache", {}).get(sys.argv[2], {})
+out = {
+ "profile": sys.argv[2],
+ "avatar_icon": entry.get("avatar_icon", ""),
+ "is_using_default_avatar": entry.get("is_using_default_avatar", True),
+ "profile_highlight_color": entry.get("profile_highlight_color", 0),
+ "profile_color_seed": entry.get("profile_color_seed", 0),
+ "default_avatar_fill_color": entry.get("default_avatar_fill_color", 0),
+ "backup": "Local State.bak." + __import__("datetime").date.today().strftime("%Y%m%d"),
+}
+print(json.dumps(out, separators=(",",":")))
+PY
+)"
+
+ echo "$_result_json"
+}
+
+# ── auto-ejecución ────────────────────────────────────────────────────────────
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ set_chrome_profile_appearance "$@"
+fi