diff --git a/app/assets/stylesheets/alt_styles/index.sass b/app/assets/stylesheets/alt_styles/index.sass
index f2586027..66f146c1 100644
--- a/app/assets/stylesheets/alt_styles/index.sass
+++ b/app/assets/stylesheets/alt_styles/index.sass
@@ -1,9 +1,9 @@
 @import "../partials/clean/constants"
 
-// Prefer to break the name at certain points.
+// Prefer to break the name at visually appealing points.
 .rainbow-pool-list
-	.name span
-		display: inline-block
+	.name
+		text-wrap: balance
 
 // De-emphasize Prismatic styles, in browsers that support it.
 .rainbow-pool-filters
diff --git a/app/controllers/alt_styles_controller.rb b/app/controllers/alt_styles_controller.rb
index 9da54fde..aad704c6 100644
--- a/app/controllers/alt_styles_controller.rb
+++ b/app/controllers/alt_styles_controller.rb
@@ -61,7 +61,8 @@ class AltStylesController < ApplicationController
 	protected
 
 	def alt_style_params
-		params.require(:alt_style).permit(:real_series_name, :thumbnail_url)
+		params.require(:alt_style).
+			permit(:real_series_name, :real_full_name, :thumbnail_url)
 	end
 
 	def find_color
diff --git a/app/javascript/wardrobe-2020/WardrobePage/OutfitControls.js b/app/javascript/wardrobe-2020/WardrobePage/OutfitControls.js
index bdd98f54..65465378 100644
--- a/app/javascript/wardrobe-2020/WardrobePage/OutfitControls.js
+++ b/app/javascript/wardrobe-2020/WardrobePage/OutfitControls.js
@@ -233,7 +233,7 @@ function OutfitControls({
 										/>
 									</DarkMode>
 								</Box>
-								<Flex flex="0 0 auto" align="center" pl="2">
+								<Flex flex="0 1 auto" align="center" pl="2">
 									<PosePicker
 										speciesId={outfitState.speciesId}
 										colorId={outfitState.colorId}
diff --git a/app/javascript/wardrobe-2020/WardrobePage/PosePicker.js b/app/javascript/wardrobe-2020/WardrobePage/PosePicker.js
index d13db88d..f9dee373 100644
--- a/app/javascript/wardrobe-2020/WardrobePage/PosePicker.js
+++ b/app/javascript/wardrobe-2020/WardrobePage/PosePicker.js
@@ -283,7 +283,10 @@ const PosePickerButton = React.forwardRef(
 		const theme = useTheme();
 
 		const icon = altStyle != null ? twemojiSunglasses : getIcon(pose);
-		const label = altStyle != null ? altStyle.seriesMainName : getLabel(pose);
+		const label =
+			altStyle != null
+				? altStyle.seriesMainName.split(/\s+/)[0]
+				: getLabel(pose);
 
 		return (
 			<ClassNames>
@@ -336,9 +339,9 @@ const PosePickerButton = React.forwardRef(
 						ref={ref}
 					>
 						<EmojiImage src={icon} alt="Style" />
-						<Box width=".5em" />
-						{label}
-						<Box width=".5em" />
+						<Box overflow="hidden" textOverflow="ellipsis" marginX=".5em">
+							{label}
+						</Box>
 						<ChevronDownIcon />
 					</Button>
 				)}
diff --git a/app/models/alt_style.rb b/app/models/alt_style.rb
index b343fdc1..751ca3e8 100644
--- a/app/models/alt_style.rb
+++ b/app/models/alt_style.rb
@@ -9,11 +9,15 @@ class AltStyle < ApplicationRecord
   has_many :contributions, as: :contributed, inverse_of: :contributed
 
   validates :body_id, presence: true
+  validates :full_name, presence: true, allow_nil: true
   validates :series_name, presence: true, allow_nil: true
   validates :thumbnail_url, presence: true
 
   before_validation :infer_thumbnail_url, unless: :thumbnail_url?
 
+  fallback_for(:full_name) { "#{series_name} #{pet_name}" }
+  fallback_for(:series_name) { AltStyle.placeholder_name }
+
   scope :matching_name, ->(series_name, color_name, species_name) {
     color = Color.find_by_name!(color_name)
     species = Species.find_by_name!(species_name)
@@ -54,28 +58,6 @@ class AltStyle < ApplicationRecord
 
   alias_method :name, :pet_name
 
-  # If the series_name hasn't yet been set manually by support staff, show the
-  # string "<New?>" instead. But it won't be searchable by that string—that is,
-  # `fits:<New?>-faerie-draik` intentionally will not work, and the canonical
-  # filter name will be `fits:alt-style-IDNUMBER`, instead.
-  def series_name
-    real_series_name || AltStyle.placeholder_name
-  end
-
-  def real_series_name=(new_series_name)
-    self[:series_name] = new_series_name
-  end
-
-  def real_series_name
-    self[:series_name]
-  end
-
-  # You can use this to check whether `series_name` is returning the actual
-  # value or its placeholder value.
-  def real_series_name?
-    real_series_name.present?
-  end
-
   def series_main_name
     series_name.split(': ').last
   end
@@ -84,12 +66,9 @@ class AltStyle < ApplicationRecord
     series_name.split(': ').first
   end
 
+  # Returns the full name, with the species removed from the end (if present).
   def adjective_name
-    "#{series_name} #{color.human_name}"
-  end
-
-  def full_name
-    "#{series_name} #{name}"
+    full_name.sub(/\s+#{Regexp.escape(species.name)}\Z/i, "")
   end
 
   EMPTY_IMAGE_URL = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 71a1a03c..a02a0e36 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,35 @@
 class ApplicationRecord < ActiveRecord::Base
   self.abstract_class = true
+
+  # When the given attribute's value is nil, return the given block's value
+  # instead. This is useful for fields where there's a sensible derived value
+  # to use as the default for display purposes, but it's distinctly *not* the
+  # true value, and should be recognizable as such.
+  #
+  # This also creates methods `real_<attr>`, `real_<attr>=`, and `real_<attr>?`,
+  # to work with the actual attribute when necessary.
+  #
+  # It also creates `fallback_<attr>`, to find what the fallback value *would*
+  # be if the attribute's value were nil.
+  def self.fallback_for(attribute_name, &block)
+    define_method attribute_name do
+      read_attribute(attribute_name) || instance_eval(&block)
+    end
+
+    define_method "real_#{attribute_name}" do
+      read_attribute(attribute_name)
+    end
+
+    define_method "real_#{attribute_name}?" do
+      read_attribute(attribute_name).present?
+    end
+
+    define_method "real_#{attribute_name}=" do |new_value|
+      write_attribute(attribute_name, new_value)
+    end
+
+    define_method "fallback_#{attribute_name}" do
+      instance_eval(&block)
+    end
+  end
 end
\ No newline at end of file
diff --git a/app/views/alt_styles/_alt_style.html.haml b/app/views/alt_styles/_alt_style.html.haml
index 9be4b858..9f20ab94 100644
--- a/app/views/alt_styles/_alt_style.html.haml
+++ b/app/views/alt_styles/_alt_style.html.haml
@@ -1,12 +1,12 @@
 %li
 	= link_to view_or_edit_alt_style_url(alt_style) do
 		= image_tag alt_style.preview_image_url, class: "preview", loading: "lazy"
-		.name
-			%span= alt_style.series_name
-			%span= alt_style.pet_name
+		.name= alt_style.full_name
 		.info
 			%p
 				Added
 				= time_tag alt_style.created_at,
 					title: alt_style.created_at.to_formatted_s(:long_nst) do
-					= time_with_only_month_if_old alt_style.created_at
\ No newline at end of file
+					= time_with_only_month_if_old alt_style.created_at
+			- if support_staff? && !alt_style.real_series_name?
+				%p ⚠️ Needs series name
diff --git a/app/views/alt_styles/edit.html.haml b/app/views/alt_styles/edit.html.haml
index d6ee1835..6daaa8ba 100644
--- a/app/views/alt_styles/edit.html.haml
+++ b/app/views/alt_styles/edit.html.haml
@@ -22,6 +22,10 @@
 			= f.text_field :real_series_name, autofocus: !@alt_style.real_series_name?,
 				placeholder: AltStyle.placeholder_name
 
+		= f.field do
+			= f.label :real_series_name, "Full name"
+			= f.text_field :real_full_name, placeholder: @alt_style.fallback_full_name
+
 		= f.field do
 			= f.label :thumbnail_url, "Thumbnail"
 			= f.thumbnail_input :thumbnail_url
diff --git a/db/migrate/20250216041650_add_full_name_to_alt_styles.rb b/db/migrate/20250216041650_add_full_name_to_alt_styles.rb
new file mode 100644
index 00000000..0d27e085
--- /dev/null
+++ b/db/migrate/20250216041650_add_full_name_to_alt_styles.rb
@@ -0,0 +1,5 @@
+class AddFullNameToAltStyles < ActiveRecord::Migration[8.0]
+  def change
+    add_column :alt_styles, :full_name, :string, null: true
+  end
+end
diff --git a/db/openneo_id_schema.rb b/db/openneo_id_schema.rb
index e6d281a4..250d7a4d 100644
--- a/db/openneo_id_schema.rb
+++ b/db/openneo_id_schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.2].define(version: 2024_04_08_120359) do
+ActiveRecord::Schema[8.0].define(version: 2024_04_08_120359) do
   create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
     t.string "name", limit: 30, null: false
     t.string "encrypted_password", limit: 64
diff --git a/db/schema.rb b/db/schema.rb
index 6c888b83..809c71ff 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
+ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
   create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
     t.integer "species_id", null: false
     t.integer "color_id", null: false
@@ -19,6 +19,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
     t.datetime "updated_at", precision: nil, null: false
     t.string "series_name"
     t.string "thumbnail_url", null: false
+    t.string "full_name"
     t.index ["color_id"], name: "index_alt_styles_on_color_id"
     t.index ["species_id"], name: "index_alt_styles_on_species_id"
   end
diff --git a/lib/tasks/neopets/import/styling_studio.rake b/lib/tasks/neopets/import/styling_studio.rake
index 8284c393..322eaa72 100644
--- a/lib/tasks/neopets/import/styling_studio.rake
+++ b/lib/tasks/neopets/import/styling_studio.rake
@@ -47,6 +47,14 @@ namespace "neopets:import" do
 					next
 				end
 
+				if !record.real_full_name?
+					record.full_name = style[:name]
+					puts "✅ [#{label}]: Full name is now #{style[:name].inspect}"
+				elsif record.full_name != style[:name]
+					puts "⚠️  [#{label}: Full name may have changed, handle manually? " +
+						"#{record.full_name.inspect} -> #{style[:name].inspect}"
+				end
+
 				if !record.real_thumbnail_url?
 					record.thumbnail_url = style[:image]
 					puts "✅ [#{label}]: Thumbnail URL is now #{style[:image].inspect}"
@@ -72,7 +80,7 @@ namespace "neopets:import" do
 					end
 				else
 					puts "⚠️  [#{label}]: Unable to detect series name, handle manually? " +
-							"#{record.full_name.inspect} -> #{style[:name].inspect}"
+							"#{record.pet_name.inspect} <-> #{style[:name].inspect}"
 				end
 
 				if record.changed?
diff --git a/spec/models/alt_style_spec.rb b/spec/models/alt_style_spec.rb
new file mode 100644
index 00000000..38546838
--- /dev/null
+++ b/spec/models/alt_style_spec.rb
@@ -0,0 +1,112 @@
+require_relative '../rails_helper'
+
+RSpec.describe AltStyle do
+	fixtures :colors, :species
+
+	describe "series name" do
+		subject(:alt_style) { AltStyle.new }
+
+		describe "for a new alt style" do
+			it("returns a placeholder value by default") do
+				expect(alt_style.series_name).to eq "<New?>"
+			end
+
+			it("has no real_series_name by default") do
+				expect(alt_style.real_series_name?).to be false
+				expect(alt_style.real_series_name).to be nil
+			end
+		end
+
+		describe "with a real series name" do
+			before { alt_style.real_series_name = "Nostalgic" }
+
+			it("returns the real series name") do
+				expect(alt_style.series_name).to eq "Nostalgic"
+			end
+
+			it("has a real_series_name field too") do
+				expect(alt_style.real_series_name?).to be true
+				expect(alt_style.real_series_name).to eq "Nostalgic"
+			end
+		end
+	end
+
+	describe "full name" do
+		subject(:alt_style) do
+			AltStyle.new(color: colors(:blue), species: species(:acara))
+		end
+
+		describe "for a new alt style" do
+			it("returns a placeholder value by default") do
+				expect(alt_style.full_name).to eq "<New?> Blue Acara"
+			end
+
+			it("has no real_full_name by default") do
+				expect(alt_style.real_full_name?).to be false
+				expect(alt_style.real_full_name).to be nil
+			end
+		end
+
+		describe "with a real series name, but no full name" do
+			before { alt_style.real_series_name = "Nostalgic" }
+
+			it("returns a placeholder value including the real series name") do
+				expect(alt_style.full_name).to eq "Nostalgic Blue Acara"
+			end
+
+			it("still has no real_full_name") do
+				expect(alt_style.real_full_name?).to be false
+				expect(alt_style.real_full_name).to eq nil
+			end
+		end
+
+		describe "with a real full name" do
+			before { alt_style.real_full_name = "Cute Lil Guy" }
+
+			it("returns the real full name") do
+				expect(alt_style.full_name).to eq "Cute Lil Guy"
+			end
+
+			it("has a real_full_name field too") do
+				expect(alt_style.real_full_name?).to be true
+				expect(alt_style.real_full_name).to eq "Cute Lil Guy"
+			end
+		end
+	end
+
+	describe "adjective name" do
+		subject(:alt_style) do
+			AltStyle.new(color: colors(:blue), species: species(:acara))
+		end
+
+		describe "for a new alt style" do
+			it("returns the placeholder series name and color name") do
+				expect(alt_style.adjective_name).to eq "<New?> Blue"
+			end
+		end
+
+		describe "with a real series name, but no full name" do
+			before { alt_style.series_name = "Nostalgic" }
+
+			it("returns the series name and color name") do
+				expect(alt_style.adjective_name).to eq "Nostalgic Blue"
+			end
+		end
+
+		describe "with a real full name that ends with the species name" do
+			before { alt_style.real_full_name = "Cute Lil Acara" }
+
+			it("returns the real full name minus the species") do
+				expect(alt_style.adjective_name).to eq "Cute Lil"
+			end
+		end
+
+		describe "with a real full name that does not end with the species name" do
+			before { alt_style.real_full_name = "Cute Lil Guy" }
+
+			it("returns the real full name") do
+				expect(alt_style.adjective_name).to eq "Cute Lil Guy"
+			end
+		end
+	end
+end