extends RefCounted
## Shared look of the Starwell (starwell_ui.gd), the Stable (stable_ui.gd) and the mount HUD (mount_hud.gd):
## the night-sky panel with a gold frame, glossy buttons, rarity colours, Kanit text, the Star Shard icon, rarity
## cards (flip, glow, shine sweep) and live 3D stages (a mount on a glowing pedestal in its own little world).
## All our own procedural art (shaders/ui_starwell*.gdshader) - no image files.

const Mounts = preload("res://scripts/server/mounts.gd")
const MountView = preload("res://scripts/mount_view.gd")
const Kanit = preload("res://scripts/ui/kanit.gd")
const UiSkin = preload("res://scripts/ui/ui_skin.gd")
const SKY = preload("res://shaders/ui_starwell.gdshader")
const ADD = preload("res://shaders/ui_starwell_add.gdshader")

const GOLD := Color("#f3cf6b")
const GOLD_DARK := Color("#9a6a1c")
const INK := Color("#fff6e0")
const INK_SOFT := Color("#c9c3e6")
const NAVY := Color("#15163a")
const PANEL_BG := Color(0.1, 0.09, 0.24, 0.92)

## Pull animation look per rarity: beam colour, particle colour, "name" plate colour.
static func rarity_col(r: String) -> Color:
	return Mounts.rarity_color(r)


static func thai() -> bool:
	return str(Settings.val("language")) == "th" if Settings.has_method("val") else false


## "Name  ·  ชื่อไทย" or one of them.
static func both(en: String, th: String) -> String:
	return "%s  ·  %s" % [en, th] if th != "" and th != en else en


# ---------------------------------------------------------------- materials / rects

static func mat(mode: int, col: Color, add: bool = true, extra: Dictionary = {}) -> ShaderMaterial:
	var m := ShaderMaterial.new()
	m.shader = ADD if add else SKY
	m.set_shader_parameter("mode", mode)
	m.set_shader_parameter("color", col)
	for k in extra:
		m.set_shader_parameter(k, extra[k])
	return m


## Full-rect effect ColorRect (mouse ignored) under `parent`.
static func fx_rect(parent: Control, mode: int, col: Color, add: bool = true, extra: Dictionary = {}) -> ColorRect:
	var r := ColorRect.new()
	r.color = Color.WHITE
	r.material = mat(mode, col, add, extra)
	r.mouse_filter = Control.MOUSE_FILTER_IGNORE
	r.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	parent.add_child(r)
	return r


static func label(text: String, fs: int = 16, col: Color = INK, weight: String = "Medium", outline: int = 0) -> Label:
	var l := Label.new()
	l.text = text
	Kanit.apply(l, fs, weight)
	l.add_theme_color_override("font_color", col)
	if outline > 0:
		l.add_theme_constant_override("outline_size", outline)
		l.add_theme_color_override("font_outline_color", Color(0.08, 0.04, 0.12, 0.95))
	l.mouse_filter = Control.MOUSE_FILTER_IGNORE
	return l


## Glossy ui_skin button in `base` colour with Kanit text.
static func button(text: String, base: Color, cb: Callable, fs: int = 17, tip: String = "") -> Button:
	var b := Button.new()
	b.text = text
	b.focus_mode = Control.FOCUS_NONE
	Kanit.apply(b, fs, "SemiBold")
	b.add_theme_color_override("font_color", Color(1, 1, 1))
	b.add_theme_color_override("font_hover_color", Color(1, 1, 0.9))
	b.add_theme_color_override("font_outline_color", base.darkened(0.6))
	b.add_theme_constant_override("outline_size", 4)
	var flats: Array = []
	for i in 4:
		var f := StyleBoxFlat.new()
		f.bg_color = [base, base.lightened(0.14), base.darkened(0.18), Color(0.45, 0.44, 0.5)][i]
		f.set_corner_radius_all(12)
		f.border_color = base.darkened(0.45)
		f.set_border_width_all(2)
		f.content_margin_left = 14
		f.content_margin_right = 14
		f.content_margin_top = 6
		f.content_margin_bottom = 6
		flats.append(f)
	var tex: Array = UiSkin.buttons(base, flats)
	var names := ["normal", "hover", "pressed", "disabled"]
	for i in 4:
		b.add_theme_stylebox_override(names[i], tex[i])
	if tip != "":
		b.tooltip_text = tip
	if cb.is_valid():
		b.pressed.connect(cb)
	return b


## Round close button (x) like the game's windows.
static func close_button(cb: Callable) -> Button:
	var x := Button.new()
	x.text = "×"
	x.focus_mode = Control.FOCUS_NONE
	Kanit.apply(x, 18, "Bold")
	x.custom_minimum_size = Vector2(38, 38)
	for st in ["normal", "hover", "pressed"]:
		var f := StyleBoxFlat.new()
		f.bg_color = Color("#e0503a") if st != "hover" else Color("#f06a50")
		f.set_corner_radius_all(19)
		f.border_color = Color("#fff0c8")
		f.set_border_width_all(3)
		f.shadow_color = Color(0, 0, 0, 0.35)
		f.shadow_size = 3
		x.add_theme_stylebox_override(st, f)
	x.add_theme_color_override("font_color", Color.WHITE)
	x.tooltip_text = "Close (Esc)"
	x.pressed.connect(cb)
	if UiSkin.v3():
		# UI v3: the game's round orange close button with a gold X
		UiSkin.style_close(x, 38.0)
	return x


## Window title: UI v3 = the game's dark leather pill with a gold medallion (ui_skin.gd), older looks = a
## gold-rimmed plate in `tint`. Two lines (EN / TH sub line). Placed centred on the window's top edge.
static func title_plate(win: Control, win_w: float, text: String, sub: String, tint: Color) -> Control:
	var plaque := PanelContainer.new()
	plaque.name = "TitlePlate"
	var tv := VBoxContainer.new()
	tv.add_theme_constant_override("separation", -6)
	plaque.add_child(tv)
	var t := label(text, 28, GOLD, "Bold", 6)
	t.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	tv.add_child(t)
	var st := label(sub, 14, INK_SOFT, "Regular", 3)
	st.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	tv.add_child(st)
	var w := 380.0
	if UiSkin.v3():
		var pl := UiSkin.pill(true)
		pl.content_margin_top = 4
		pl.content_margin_bottom = 6
		pl.content_margin_right = 34
		plaque.add_theme_stylebox_override("panel", pl)
		Kanit.apply(t, 26, "Bold")
		t.add_theme_color_override("font_color", UiSkin.TITLE_TXT)
		t.add_theme_constant_override("outline_size", 0)
		t.add_theme_color_override("font_shadow_color", Color(0.08, 0.03, 0.0, 0.6))
		t.add_theme_constant_override("shadow_offset_y", 2)
		st.add_theme_color_override("font_color", Color("#e8d2a6"))
		st.add_theme_constant_override("outline_size", 0)
		w = 420.0
	else:
		var ps := StyleBoxFlat.new()
		ps.bg_color = tint
		ps.border_color = GOLD
		ps.set_border_width_all(3)
		ps.set_corner_radius_all(22)
		ps.shadow_color = Color(tint.lightened(0.4), 0.45)
		ps.shadow_size = 12
		ps.content_margin_left = 34
		ps.content_margin_right = 34
		ps.content_margin_top = 2
		ps.content_margin_bottom = 4
		plaque.add_theme_stylebox_override("panel", ps)
	plaque.custom_minimum_size = Vector2(w, 0)
	plaque.position = Vector2(win_w * 0.5 - w * 0.5, -26)
	win.add_child(plaque)
	return plaque


## The night-sky window: backdrop shader + ornate gold frame. Returns the panel (children go on top).
static func sky_panel(sz: Vector2, seed: float = 0.0) -> Control:
	var p := Control.new()
	p.custom_minimum_size = sz
	p.size = sz
	p.mouse_filter = Control.MOUSE_FILTER_STOP
	var shadow := Panel.new()
	var ss := StyleBoxFlat.new()
	ss.bg_color = Color(0, 0, 0, 0)
	ss.shadow_color = Color(0.02, 0.0, 0.08, 0.55)
	ss.shadow_size = 22
	ss.set_corner_radius_all(20)
	shadow.add_theme_stylebox_override("panel", ss)
	shadow.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	shadow.mouse_filter = Control.MOUSE_FILTER_IGNORE
	p.add_child(shadow)
	var ins := 0.0
	if UiSkin.v3():
		# UI v3: the game's storybook page (wood frame, parchment, woven bands) holds the night sky like a
		# painted plate
		var page := Panel.new()
		page.name = "Page"
		page.add_theme_stylebox_override("panel", UiSkin.window_panel())
		page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
		page.mouse_filter = Control.MOUSE_FILTER_IGNORE
		p.add_child(page)
		ins = 14.0
	var bg := fx_rect(p, 0, Color(0.95, 0.8, 0.45, 1.0), false, {"size": sz - Vector2(ins, ins) * 2.0, "radius": 20.0 - ins * 0.6, "seed": seed})
	bg.name = "Sky"
	bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	bg.offset_left = ins
	bg.offset_top = ins
	bg.offset_right = -ins
	bg.offset_bottom = -ins
	bg.resized.connect(func(): (bg.material as ShaderMaterial).set_shader_parameter("size", bg.size))
	var fr := Frame.new()
	fr.name = "Frame"
	fr.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	if ins > 0.0:
		fr.offset_left = ins
		fr.offset_top = ins
		fr.offset_right = -ins
		fr.offset_bottom = -ins
		fr.r = 12.0
		fr.gem = Color("#c4923a")
	p.add_child(fr)
	return p


## Star Shard counter pill (icon + number).
static func shard_pill(n: int, fs: int = 20) -> Control:
	var pill := PanelContainer.new()
	var st := StyleBoxFlat.new()
	st.bg_color = Color(0.05, 0.05, 0.16, 0.85)
	st.border_color = GOLD
	st.set_border_width_all(2)
	st.set_corner_radius_all(18)
	st.content_margin_left = 8
	st.content_margin_right = 16
	st.content_margin_top = 2
	st.content_margin_bottom = 2
	pill.add_theme_stylebox_override("panel", st)
	if UiSkin.v3():
		var lp := UiSkin.pill(false)
		lp.content_margin_left = 10
		lp.content_margin_right = 18
		pill.add_theme_stylebox_override("panel", lp)
	var h := HBoxContainer.new()
	h.add_theme_constant_override("separation", 6)
	pill.add_child(h)
	var ic := ShardIcon.new()
	ic.custom_minimum_size = Vector2(fs + 12, fs + 12)
	h.add_child(ic)
	var l := label(str(n), fs, INK, "SemiBold", 4)
	l.name = "Count"
	h.add_child(l)
	pill.tooltip_text = "Star Shards (เศษดวงดาว) - earned in play: quests, mini-bosses / MVPs, new areas, daily login.\nNo real money, ever."
	return pill


# ---------------------------------------------------------------- drawn widgets

## Ornate gold frame (double line, corner gems, soft inner glow) over a panel.
class Frame extends Control:
	var edge := Color("#f3cf6b")
	var gem := Color("#7c3cc8")
	var r := 20.0

	func _init() -> void:
		mouse_filter = Control.MOUSE_FILTER_IGNORE

	func _draw() -> void:
		var rc := Rect2(Vector2.ZERO, size)
		var o := StyleBoxFlat.new()
		o.bg_color = Color(0, 0, 0, 0)
		o.draw_center = false
		o.border_color = edge
		o.set_border_width_all(3)
		o.set_corner_radius_all(int(r))
		o.anti_aliasing = true
		draw_style_box(o, rc)
		var i := o.duplicate() as StyleBoxFlat
		i.border_color = Color(edge.r, edge.g, edge.b, 0.45)
		i.set_border_width_all(1)
		i.set_corner_radius_all(int(r) - 6)
		draw_style_box(i, rc.grow(-7))
		var dk := StyleBoxFlat.new()
		dk.draw_center = false
		dk.border_color = Color(0.25, 0.14, 0.02, 0.9)
		dk.set_border_width_all(1)
		dk.set_corner_radius_all(int(r) + 1)
		draw_style_box(dk, rc.grow(1))
		# corner gems
		for c in [Vector2(r * 0.55, r * 0.55), Vector2(size.x - r * 0.55, r * 0.55), Vector2(r * 0.55, size.y - r * 0.55), Vector2(size.x - r * 0.55, size.y - r * 0.55)]:
			_gem(c, 7.0)
		# top centre crest
		_gem(Vector2(size.x * 0.5, 0), 10.0)

	func _gem(c: Vector2, s: float) -> void:
		var pts := PackedVector2Array([c + Vector2(0, -s), c + Vector2(s * 0.8, 0), c + Vector2(0, s), c + Vector2(-s * 0.8, 0)])
		draw_colored_polygon(pts, gem)
		draw_polyline(PackedVector2Array([pts[0], pts[1], pts[2], pts[3], pts[0]]), edge, 2.0, true)
		draw_circle(c + Vector2(-s * 0.2, -s * 0.3), s * 0.22, Color(1, 1, 1, 0.7))


## Star Shard icon: a four-point crystal star with a glow.
class ShardIcon extends Control:
	var col := Color("#9fd8ff")

	func _init() -> void:
		mouse_filter = Control.MOUSE_FILTER_IGNORE

	func _draw() -> void:
		var c := size * 0.5
		var s := minf(size.x, size.y) * 0.5
		for k in 4:
			draw_circle(c, s * (0.9 - k * 0.15), Color(col.r, col.g, col.b, 0.07 + k * 0.03))
		var pts := PackedVector2Array()
		for i in 8:
			var a := -PI / 2.0 + i * PI / 4.0
			var rr := s * (0.95 if i % 2 == 0 else 0.3)
			pts.append(c + Vector2(cos(a), sin(a)) * rr)
		draw_colored_polygon(pts, Color("#5aa8ff"))
		var hi := PackedVector2Array()
		for i in 8:
			var a2 := -PI / 2.0 + i * PI / 4.0
			var r2 := s * (0.62 if i % 2 == 0 else 0.18)
			hi.append(c + Vector2(cos(a2), sin(a2)) * r2 + Vector2(-s * 0.06, -s * 0.06))
		draw_colored_polygon(hi, Color("#e8f6ff"))
		pts.append(pts[0])
		draw_polyline(pts, Color("#1f3a8a"), 1.5, true)


## Five-point stars row (mount stars 0..5).
class StarRow extends Control:
	var n := 0
	var total := 5
	var on_col := Color("#ffd24a")

	func _init() -> void:
		mouse_filter = Control.MOUSE_FILTER_IGNORE

	func _draw() -> void:
		var s := size.y * 0.5
		for i in total:
			var c := Vector2(s + i * s * 2.1, s)
			var pts := PackedVector2Array()
			for k in 10:
				var a := -PI / 2.0 + k * PI / 5.0
				pts.append(c + Vector2(cos(a), sin(a)) * s * (1.0 if k % 2 == 0 else 0.45))
			draw_colored_polygon(pts, on_col if i < n else Color(0.3, 0.3, 0.42, 0.8))
			pts.append(pts[0])
			draw_polyline(pts, Color(0.25, 0.14, 0.02, 0.9), 1.0, true)


## A mount in its own little 3D world (transparent background): glowing pedestal, key / rim lights, glow.
## live = keeps rendering (turntable), else renders once when ready (card portraits).
class Stage extends SubViewportContainer:
	var vp: SubViewport
	var root: Node3D
	var pivot: Node3D
	var cam: Camera3D
	var view = null
	var live := true
	var spin := 0.45
	var mount := ""
	var ring_mat: ShaderMaterial
	var rim: DirectionalLight3D
	var pedestal := true
	var _frames := 0
	var _drag := false

	func _init(sz: Vector2i = Vector2i(400, 400), live_: bool = true, with_pedestal: bool = true) -> void:
		live = live_
		pedestal = with_pedestal
		stretch = true
		custom_minimum_size = Vector2(sz)
		mouse_filter = Control.MOUSE_FILTER_PASS if live_ else Control.MOUSE_FILTER_IGNORE
		vp = SubViewport.new()
		vp.size = sz
		vp.transparent_bg = true
		vp.own_world_3d = true
		vp.msaa_3d = Viewport.MSAA_4X
		vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS if live_ else SubViewport.UPDATE_DISABLED
		add_child(vp)
		root = Node3D.new()
		vp.add_child(root)
		var we := WorldEnvironment.new()
		var env := Environment.new()
		env.background_mode = Environment.BG_CLEAR_COLOR
		env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
		env.ambient_light_color = Color(0.78, 0.78, 0.95)
		# Boss 2026-10-01: the 3D gacha stages were washed out (clipped whites / gold) - lower exposure, bloom
		# only on real highlights, softer key + rim
		env.ambient_light_energy = 0.42
		env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
		env.tonemap_exposure = 0.88
		env.tonemap_white = 6.0
		env.glow_enabled = true
		env.glow_intensity = 0.32
		env.glow_bloom = 0.0
		env.glow_hdr_threshold = 1.45
		we.environment = env
		root.add_child(we)
		var sun := DirectionalLight3D.new()
		sun.rotation = Vector3(deg_to_rad(-48), deg_to_rad(28), 0)
		sun.light_energy = 0.95
		root.add_child(sun)
		rim = DirectionalLight3D.new()
		rim.rotation = Vector3(deg_to_rad(-20), deg_to_rad(200), 0)
		rim.light_energy = 0.5
		root.add_child(rim)
		cam = Camera3D.new()
		cam.fov = 30.0
		root.add_child(cam)
		if pedestal:
			_build_pedestal()
		pivot = Node3D.new()
		root.add_child(pivot)

	func _build_pedestal() -> void:
		var disc := MeshInstance3D.new()
		var cm := CylinderMesh.new()
		cm.top_radius = 1.45
		cm.bottom_radius = 1.6
		cm.height = 0.22
		cm.radial_segments = 40
		disc.mesh = cm
		var sm := StandardMaterial3D.new()
		sm.albedo_color = Color(0.3, 0.28, 0.5)
		sm.metallic = 0.2
		sm.roughness = 0.5
		disc.material_override = sm
		disc.position.y = -0.11
		root.add_child(disc)
		var trim := MeshInstance3D.new()
		var tm := TorusMesh.new()
		tm.inner_radius = 1.44
		tm.outer_radius = 1.52
		tm.rings = 48
		trim.mesh = tm
		var gm := StandardMaterial3D.new()
		gm.albedo_color = Color("#f3cf6b")
		gm.metallic = 0.8
		gm.roughness = 0.3
		gm.emission_enabled = true
		gm.emission = Color(1, 0.8, 0.4)
		gm.emission_energy_multiplier = 0.3
		trim.material_override = gm
		trim.position.y = 0.0
		root.add_child(trim)
		var ring := MeshInstance3D.new()
		var pm := PlaneMesh.new()
		pm.size = Vector2(3.4, 3.4)
		ring.mesh = pm
		ring_mat = ShaderMaterial.new()
		ring_mat.shader = load("res://shaders/fx_glow.gdshader")
		ring_mat.set_shader_parameter("shape", 6)
		ring_mat.set_shader_parameter("billboard", false)
		ring_mat.set_shader_parameter("color", Color(0.7, 0.8, 1.0, 0.9))
		ring_mat.set_shader_parameter("energy", 1.3)
		ring_mat.set_shader_parameter("ring_width", 0.08)
		ring_mat.set_shader_parameter("fill", 0.12)
		ring.material_override = ring_mat
		ring.position.y = 0.02
		root.add_child(ring)

	## Shows mount `id` (rarity colour lights the rim + pedestal ring). yaw = starting turn.
	func set_mount(id: String, yaw: float = -0.55) -> void:
		mount = id
		if view:
			view.queue_free()
			view = null
		if id == "":
			return
		view = MountView.new()
		view.name = "Mount"
		pivot.add_child(view)
		view.setup(id)
		pivot.rotation.y = yaw
		var col: Color = Mounts.rarity_color(Mounts.rarity_of(id)) if Mounts.exists(id) else Color(1, 0.85, 0.5)
		rim.light_color = col.lightened(0.2)
		if ring_mat:
			ring_mat.set_shader_parameter("color", Color(col.r, col.g, col.b, 0.8))
		var h := float(view.cfg.get("height", 2.0))
		var dist := h * 2.35 + 1.4
		cam.position = Vector3(0, h * 0.62 + 0.25, dist)
		cam.look_at(Vector3(0, h * 0.42, 0))
		_frames = 0
		if not live:
			vp.render_target_update_mode = SubViewport.UPDATE_DISABLED

	func _process(delta: float) -> void:
		if view:
			view.animate(delta, false)
		if live:
			if not _drag:
				pivot.rotation.y += delta * spin
		else:
			_frames += 1
			if _frames == 4:
				# the rig is posed: one render for the card portrait
				vp.render_target_update_mode = SubViewport.UPDATE_ONCE

	func _gui_input(ev: InputEvent) -> void:
		if not live:
			return
		if ev is InputEventMouseButton and (ev as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT:
			_drag = (ev as InputEventMouseButton).pressed
		elif ev is InputEventMouseMotion and _drag:
			pivot.rotation.y += (ev as InputEventMouseMotion).relative.x * 0.012


## Rarity card: face (rarity gradient, glow, portrait stage, name, stars, badge) or back (for the flip).
class Card extends Control:
	var id := ""
	var rarity := "common"
	var owned := true
	var stars := 0
	var badge := ""
	var show_face := true
	var t := 0.0
	var stage = null
	var shine: ColorRect = null
	var _name: Label
	var _sub: Label
	var _stars = null
	var _badge: Label

	func _init() -> void:
		mouse_filter = Control.MOUSE_FILTER_PASS

	## sz: card size; portrait = a 3D stage (static render) inside.
	func setup(mid: String, sz: Vector2, own := true, st := 0, bdg := "", portrait := true) -> void:
		id = mid
		rarity = Mounts.rarity_of(mid)
		owned = own
		stars = st
		badge = bdg
		custom_minimum_size = sz
		size = sz
		pivot_offset = sz * 0.5
		clip_contents = false
		var d := Mounts.def(mid)
		if portrait:
			var ps := Vector2i(int(sz.x - 12), int(sz.y * 0.64))
			stage = Stage.new(ps, false, false)
			stage.position = Vector2(6, 26)
			stage.size = Vector2(ps)
			add_child(stage)
			stage.set_mount(mid, -0.7)
			if not owned:
				stage.modulate = Color(0.08, 0.08, 0.16, 0.9)
		if rarity in ["epic", "mythic"]:
			var sh := ColorRect.new()
			sh.color = Color.WHITE
			var m := ShaderMaterial.new()
			m.shader = load("res://shaders/ui_starwell_add.gdshader")
			m.set_shader_parameter("mode", 4)
			m.set_shader_parameter("color", Color(1, 1, 1, 0.55 if rarity == "mythic" else 0.35))
			m.set_shader_parameter("speed", 2.4 if rarity == "mythic" else 3.2)
			m.set_shader_parameter("seed", randf())
			sh.material = m
			sh.mouse_filter = Control.MOUSE_FILTER_IGNORE
			sh.position = Vector2(3, 3)
			sh.size = sz - Vector2(6, 6)
			add_child(sh)
			shine = sh
		_name = Label.new()
		_name.text = str(d.get("name", mid)) if owned else "???"
		Kanit.apply(_name, clampi(int(sz.x / 10.5), 12, 22), "SemiBold")
		_name.add_theme_color_override("font_color", Color.WHITE)
		_name.add_theme_constant_override("outline_size", 5)
		_name.add_theme_color_override("font_outline_color", Color(0.06, 0.03, 0.1))
		_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
		_name.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
		_name.position = Vector2(4, sz.y * 0.7)
		_name.size = Vector2(sz.x - 8, 30)
		_name.mouse_filter = Control.MOUSE_FILTER_IGNORE
		add_child(_name)
		_sub = Label.new()
		_sub.text = str(d.get("name_th", "")) if owned else str(Mounts.rarity(rarity).get("name", ""))
		Kanit.apply(_sub, clampi(int(sz.x / 14.0), 10, 16), "Regular")
		_sub.add_theme_color_override("font_color", Color(1, 0.95, 0.85, 0.9))
		_sub.add_theme_constant_override("outline_size", 4)
		_sub.add_theme_color_override("font_outline_color", Color(0.06, 0.03, 0.1))
		_sub.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
		_sub.position = Vector2(4, sz.y * 0.7 + clampf(sz.x / 8.0, 16, 28))
		_sub.size = Vector2(sz.x - 8, 22)
		_sub.mouse_filter = Control.MOUSE_FILTER_IGNORE
		add_child(_sub)
		if owned:
			var sr := StarRow.new()
			sr.n = stars
			var sh2 := clampf(sz.x / 16.0, 9.0, 14.0)
			sr.size = Vector2(sh2 * 2.1 * 5, sh2)
			sr.position = Vector2((sz.x - sr.size.x) * 0.5 + sh2 * 0.05, sz.y - sh2 - 9)
			add_child(sr)
			_stars = sr
		_badge = Label.new()
		_badge.text = badge
		Kanit.apply(_badge, clampi(int(sz.x / 11.0), 11, 18), "Bold")
		_badge.add_theme_color_override("font_color", Color("#fff3a8"))
		_badge.add_theme_constant_override("outline_size", 5)
		_badge.add_theme_color_override("font_outline_color", Color("#b0261a") if badge.begins_with("NEW") else Color(0.1, 0.05, 0.2))
		_badge.position = Vector2(sz.x - 64, 2)
		_badge.size = Vector2(60, 24)
		_badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
		_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
		add_child(_badge)
		_face(show_face)

	func _face(on: bool) -> void:
		show_face = on
		for c in [stage, shine, _name, _sub, _stars, _badge]:
			if c != null:
				(c as CanvasItem).visible = on
		queue_redraw()

	## Flip animation: 0 = back, 1 = face (scale.x folds through 0 at the half-way point).
	func set_flip(f: float) -> void:
		scale.x = maxf(0.02, absf(cos(f * PI)))
		var want := f >= 0.5
		if want != show_face:
			_face(want)

	func _process(delta: float) -> void:
		t += delta
		if rarity in ["epic", "mythic"] and show_face:
			queue_redraw()

	func _draw() -> void:
		var rc := Rect2(Vector2.ZERO, size)
		var col: Color = Mounts.rarity_color(rarity)
		if not show_face:
			var bk := StyleBoxFlat.new()
			bk.bg_color = Color("#1b2260")
			bk.border_color = Color("#f3cf6b")
			bk.set_border_width_all(3)
			bk.set_corner_radius_all(12)
			bk.shadow_color = Color(0, 0, 0, 0.4)
			bk.shadow_size = 6
			draw_style_box(bk, rc)
			var c := size * 0.5
			for k in 3:
				draw_arc(c, size.x * (0.18 + k * 0.08), 0, TAU, 40, Color(0.95, 0.8, 0.4, 0.5 - k * 0.12), 2.0, true)
			var pts := PackedVector2Array()
			for i in 8:
				var a := -PI / 2.0 + i * PI / 4.0
				pts.append(c + Vector2(cos(a), sin(a)) * size.x * (0.2 if i % 2 == 0 else 0.07))
			draw_colored_polygon(pts, Color("#ffe08a"))
			return
		var glow := 0.0
		if rarity == "mythic":
			glow = 16.0 + sin(t * 3.0) * 5.0
		elif rarity == "epic":
			glow = 10.0 + sin(t * 2.4) * 3.0
		var bg := StyleBoxFlat.new()
		bg.bg_color = col.darkened(0.55) if owned else Color(0.14, 0.14, 0.22)
		bg.set_corner_radius_all(12)
		bg.border_color = col.lightened(0.15) if owned else Color(0.35, 0.35, 0.45)
		bg.set_border_width_all(3)
		bg.shadow_color = Color(col.r, col.g, col.b, 0.75) if glow > 0.0 else Color(0, 0, 0, 0.35)
		bg.shadow_size = int(glow) if glow > 0.0 else 5
		draw_style_box(bg, rc)
		# rarity light from the top + dark foot for the name
		var top := Rect2(Vector2(3, 3), Vector2(size.x - 6, size.y * 0.55))
		draw_rect(top, Color(col.r, col.g, col.b, 0.28 if owned else 0.08))
		var foot := Rect2(Vector2(3, size.y * 0.66), Vector2(size.x - 6, size.y * 0.34 - 3))
		draw_rect(foot, Color(0.03, 0.02, 0.08, 0.55))
		var inner := StyleBoxFlat.new()
		inner.draw_center = false
		inner.border_color = Color(1, 0.9, 0.6, 0.55)
		inner.set_border_width_all(1)
		inner.set_corner_radius_all(9)
		draw_style_box(inner, rc.grow(-6))
		# rarity chip (top-left)
		var chip := Rect2(Vector2(8, 6), Vector2(minf(size.x * 0.46, 90.0), 18))
		var cs := StyleBoxFlat.new()
		cs.bg_color = col.darkened(0.45 if rarity == "mythic" else 0.2)
		cs.set_corner_radius_all(9)
		draw_style_box(cs, chip)
		var f: Font = Kanit.font("SemiBold")
		draw_string(f, chip.position + Vector2(8, 14), str(Mounts.rarity(rarity).get("name", "")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, chip.size.x - 10, 11, Color.WHITE)

