Repository: MegaMek/mekhq
Pull request #6894: Improvement: Improved Person View to Include a Larger Portrait & Upgraded Several Components; Moved Medical Record Button to Personnel Right-Click Menu
Author: login: IllianiBird
Labels: name: Personnel
description: Personnel-related Issues
name: GUI
name: Improvement to Existing Feature
description: Used with the RFE tag to indicate an improvement to an existing feature
Created: 2025-05-07T00:15:44Z | Merged: 2025-05-07T15:36:34Z

### Requires #6839

Fix #4451
Fix #6326

### Dev Notes
The goal of this change was to break up the visual 'blocking' of the display so that users could more easily pick out the information they needed, while also providing better support for attribute modifiers and role-play skills.

The little green 'medical' symbol previously linked the user to the characters' medical history. That has been removed, instead the medical report can be accessed from the characters' right-click menu.

### Before
<img width="503" alt="image" src="https://github.com/user-attachments/assets/133dbbc3-8c9a-4a1c-b151-a01e5065c2c9" />

### After
<img width="610" alt="image" src="https://github.com/user-attachments/assets/edae869a-2c77-45f5-863f-48a1f3825752" />

(The genealogy is further down in the view)

<img width="876" alt="image" src="https://github.com/user-attachments/assets/a7965402-0f28-4676-99e8-fa1110aa1baa" />

<img width="580" alt="image" src="https://github.com/user-attachments/assets/3ac45ea5-86eb-47c2-97e6-196bb80a27ca" />


Linked issue #4451: RFE: Larger/Adjustable Portrait Display Size
Fixed display size of character portraits within MekHQ is rather small. 

A larger default size or the option to adjust portrait sizes within the MekHQ>Personnel Tab

Diff:
diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties
index bac1c9f8f8f..e06515b98f7 100644
--- a/MekHQ/resources/mekhq/resources/GUI.properties
+++ b/MekHQ/resources/mekhq/resources/GUI.properties
@@ -88,6 +88,7 @@ enterNewCallsign.text=Enter new callsign
 editCallsign.text=Edit Callsign
 changeSalary.text=Change Salary (-1 to remove custom salary)
 makeSkillCheck.text=Perform Skill Check
+viewMedicalRecords.text=View Medical Records
 changeRank.text=Change Rank
 changeRankSystem.text=Change Rank System
 useCampaignRankSystem.text=Use Campaign Rank System
diff --git a/MekHQ/resources/mekhq/resources/PersonViewPanel.properties b/MekHQ/resources/mekhq/resources/PersonViewPanel.properties
index deb975a62b7..60cd4b5b6c1 100644
--- a/MekHQ/resources/mekhq/resources/PersonViewPanel.properties
+++ b/MekHQ/resources/mekhq/resources/PersonViewPanel.properties
@@ -21,26 +21,38 @@ lblPilot1.text=<html><nobr><b>Piloting:</b></nobr></html>;
 lblArty1.text=<html><nobr><b>Artillery:</b></nobr></html>;
 lblTactics1.text=<html><nobr><b>Tactics:</b></nobr></html>;
 lblInit1.text=<html><nobr><b>Init Bonus:</b></nobr></html>;
-lblTough1.text=<html><nobr><b>Toughness:</b></nobr></html>;
-lblConnections1.text=<html><nobr><b>Connections:</b></nobr></html>;
-lblWealth1.text=<html><nobr><b>Wealth:</b></nobr></html>;
-lblReputation1.text=<html><nobr><b>Reputation:</b></nobr></html>;
-lblEducationLevel1.text=<html><nobr><b>Highest Education:</b></nobr></html>;
+lblToughness.text=Toughness
+lblToughness.tooltip=Toughness applies a modifier to all consciousness checks made during a scenario.
+lblConnections.text=Connections
+lblConnections.tooltip=If this character is the campaign commander, the unit's Reputation will be modified by their\
+  \ Connections score.
+lblWealth.text=Wealth
+lblWealth.tooltip=Raising this trait to at least rank 7 will improve unit Reputation by 1 if this character is the\
+  \ campaign commander. If this character is the campaign commander, they will reinvest funds back into the unit at the\
+  \ beginning of each month based on their Wealth score.
+lblReputation.text=Reputation
+lblReputation.tooltip=If this character is the campaign commander, the unit's Reputation will be modified by their\
+  \ Reputation score. Also modifies all Negotiation, Protocols, and Streetwise skills.\
+  <br>\
+  <br><b>Base Reputation: %s\
+  <br><b>Adjusted Reputation: %s
+lblHighestEducation.text=Highest Education
 lblEducationDurationDays.text=%s days
 lblEducationTravelTo.text=%s/%s days (traveling to %s)
 lblEducationTravelFrom.text=%s/%s days (traveling from %s)
 lblEducationDurationAge.text=Until %s years old
-lblEducationStage1.text=<html><nobr><b>Education Stage:</b></nobr></html>;
-lblEducationDays1.text=<html><nobr><b>Education Time:</b></nobr></html>;
-lblEducationJourneyDays1.text=<html><nobr><b>Journey Time:</b></nobr></html>;
-lblAbility1.text=<html><nobr><b>Abilities:</b></nobr></html>;
-lblImplants1.text=<html><nobr><b>Implants:</b></nobr></html>;
+lblEducationStage.educationTime=Education Time
+lblEducationStage.journeyTime=Journey Time
 lblAdvancedMedical1.text=<html><nobr><b>Injury Penalties:</b></nobr></html>;
 lblInjuries.text=<html><nobr><b>Injuries:</b></nobr></html>;
-lblEdge1.text=<html><nobr><b>Edge:</b></nobr></html>;
-lblEdgeAvail1.text=<html><nobr><b>Edge Available:</b></nobr></html>;
-lblLoyalty1.text=<html><nobr><b>Loyalty Modifier:</b></nobr></html>;
-lblFatigue1.text=<html><nobr><b>Fatigue:</b></nobr></html>;
+lblEdge.text=Edge
+lblEdge.tooltip=Edge is used to reroll negative events, such as some failed skilled checks. Edge triggers can be adjusted\
+  \ through the right-click personnel menu.
+lblLoyalty.text=Loyalty
+lblLoyalty.tooltip=Loyalty applies a modifier to the target number of all Turnover checks.
+lblFatigue.text=Fatigue
+lblFatigue.tooltip=Fatigue applies a penalty to Turnover checks. It also reduces the number of rounds until fatigue\
+  \ penalties occur while in scenarios (requires Fatigue to be enabled in MegaMek Options)
 lblSpouse1.text=<html><nobr><b>Spouse:</b></nobr></html>;
 lblFormerSpouses1.text=<html><nobr><b>Former Spouse:</b></nobr></html>;
 lblChildren1.text=<html><nobr><b>Children:</b></nobr></html>;
@@ -56,7 +68,12 @@ scenarioLog.title=Hide Scenario Log
 assignmentLogHeader.title=Show Assignments Log
 assignmentLog.title=Hide Assignments Log
 pnlInjuries.title=Injury Report
-pnlSkills.title=Skills and Abilities
+pnlSkills.combat=Combat Skills
+pnlSkills.support=Support Skills
+pnlSkills.roleplay=Roleplay Skills
+pnlSkills.attributes=Attribute Modifiers
+pnlSkills.abilities=Traits and Misc
+pnlSkills.traits=Traits and Misc
 pnlAwards.title=Medals and Awards
 pnlFamily.title=Family Information
 pnlPersonality.title=About
@@ -70,9 +87,8 @@ pnlMedicalLog.title=Hide Medical Log
 pnlKillsHeader.title=Show Kill Record
 pnlKills.title=Hide Kill Record
 lblPermanentInjury.text=permanent injury
-btnMedical.tooltip=View medical details
 format.itemHeader=<html><b>%s:</b></html>
-format.itemHeader.roleplay=<html><i>%s:</i></html>
+format.traitValue=<html><b>%s:</b> %s%s</html>
 format.kills=Kills: %d
 format.killDetail=%s with %s
 format.scenarios=Scenarios: %d
diff --git a/MekHQ/resources/mekhq/resources/Skill.properties b/MekHQ/resources/mekhq/resources/Skill.properties
new file mode 100644
index 00000000000..c947edf7736
--- /dev/null
+++ b/MekHQ/resources/mekhq/resources/Skill.properties
@@ -0,0 +1,4 @@
+tooltip.format.aging=<b>Aging Modifier:</b> {0}<br>
+tooltip.format.spa=<b>SPA & Trait Modifier:</b> {0}<br>
+tooltip.format.linkedAttribute=<b>{0}:</b> {1} (Coming in 50.07)<br>
+tooltip.format.addition=+
diff --git a/MekHQ/resources/mekhq/resources/SkillType.properties b/MekHQ/resources/mekhq/resources/SkillType.properties
new file mode 100644
index 00000000000..40c9ddc6474
--- /dev/null
+++ b/MekHQ/resources/mekhq/resources/SkillType.properties
@@ -0,0 +1,81 @@
+# suppress inspection "UnusedProperty" for the whole file
+PilotingMek.flavorText=Used for piloting skill checks while piloting a 'Mek.
+PilotingAerospace.flavorText=Used for piloting skill checks while piloting an AeroSpace Fighter.
+PilotingAircraft.flavorText=Used for piloting skill checks while piloting a Conventional Fighter.
+PilotingGroundVehicle.flavorText=Used for piloting skill checks while driving a Ground Vehicle.
+PilotingVTOL.flavorText=Used for piloting skill checks while driving a VTOL Vehicle.
+PilotingNaval.flavorText=Used for piloting skill checks while driving a Naval Vehicle.
+PilotingSpacecraft.flavorText=Used for piloting skill checks while piloting a spacecraft, such as a WarShip or DropShip.
+GunneryMek.flavorText=Used for gunnery skill checks while piloting a 'Mek.
+GunneryAerospace.flavorText=Used for gunnery skill checks while piloting an AeroSpace Fighter.
+GunneryAircraft.flavorText=Used for gunnery skill checks while piloting a Conventional Fighter.
+GunneryVehicle.flavorText=Used for gunnery skill checks while piloting any Vehicle.
+GunnerySpacecraft.flavorText=Used for gunnery skill checks while piloting a 'Mek.
+GunneryBattleArmor.flavorText=Used for gunnery skill checks while piloting Battle Armor.
+GunneryProtoMek.flavorText=Used for gunnery skill checks while piloting a ProtoMek.
+Artillery.flavorText=Used for gunnery skill checks with artillery weapons.
+SmallArms.flavorText=Used for non-Battle Armor infantry gunnery skill checks.
+AntiMek.flavorText=Used by infantry for Anti-Mek attacks. Also used in infantry piloting skill checks. Corresponds to\
+  \ the ATOW skill 'climbing.'
+TechMek.flavorText=Used to repair and maintain 'Meks.
+TechMechanic.flavorText=Used to repair and maintain Vehicles.
+TechAero.flavorText=Used to repair and maintain Aerospace fighters.
+TechBattleArmor.flavorText=Used to repair and maintain Battle Armor.
+TechVessel.flavorText=Used to repair and maintain Spacecraft, such as WarShips or DropShips.
+Astech.flavorText=Required for the Astech role and grants eligibility for the Vehicle Crewmember role. Otherwise, no\
+  \ mechanical effects.
+Doctor.flavorText=Used to heal injured personnel.
+MedTech.flavorText=Required for the Medic role and grants eligibility for the Vehicle Crewmember role. Otherwise, no\
+  \ mechanical effects.
+HyperspaceNavigation.flavorText=Required for the Navigator role, otherwise no mechanical effects.
+Administration.flavorText=Used by the different Admin professions (see documentation). Depending on campaign options may\
+  \ also affect the number of patients a doctor can attend per day and how many minutes per day a tech has.
+Negotiation.flavorText=Used by the different Admin professions (see documentation). Used by the campaign commander to\
+  \ improve Resupplies while on Guerrilla Warfare contracts (StratCon only).
+Leadership.flavorText=May reduce Turnover target numbers (see documentation). Allows the leader of combat teams to pull\
+  \ in additional units when deploying to a scenario (StratCon only).
+Scrounge.flavorText=Deprecated and scheduled for removal.
+Strategy.flavorText=Reduces the arrival time of reinforcements.
+Tactics.flavorText=Depending on campaign options may increase Initiative rolls. Increases the number of additional infantry\
+  \ units (or minefields) that can be deployed to a scenario while in the Frontline combat role (StratCon Only). Increases\
+  \ the number of times scenario attributes (weather, map, etc.) can be rerolled (AtB & StratCon Only).
+Acrobatics.flavorText=No mechanical effects.
+Acting.flavorText=No mechanical effects.
+AnimalHandling.flavorText=No mechanical effects.
+Appraisal.flavorText=No mechanical effects.
+Archery.flavorText=Deprecated and scheduled for removal.
+ArtDancing.flavorText=No mechanical effects.
+ArtDrawing.flavorText=No mechanical effects.
+ArtPainting.flavorText=No mechanical effects.
+ArtWriting.flavorText=No mechanical effects.
+Climbing.flavorText=Deprecated and scheduled for removal.
+Communications.flavorText=Grants eligibility for the Vehicle Crewmember role. Otherwise, no mechanical effects.
+Computers.flavorText=No mechanical effects.
+Cryptography.flavorText=No mechanical effects.
+Demolitions.flavorText=No mechanical effects.
+Disguise.flavorText=No mechanical effects.
+EscapeArtist.flavorText=No mechanical effects.
+Forgery.flavorText=No mechanical effects.
+InterestHistory.flavorText=No mechanical effects.
+InterestLiterature.flavorText=No mechanical effects.
+InterestHoloGames.flavorText=No mechanical effects.
+InterestSports.flavorText=No mechanical effects.
+Interrogation.flavorText=No mechanical effects.
+Investigation.flavorText=No mechanical effects.
+Languages.flavorText=No mechanical effects.
+MartialArts.flavorText=No mechanical effects.
+Perception.flavorText=No mechanical effects.
+SleightofHand.flavorText=No mechanical effects.
+Protocols.flavorText=No mechanical effects.
+ScienceBiology.flavorText=No mechanical effects.
+ScienceChemistry.flavorText=No mechanical effects.
+ScienceMathematics.flavorText=No mechanical effects.
+SciencePhysics.flavorText=No mechanical effects.
+SecuritySystemsElectronic.flavorText=No mechanical effects.
+SecuritySystemsMechanical.flavorText=No mechanical effects.
+SensorOperations.flavorText=Grants eligibility for the Vehicle Crewmember role. Otherwise, no mechanical effects.
+Stealth.flavorText=No mechanical effects.
+Streetwise.flavorText=No mechanical effects.
+Survival.flavorText=No mechanical effects.
+Tracking.flavorText=No mechanical effects.
+Training.flavorText=No mechanical effects.
diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java
index 5190e33561a..4377b980ce2 100644
--- a/MekHQ/src/mekhq/campaign/personnel/Person.java
+++ b/MekHQ/src/mekhq/campaign/personnel/Person.java
@@ -52,6 +52,7 @@
 import static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;
 import static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;
 import static mekhq.campaign.personnel.skills.SkillType.S_ADMIN;
+import static mekhq.campaign.personnel.skills.SkillType.getSkillsBySkillSubType;
 import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;
 import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;
 
@@ -115,6 +116,7 @@
 import mekhq.campaign.personnel.skills.SkillType;
 import mekhq.campaign.personnel.skills.Skills;
 import mekhq.campaign.personnel.skills.enums.SkillAttribute;
+import mekhq.campaign.personnel.skills.enums.SkillSubType;
 import mekhq.campaign.randomEvents.personalities.enums.Aggression;
 import mekhq.campaign.randomEvents.personalities.enums.Ambition;
 import mekhq.campaign.randomEvents.personalities.enums.Greed;
@@ -4105,10 +4107,38 @@ public void removeSkill(final String skillName) {
         skills.removeSkill(skillName);
     }
 
+    /**
+     * @return the the number of skills learned by the character.
+     */
     public int getSkillNumber() {
         return skills.size();
     }
 
+    /**
+     * Returns a list of skill names that the current object possesses, filtered by the specified skill subtypes.
+     *
+     * <p>For each skill subtype provided, this method collects all skill names associated
+     * with those subtypes, then adds to the result only those skills that the object is known to have (i.e., those for
+     * which {@code hasSkill(skillName)} returns true).</p>
+     *
+     * @param skillSubTypes the list of {@link SkillSubType} to use for filtering skills
+     *
+     * @return a {@link List} of skill names that are both of the specified subtypes and known to the object
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public List<String> getKnownSkillsBySkillSubType(List<SkillSubType> skillSubTypes) {
+        List<String> knownSkills = new ArrayList<>();
+        for (String skillName : getSkillsBySkillSubType(skillSubTypes)) {
+            if (hasSkill(skillName)) {
+                knownSkills.add(skillName);
+            }
+        }
+
+        return knownSkills;
+    }
+
     /**
      * Remove all skills
      */
@@ -5223,13 +5253,13 @@ public int getReputation() {
      */
     public int getAdjustedReputation(boolean isUseAgingEffects, boolean isClanCampaign, LocalDate today,
           int rankIndex) {
-        return reputation +
-                     (isUseAgingEffects ?
-                            getReputationAgeModifier(getAge(today),
-                                  isClanCampaign,
-                                  !isNullOrBlank(bloodname),
-                                  rankIndex) :
-                            0);
+        int modifier = isUseAgingEffects ?
+                             getReputationAgeModifier(getAge(today),
+                                   isClanCampaign,
+                                   !isNullOrBlank(bloodname),
+                                   rankIndex) :
+                             0;
+        return reputation + modifier;
     }
 
     public void setReputation(final int reputation) {
diff --git a/MekHQ/src/mekhq/campaign/personnel/skills/Skill.java b/MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
index 84c068908c2..274489a175d 100644
--- a/MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
+++ b/MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
@@ -43,6 +43,8 @@
 import static mekhq.campaign.personnel.skills.SkillType.S_PROTOCOLS;
 import static mekhq.campaign.personnel.skills.SkillType.S_STREETWISE;
 import static mekhq.campaign.personnel.skills.enums.SkillAttribute.CHARISMA;
+import static mekhq.utilities.MHQInternationalization.getFormattedTextAt;
+import static mekhq.utilities.MHQInternationalization.getTextAt;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -53,6 +55,7 @@
 import megamek.logging.MMLogger;
 import mekhq.campaign.personnel.Person;
 import mekhq.campaign.personnel.PersonnelOptions;
+import mekhq.campaign.personnel.skills.enums.SkillAttribute;
 import mekhq.campaign.randomEvents.personalities.enums.Reasoning;
 import mekhq.utilities.MHQXMLUtility;
 import org.w3c.dom.Node;
@@ -92,6 +95,7 @@ public class Skill {
     public static int COUNT_UP_MAX_VALUE = 10;
     public static int COUNT_DOWN_MIN_VALUE = 0;
 
+    private static final String RESOURCE_BUNDLE = "mekhq.resources.Skill";
     private static final MMLogger logger = MMLogger.create(Skill.class);
 
     private SkillType type;
@@ -225,6 +229,10 @@ public void setBonus(int b) {
         this.bonus = b;
     }
 
+    public int getAgingModifier() {
+        return agingModifier;
+    }
+
     public void setAgingModifier(int agingModifier) {
         this.agingModifier = agingModifier;
     }
@@ -292,7 +300,7 @@ public int getFinalSkillValue(PersonnelOptions characterOptions, int reputation)
      *
      * @return The calculated skill modifier for the current skill type.
      */
-    private int getSPAModifiers(PersonnelOptions characterOptions, int reputation) {
+    public int getSPAModifiers(PersonnelOptions characterOptions, int reputation) {
         int modifier = 0;
 
         String name = type.getName();
@@ -516,6 +524,65 @@ public String toString(PersonnelOptions options, int reputation) {
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        String additionSymbol = getTextAt(RESOURCE_BUNDLE, "tooltip.format.addition");
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? additionSymbol : "") + firstLinkedAttributeModifier));
+
+        SkillAttribute secondLinkedAttribute = type.getSecondAttribute();
+        if (secondLinkedAttribute != SkillAttribute.NONE) {
+            // TODO enable once attribute modifier sare implemented
+            //            int secondLinkedAttributeModifier = attributes.getAttributeModifier(secondLinkedAttribute);
+            int secondLinkedAttributeModifier = 0;
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.linkedAttribute",
+                  secondLinkedAttribute.getLabel(),
+                  (secondLinkedAttributeModifier > 0 ? additionSymbol : "") + secondLinkedAttributeModifier));
+        }
+
+        return tooltip.toString();
+    }
+
     public void writeToXML(final PrintWriter pw, int indent) {
         MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "skill");
         MHQXMLUtility.writeSimpleXMLTag(pw, indent, "type", type.getName());
diff --git a/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java b/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java
index 66271645597..2a839c6727e 100644
--- a/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java
+++ b/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java
@@ -49,6 +49,7 @@
 import static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_SECURITY;
 import static mekhq.campaign.personnel.skills.enums.SkillSubType.SUPPORT;
 import static mekhq.campaign.personnel.skills.enums.SkillSubType.SUPPORT_COMMAND;
+import static mekhq.utilities.MHQInternationalization.getTextAt;
 import static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;
 
 import java.io.PrintWriter;
@@ -90,6 +91,7 @@
  * @author Jay Lawson (jaylawson39 at yahoo.com)
  */
 public class SkillType {
+    private static final String RESOURCE_BUNDLE = "mekhq.resources.SkillType";
     private static final MMLogger logger = MMLogger.create(SkillType.class);
 
     /**
@@ -383,6 +385,33 @@ public static String[] getSkillList() {
         return skillList;
     }
 
+    /**
+     * Retrieves a list of unique skill names that match any of the specified {@link SkillSubType}s.
+     *
+     * <p>This method iterates through all known {@link SkillType} instances and collects the names of those whose
+     * sub-type is included in the provided list of {@code skillSubTypes}. Each skill name will only appear once in the
+     * resulting list, even if multiple {@code SkillType}s with the same name are found.</p>
+     *
+     * @param skillSubTypes List of {@link SkillSubType}s for which to find matching skill names.
+     *
+     * @return A list of unique skill names that belong to one of the specified skill sub-types.
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public static List<String> getSkillsBySkillSubType(List<SkillSubType> skillSubTypes) {
+        List<String> relevantSkills = new ArrayList<>();
+        for (SkillType skillType : lookupHash.values()) {
+            SkillSubType subType = skillType.getSubType();
+            if (skillSubTypes.contains(subType)) {
+                if (!relevantSkills.contains(skillType.name)) {
+                    relevantSkills.add(skillType.name);
+                }
+            }
+        }
+        return relevantSkills;
+    }
+
     /**
      * Default constructor for the {@code SkillType} class.
      *
@@ -491,6 +520,44 @@ public String getName() {
         return name;
     }
 
+    /**
+     * Generates a resource bundle key derived from the {@code name} field by removing all occurrences of '/', '-', and
+     * whitespace characters.
+     *
+     * @return a normalized resource bundle key string with specific characters removed from {@code name}
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private String getResourceBundleKey() {
+        String key = name;
+        key = key.replace(RP_ONLY_TAG, "");
+        key = key.replace("/", "");
+        key = key.replace("-", "");
+        key = key.replace(" ", "");
+        return key;
+    }
+
+    public String getFlavorText(boolean includeHtmlTags, boolean includeAttributes) {
+        String key = getResourceBundleKey();
+        String rawFlavorText = getTextAt(RESOURCE_BUNDLE, key + ".flavorText");
+
+        String htmlOpenTag = includeHtmlTags ? "<html>" : "";
+        String htmlCloseTag = includeHtmlTags ? "</html>" : "";
+
+        if (!includeAttributes) {
+            return htmlOpenTag + rawFlavorText + htmlCloseTag;
+        }
+
+        String flavorText = htmlOpenTag + rawFlavorText + "<br>(" + firstAttribute.getLabel();
+
+        if (secondAttribute != NONE) {
+            flavorText += ", " + secondAttribute.getLabel() + ')' + htmlCloseTag;
+        }
+
+        return flavorText;
+    }
+
     public int getTarget() {
         return target;
     }
diff --git a/MekHQ/src/mekhq/gui/PersonnelTab.java b/MekHQ/src/mekhq/gui/PersonnelTab.java
index 02424d10feb..1e4bca17138 100644
--- a/MekHQ/src/mekhq/gui/PersonnelTab.java
+++ b/MekHQ/src/mekhq/gui/PersonnelTab.java
@@ -32,6 +32,8 @@
  */
 package mekhq.gui;
 
+import static java.lang.Math.round;
+
 import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.GridBagConstraints;
@@ -76,7 +78,7 @@
 public final class PersonnelTab extends CampaignGuiTab {
     private static final MMLogger logger = MMLogger.create(PersonnelTab.class);
 
-    public static final int PERSONNEL_VIEW_WIDTH = 490;
+    public static final int PERSONNEL_VIEW_WIDTH = UIUtil.scaleForGUI(600);
 
     private JSplitPane splitPersonnel;
     private JTable personnelTable;
@@ -223,7 +225,7 @@ public Component getListCellRendererComponent(final JList<?> list, final Object
         personnelTable.getSelectionModel().addListSelectionListener(ev -> refreshPersonnelView());
 
         scrollPersonnelView = new JScrollPaneWithSpeed();
-        scrollPersonnelView.setMinimumSize(new Dimension(PERSONNEL_VIEW_WIDTH, 600));
+        scrollPersonnelView.setMinimumSize(new Dimension((int) round(PERSONNEL_VIEW_WIDTH * 0.9), 600));
         scrollPersonnelView.setPreferredSize(new Dimension(PERSONNEL_VIEW_WIDTH, 600));
         scrollPersonnelView.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
         scrollPersonnelView.setViewportView(null);
diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java
index 8858d967d37..b20e4361649 100644
--- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java
+++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java
@@ -56,6 +56,7 @@
 import static mekhq.campaign.personnel.skills.Attributes.ATTRIBUTE_IMPROVEMENT_COST;
 import static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;
 import static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;
+import static mekhq.campaign.personnel.skills.SkillType.S_ARTILLERY;
 import static mekhq.campaign.personnel.skills.SkillType.S_SURGERY;
 import static mekhq.campaign.personnel.skills.SkillType.getType;
 import static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER;
@@ -64,6 +65,7 @@
 import static mekhq.utilities.spaUtilities.SpaUtilities.getSpaCategory;
 
 import java.awt.Color;
+import java.awt.Dialog;
 import java.awt.event.ActionEvent;
 import java.awt.event.MouseEvent;
 import java.time.LocalDate;
@@ -162,6 +164,7 @@ public class PersonnelTableMouseAdapter extends JPopupMenuAdapter {
 
     // region Variable Declarations
     private static final String CMD_SKILL_CHECK = "SKILL_CHECK";
+    private static final String CMD_MEDICAL_RECORDS = "MEDICAL_RECORDS";
     private static final String CMD_RANKSYSTEM = "RANKSYSTEM";
     private static final String CMD_RANK = "RANK";
     private static final String CMD_MANEI_DOMINI_RANK = "MD_RANK";
@@ -358,6 +361,12 @@ public void actionPerformed(ActionEvent action) {
                 }
                 break;
             }
+            case CMD_MEDICAL_RECORDS: {
+                MedicalViewDialog medDialog = new MedicalViewDialog(null, getCampaign(), selectedPerson);
+                medDialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
+                medDialog.setVisible(true);
+                break;
+            }
             case CMD_RANKSYSTEM: {
                 final RankSystem rankSystem = Ranks.getRankSystemFromCode(data[1]);
                 final RankValidator rankValidator = new RankValidator();
@@ -1827,6 +1836,13 @@ protected Optional<JPopupMenu> createPopupMenu() {
         menuItem.addActionListener(this);
         popup.add(menuItem);
 
+        if (getCampaignOptions().isUseAdvancedMedical() && oneSelected) {
+            menuItem = new JMenuItem(resources.getString("viewMedicalRecords.text"));
+            menuItem.setActionCommand(makeCommand(CMD_MEDICAL_RECORDS));
+            menuItem.addActionListener(this);
+            popup.add(menuItem);
+        }
+
         if (StaticChecks.areAllEligible(true, selected)) {
             menu = new JMenu(resources.getString("changeRank.text"));
             final Profession initialProfession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole());
@@ -2989,6 +3005,11 @@ protected Optional<JPopupMenu> createPopupMenu() {
             JMenu roleplaySkillsSecurityNew = new JMenu(resources.getString("roleplaySkills.security"));
             roleplaySkillsNew.add(roleplaySkillsSecurityNew);
 
+            int adjustedReputation = person.getAdjustedReputation(getCampaignOptions().isUseAgeEffects(),
+                  getCampaign().isClanCampaign(),
+                  getCampaign().getLocalDate(),
+                  person.getRankLevel());
+
             for (int i = 0; i < SkillType.getSkillList().length; i++) {
                 String typeName = SkillType.getSkillList()[i];
 
@@ -2996,8 +3017,14 @@ protected Optional<JPopupMenu> createPopupMenu() {
                 cost = (int) round(cost * xpCostMultiplier);
 
                 if (cost >= 0) {
-                    String desc = String.format(resources.getString("skillDesc.format"), typeName, cost);
-                    menuItem = new JMenuItem(desc);
+                    if (Objects.equals(typeName, S_ARTILLERY)) {
+                        if (!getCampaignOptions().isUseArtillery()) {
+                            continue;
+                        }
+                    }
+
+                    String description = String.format(resources.getString("skillDesc.format"), typeName, cost);
+                    menuItem = new JMenuItem(description);
                     menuItem.setActionCommand(makeCommand(CMD_IMPROVE, typeName, String.valueOf(cost)));
                     menuItem.addActionListener(this);
                     menuItem.setEnabled(person.getXP() >= cost);
@@ -3010,6 +3037,11 @@ protected Optional<JPopupMenu> createPopupMenu() {
                                 continue;
                             }
 
+                            String tooltip = wordWrap(skill.getTooltip(person.getOptions(),
+                                  person.getATOWAttributes(),
+                                  adjustedReputation));
+                            menuItem.setToolTipText(tooltip);
+
                             SkillSubType subType = skillType.getSubType();
                             switch (subType) {
                                 case NONE -> currentMenu.add(menuItem);
@@ -3032,6 +3064,9 @@ protected Optional<JPopupMenu> createPopupMenu() {
                             continue;
                         }
 
+                        String tooltip = wordWrap(skillType.getFlavorText(false, true));
+                        menuItem.setToolTipText(tooltip);
+
                         SkillSubType subType = skillType.getSubType();
                         switch (subType) {
                             case NONE -> newSkillsMenu.add(menuItem);
diff --git a/MekHQ/src/mekhq/gui/view/PersonViewPanel.java b/MekHQ/src/mekhq/gui/view/PersonViewPanel.java
index 3527a1d439f..288f1700299 100644
--- a/MekHQ/src/mekhq/gui/view/PersonViewPanel.java
+++ b/MekHQ/src/mekhq/gui/view/PersonViewPanel.java
@@ -24,24 +24,43 @@
  *
  * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
  * InMediaRes Productions, LLC.
+ *
+ * MechWarrior Copyright Microsoft Corporation. MekHQ was created under
+ * Microsoft's "Game Content Usage Rules"
+ * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
+ * affiliated with Microsoft.
  */
 package mekhq.gui.view;
 
 import static java.awt.Color.BLACK;
 import static java.awt.Color.RED;
+import static java.lang.Math.ceil;
 import static megamek.client.ui.WrapLayout.wordWrap;
 import static megamek.common.EntityWeightClass.WEIGHT_ULTRA_LIGHT;
+import static megamek.common.options.PilotOptions.LVL3_ADVANTAGES;
+import static megamek.common.options.PilotOptions.MD_ADVANTAGES;
 import static megamek.utilities.ImageUtilities.addTintToImageIcon;
 import static mekhq.campaign.personnel.Person.getLoyaltyName;
+import static mekhq.campaign.personnel.SpecialAbility.getOption;
 import static mekhq.campaign.personnel.skills.SkillType.RP_ONLY_TAG;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.COMBAT_GUNNERY;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.COMBAT_PILOTING;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_ART;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_GENERAL;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_INTEREST;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_SCIENCE;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.ROLEPLAY_SECURITY;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.SUPPORT;
+import static mekhq.campaign.personnel.skills.enums.SkillSubType.SUPPORT_COMMAND;
 import static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue;
+import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;
+import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;
 import static org.jfree.chart.ChartColor.DARK_BLUE;
 import static org.jfree.chart.ChartColor.DARK_RED;
 
 import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Cursor;
-import java.awt.Dialog.ModalityType;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
 import java.awt.GridBagConstraints;
@@ -54,12 +73,21 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.ResourceBundle;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.accessibility.AccessibleRelation;
-import javax.swing.*;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.JTextPane;
 import javax.swing.table.TableColumn;
 
 import megamek.codeUtilities.MathUtility;
@@ -69,27 +97,30 @@
 import megamek.utilities.ImageUtilities;
 import mekhq.MHQStaticDirectoryManager;
 import mekhq.MekHQ;
-import mekhq.Utilities;
 import mekhq.campaign.Campaign;
+import mekhq.campaign.CampaignOptions;
 import mekhq.campaign.Kill;
-import mekhq.campaign.event.PersonChangedEvent;
 import mekhq.campaign.finances.Money;
 import mekhq.campaign.log.LogEntry;
 import mekhq.campaign.personnel.Award;
 import mekhq.campaign.personnel.Injury;
 import mekhq.campaign.personnel.Person;
+import mekhq.campaign.personnel.PersonAwardController;
 import mekhq.campaign.personnel.PersonnelOptions;
+import mekhq.campaign.personnel.SpecialAbility;
 import mekhq.campaign.personnel.education.Academy;
 import mekhq.campaign.personnel.education.EducationController;
 import mekhq.campaign.personnel.enums.GenderDescriptors;
 import mekhq.campaign.personnel.enums.PersonnelStatus;
+import mekhq.campaign.personnel.enums.education.EducationLevel;
 import mekhq.campaign.personnel.enums.education.EducationStage;
 import mekhq.campaign.personnel.familyTree.FormerSpouse;
-import mekhq.campaign.personnel.skills.SkillType;
+import mekhq.campaign.personnel.skills.Attributes;
+import mekhq.campaign.personnel.skills.Skill;
+import mekhq.campaign.personnel.skills.enums.SkillAttribute;
 import mekhq.campaign.universe.PlanetarySystem;
 import mekhq.gui.CampaignGUI;
 import mekhq.gui.baseComponents.JScrollablePanel;
-import mekhq.gui.dialog.MedicalViewDialog;
 import mekhq.gui.enums.MHQTabType;
 import mekhq.gui.model.PersonnelEventLogModel;
 import mekhq.gui.model.PersonnelKillLogModel;
@@ -104,20 +135,22 @@
 public class PersonViewPanel extends JScrollablePanel {
     private static final MMLogger logger = MMLogger.create(PersonViewPanel.class);
 
-    private static final int MAX_NUMBER_OF_RIBBON_AWARDS_PER_ROW = 3;
+    private static final int MAX_NUMBER_OF_RIBBON_AWARDS_PER_ROW = 5;
 
     private final CampaignGUI gui;
 
     private final Person person;
     private final Campaign campaign;
+    private final CampaignOptions campaignOptions;
 
     private final transient ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.PersonViewPanel",
           MekHQ.getMHQOptions().getLocale());
 
-    public PersonViewPanel(Person p, Campaign c, CampaignGUI gui) {
+    public PersonViewPanel(Person person, Campaign campaign, CampaignGUI gui) {
         super();
-        this.person = p;
-        this.campaign = c;
+        this.person = person;
+        this.campaign = campaign;
+        campaignOptions = campaign.getCampaignOptions();
         this.gui = gui;
         initComponents();
     }
@@ -132,7 +165,7 @@ private void initComponents() {
         gbc_pnlPortrait.gridy = 0;
         gbc_pnlPortrait.fill = GridBagConstraints.NONE;
         gbc_pnlPortrait.anchor = GridBagConstraints.NORTHWEST;
-        gbc_pnlPortrait.insets = new Insets(10, 10, 0, 0);
+        gbc_pnlPortrait.insets = new Insets(10, 0, 0, 0);
         add(pnlPortrait, gbc_pnlPortrait);
 
         JPanel pnlInfo = fillInfo();
@@ -140,98 +173,123 @@ private void initComponents() {
         gridBagConstraints.gridx = 1;
         gridBagConstraints.gridy = 0;
         gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+        gridBagConstraints.insets = new Insets(0, 0, 10, 0);
         gridBagConstraints.fill = GridBagConstraints.BOTH;
         gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
         add(pnlInfo, gridBagConstraints);
 
-        int gridy = 1;
+        int gridY = 1;
 
-        JPanel pnlSkills = fillSkills();
-        gridBagConstraints = new GridBagConstraints();
-        gridBagConstraints.gridx = 0;
-        gridBagConstraints.gridy = gridy;
-        gridBagConstraints.gridwidth = 2;
-        gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-        gridBagConstraints.fill = GridBagConstraints.BOTH;
-        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-        add(pnlSkills, gridBagConstraints);
-        gridy++;
+        PersonAwardController awardController = person.getAwardController();
+        if (awardController.hasAwards()) {
+            gridY = applyAndDisplayAwards(awardController, pnlPortrait, gridY);
+        }
 
-        if (campaign.getCampaignOptions().isUseAdvancedMedical()) {
-            JPanel pnlInjuries = fillInjuries();
+        List<String> relevantSkills = person.getKnownSkillsBySkillSubType(List.of(COMBAT_GUNNERY, COMBAT_PILOTING));
+        if (!relevantSkills.isEmpty()) {
+            JPanel pnlCombatSkills = fillSkills(relevantSkills, "pnlSkills.combat");
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.weightx = 1.0;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlInjuries, gridBagConstraints);
-            gridy++;
+            add(pnlCombatSkills, gridBagConstraints);
+            gridY++;
         }
 
-        if (!person.getGenealogy().isEmpty()) {
-            JPanel pnlFamily = fillFamily();
+        relevantSkills = person.getKnownSkillsBySkillSubType(List.of(SUPPORT, SUPPORT_COMMAND));
+        if (!relevantSkills.isEmpty()) {
+            JPanel pnlSupportSkills = fillSkills(relevantSkills, "pnlSkills.support");
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
             gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlFamily, gridBagConstraints);
-            gridy++;
+            add(pnlSupportSkills, gridBagConstraints);
+            gridY++;
         }
 
-        if (person.getAwardController().hasAwards()) {
-            if (person.getAwardController().hasAwardsWithRibbons()) {
-                Box boxRibbons = drawRibbons();
-
-                GridBagConstraints gbc_pnlAllRibbons = new GridBagConstraints();
-                gbc_pnlAllRibbons.gridx = 0;
-                gbc_pnlAllRibbons.gridy = 1;
-                gbc_pnlAllRibbons.fill = GridBagConstraints.NONE;
-                gbc_pnlAllRibbons.anchor = GridBagConstraints.NORTHWEST;
-                gbc_pnlAllRibbons.insets = new Insets(0, 0, 0, 0);
-                pnlPortrait.add(boxRibbons, gbc_pnlAllRibbons);
-            }
+        relevantSkills = person.getKnownSkillsBySkillSubType(List.of(ROLEPLAY_GENERAL,
+              ROLEPLAY_ART,
+              ROLEPLAY_INTEREST,
+              ROLEPLAY_SCIENCE,
+              ROLEPLAY_SECURITY));
+        if (!relevantSkills.isEmpty()) {
+            JPanel pnlRoleplaySkills = fillSkills(relevantSkills, "pnlSkills.roleplay");
+            gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = gridY;
+            gridBagConstraints.gridwidth = 2;
+            gridBagConstraints.weightx = 1.0;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+            gridBagConstraints.fill = GridBagConstraints.BOTH;
+            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+            add(pnlRoleplaySkills, gridBagConstraints);
+            gridY++;
+        }
 
-            JPanel pnlAllAwards = new JPanel();
-            pnlAllAwards.setLayout(new BoxLayout(pnlAllAwards, BoxLayout.PAGE_AXIS));
-            pnlAllAwards.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlAwards.title")));
+        Map<SkillAttribute, Integer> relevantAttributes = getRelevantAttributes();
+        if (!relevantAttributes.isEmpty()) {
+            JPanel pnlAttributes = fillAttributeModifiers(relevantAttributes);
+            gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = gridY;
+            gridBagConstraints.gridwidth = 2;
+            gridBagConstraints.weightx = 1.0;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+            gridBagConstraints.fill = GridBagConstraints.BOTH;
+            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+            add(pnlAttributes, gridBagConstraints);
+            gridY++;
+        }
 
-            if (person.getAwardController().hasAwardsWithMedals()) {
-                JPanel pnlMedals = drawMedals();
-                pnlMedals.setName("pnlMedals");
-                pnlMedals.setLayout(new WrapLayout(FlowLayout.LEFT));
-                pnlAllAwards.add(pnlMedals);
-            }
+        List<SpecialAbility> relevantAbilities = getRelevantAbilities();
+        if (!relevantAbilities.isEmpty()) {
+            JPanel pnlAbilities = fillAbilitiesAndImplants(relevantAbilities);
+            gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = gridY;
+            gridBagConstraints.gridwidth = 2;
+            gridBagConstraints.weightx = 1.0;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+            gridBagConstraints.fill = GridBagConstraints.BOTH;
+            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+            add(pnlAbilities, gridBagConstraints);
+            gridY++;
+        }
 
-            if (person.getAwardController().hasAwardsWithMiscs()) {
-                JPanel pnlMiscAwards = drawMiscAwards();
-                pnlMiscAwards.setName("pnlMiscAwards");
-                pnlMiscAwards.setLayout(new WrapLayout(FlowLayout.LEFT));
-                pnlAllAwards.add(pnlMiscAwards);
-            }
+        JPanel pnlOther = fillOther();
+        gridBagConstraints = new GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = gridY;
+        gridBagConstraints.gridwidth = 2;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+        gridBagConstraints.fill = GridBagConstraints.BOTH;
+        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+        add(pnlOther, gridBagConstraints);
+        gridY++;
 
-            if (person.getAwardController().hasAwardsWithMedals() || person.getAwardController().hasAwardsWithMiscs()) {
-                gridBagConstraints = new GridBagConstraints();
-                gridBagConstraints.fill = GridBagConstraints.BOTH;
-                gridBagConstraints.gridwidth = 2;
-                gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-                gridBagConstraints.gridx = 0;
-                gridBagConstraints.gridy = gridy;
-                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                add(pnlAllAwards, gridBagConstraints);
-                gridy++;
-            }
+        if (campaignOptions.isUseAdvancedMedical() && person.needsFixing()) {
+            JPanel pnlInjuries = fillInjuries();
+            gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = gridY;
+            gridBagConstraints.gridwidth = 2;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+            gridBagConstraints.fill = GridBagConstraints.BOTH;
+            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+            add(pnlInjuries, gridBagConstraints);
+            gridY++;
         }
 
-        if ((!person.getPersonalityDescription().isBlank()) &&
-                  (campaign.getCampaignOptions().isUseRandomPersonalities()) &&
+        if ((!person.getPersonalityDescription().isBlank()) && (campaignOptions.isUseRandomPersonalities()) &&
                   (!person.isChild(campaign.getLocalDate()))) { // we don't display for children, as most of the
             // descriptions won't fit
             JTextPane txtDesc = new JTextPane();
@@ -242,13 +300,13 @@ private void initComponents() {
             txtDesc.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlPersonality.title")));
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(txtDesc, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!person.getBiography().isBlank()) {
@@ -261,39 +319,39 @@ private void initComponents() {
                   "pnlDescription.title")), BorderFactory.createEmptyBorder(0, 2, 2, 2)));
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(txtDesc, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!person.getPersonalLog().isEmpty()) {
             JPanel pnlPersonalLogHeader = new JPanel();
             pnlPersonalLogHeader.setName("pnlLogHeader");
             pnlPersonalLogHeader.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlLogHeader.title")));
-            pnlPersonalLogHeader.setVisible(!campaign.getCampaignOptions().isDisplayPersonnelLog());
+            pnlPersonalLogHeader.setVisible(!campaignOptions.isDisplayPersonnelLog());
 
             JPanel pnlPersonalLog = fillPersonalLog();
             pnlPersonalLog.setName("pnlLog");
             pnlPersonalLog.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlLog.title")));
-            pnlPersonalLog.setVisible(campaign.getCampaignOptions().isDisplayPersonnelLog());
+            pnlPersonalLog.setVisible(campaignOptions.isDisplayPersonnelLog());
 
             pnlPersonalLogHeader.addMouseListener(getSwitchListener(pnlPersonalLogHeader, pnlPersonalLog));
             pnlPersonalLog.addMouseListener(getSwitchListener(pnlPersonalLog, pnlPersonalLogHeader));
 
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(pnlPersonalLogHeader, gridBagConstraints);
             add(pnlPersonalLog, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!person.getPerformanceLog().isEmpty()) {
@@ -301,26 +359,26 @@ private void initComponents() {
             pnlPerformanceLogHeader.setName("pnlPerformanceLogHeader");
             pnlPerformanceLogHeader.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(
                   "pnlPerformanceLogHeader.title")));
-            pnlPerformanceLogHeader.setVisible(!campaign.getCampaignOptions().isDisplayPerformanceRecord());
+            pnlPerformanceLogHeader.setVisible(!campaignOptions.isDisplayPerformanceRecord());
 
             JPanel pnlPerformanceLog = fillPerformanceLog();
             pnlPerformanceLog.setName("pnlPerformanceLog");
             pnlPerformanceLog.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlPerformanceLog.title")));
-            pnlPerformanceLog.setVisible(campaign.getCampaignOptions().isDisplayPerformanceRecord());
+            pnlPerformanceLog.setVisible(campaignOptions.isDisplayPerformanceRecord());
 
             pnlPerformanceLogHeader.addMouseListener(getSwitchListener(pnlPerformanceLogHeader, pnlPerformanceLog));
             pnlPerformanceLog.addMouseListener(getSwitchListener(pnlPerformanceLog, pnlPerformanceLogHeader));
 
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(pnlPerformanceLogHeader, gridBagConstraints);
             add(pnlPerformanceLog, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!person.getMedicalLog().isEmpty()) {
@@ -328,26 +386,26 @@ private void initComponents() {
             pnlMedicalLogHeader.setName("pnlMedicalLogHeader");
             pnlMedicalLogHeader.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(
                   "pnlMedicalLogHeader.title")));
-            pnlMedicalLogHeader.setVisible(!campaign.getCampaignOptions().isDisplayMedicalRecord());
+            pnlMedicalLogHeader.setVisible(!campaignOptions.isDisplayMedicalRecord());
 
             JPanel pnlMedicalLog = fillMedicalLog();
             pnlMedicalLog.setName("pnlMedicalLog");
             pnlMedicalLog.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlMedicalLog.title")));
-            pnlMedicalLog.setVisible(campaign.getCampaignOptions().isDisplayMedicalRecord());
+            pnlMedicalLog.setVisible(campaignOptions.isDisplayMedicalRecord());
 
             pnlMedicalLogHeader.addMouseListener(getSwitchListener(pnlMedicalLogHeader, pnlMedicalLog));
             pnlMedicalLog.addMouseListener(getSwitchListener(pnlMedicalLog, pnlMedicalLogHeader));
 
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(pnlMedicalLogHeader, gridBagConstraints);
             add(pnlMedicalLog, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!person.getAssignmentLog().isEmpty()) {
@@ -355,56 +413,56 @@ private void initComponents() {
             pnlAssignmentsLogHeader.setName("assignmentLogHeader");
             pnlAssignmentsLogHeader.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(
                   "assignmentLogHeader.title")));
-            pnlAssignmentsLogHeader.setVisible(!campaign.getCampaignOptions().isDisplayAssignmentRecord());
+            pnlAssignmentsLogHeader.setVisible(!campaignOptions.isDisplayAssignmentRecord());
 
             JPanel pnlAssignmentsLog = fillAssignmentLog();
 
             pnlAssignmentsLog.setName("assignmentLog");
             pnlAssignmentsLog.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(
-                  "assignmentLog.title")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));
-            pnlAssignmentsLog.setVisible(campaign.getCampaignOptions().isDisplayAssignmentRecord());
+                  "assignmentLog.title")), BorderFactory.createEmptyBorder(0, 0, 10, 0)));
+            pnlAssignmentsLog.setVisible(campaignOptions.isDisplayAssignmentRecord());
 
             pnlAssignmentsLogHeader.addMouseListener(getSwitchListener(pnlAssignmentsLogHeader, pnlAssignmentsLog));
             pnlAssignmentsLog.addMouseListener(getSwitchListener(pnlAssignmentsLog, pnlAssignmentsLogHeader));
 
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(pnlAssignmentsLogHeader, gridBagConstraints);
             add(pnlAssignmentsLog, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!campaign.getKillsFor(person.getId()).isEmpty()) {
             JPanel pnlKillsHeader = new JPanel();
             pnlKillsHeader.setName("killsHeader");
             pnlKillsHeader.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlKillsHeader.title")));
-            pnlKillsHeader.setVisible(!campaign.getCampaignOptions().isDisplayKillRecord());
+            pnlKillsHeader.setVisible(!campaignOptions.isDisplayKillRecord());
 
             JPanel pnlKills = fillKillRecord();
 
             pnlKills.setName("txtKills");
             pnlKills.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(
-                  "pnlKills.title")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));
-            pnlKills.setVisible(campaign.getCampaignOptions().isDisplayKillRecord());
+                  "pnlKills.title")), BorderFactory.createEmptyBorder(0, 0, 10, 0)));
+            pnlKills.setVisible(campaignOptions.isDisplayKillRecord());
 
             pnlKillsHeader.addMouseListener(getSwitchListener(pnlKillsHeader, pnlKills));
             pnlKills.addMouseListener(getSwitchListener(pnlKills, pnlKillsHeader));
 
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(pnlKillsHeader, gridBagConstraints);
             add(pnlKills, gridBagConstraints);
-            gridy++;
+            gridY++;
         }
 
         if (!person.getScenarioLog().isEmpty()) {
@@ -412,34 +470,48 @@ private void initComponents() {
             pnlScenariosLogHeader.setName("scenarioLogHeader");
             pnlScenariosLogHeader.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(
                   "scenarioLogHeader.title")));
-            pnlScenariosLogHeader.setVisible(!campaign.getCampaignOptions().isDisplayScenarioLog());
+            pnlScenariosLogHeader.setVisible(!campaignOptions.isDisplayScenarioLog());
 
             JPanel pnlScenariosLog = fillScenarioLog();
 
             pnlScenariosLog.setName("scenarioLog");
             pnlScenariosLog.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(resourceMap.getString(
-                  "scenarioLog.title")), BorderFactory.createEmptyBorder(5, 5, 5, 5)));
-            pnlScenariosLog.setVisible(campaign.getCampaignOptions().isDisplayScenarioLog());
+                  "scenarioLog.title")), BorderFactory.createEmptyBorder(0, 0, 10, 0)));
+            pnlScenariosLog.setVisible(campaignOptions.isDisplayScenarioLog());
 
             pnlScenariosLogHeader.addMouseListener(getSwitchListener(pnlScenariosLogHeader, pnlScenariosLog));
             pnlScenariosLog.addMouseListener(getSwitchListener(pnlScenariosLog, pnlScenariosLogHeader));
 
             gridBagConstraints = new GridBagConstraints();
             gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
+            gridBagConstraints.gridy = gridY;
             gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
             gridBagConstraints.fill = GridBagConstraints.BOTH;
             gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
             add(pnlScenariosLogHeader, gridBagConstraints);
             add(pnlScenariosLog, gridBagConstraints);
-            gridy++;
+            gridY++;
+        }
+
+        if (!person.getGenealogy().isEmpty()) {
+            JPanel pnlFamily = fillFamily();
+            gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = gridY;
+            gridBagConstraints.gridwidth = 2;
+            gridBagConstraints.weightx = 1.0;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+            gridBagConstraints.fill = GridBagConstraints.BOTH;
+            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+            add(pnlFamily, gridBagConstraints);
+            gridY++;
         }
 
         // use glue to fill up the remaining space so everything is aligned to the top
         gridBagConstraints = new GridBagConstraints();
         gridBagConstraints.gridx = 0;
-        gridBagConstraints.gridy = gridy;
+        gridBagConstraints.gridy = gridY;
         gridBagConstraints.gridwidth = 2;
         gridBagConstraints.weighty = 1.0;
         gridBagConstraints.fill = GridBagConstraints.BOTH;
@@ -447,6 +519,142 @@ private void initComponents() {
         add(Box.createGlue(), gridBagConstraints);
     }
 
+    /**
+     * Initializes and lays out the award sections within a portrait panel for a person.
+     *
+     * <p>If ribbon awards are present, they are added above the awards panel. If medal or miscellaneous awards exist,
+     * they are displayed in their respective panels using a {@link WrapLayout}, all contained within a titled section.
+     * This method adds the constructed awards panel to the layout using {@link GridBagConstraints} and updates the grid
+     * Y position accordingly.</p>
+     *
+     * @param awardController the {@link PersonAwardController} providing award data and state
+     * @param pnlPortrait     the portrait {@link JPanel} to which award ribbons may be added
+     * @param gridY           the starting Y grid position for layout
+     *
+     * @return the next available grid Y position after inserting any new panels
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private int applyAndDisplayAwards(PersonAwardController awardController, JPanel pnlPortrait, int gridY) {
+        GridBagConstraints gridBagConstraints;
+        if (awardController.hasAwardsWithRibbons()) {
+            Box boxRibbons = drawRibbons();
+
+            GridBagConstraints gbc_pnlAllRibbons = new GridBagConstraints();
+            gbc_pnlAllRibbons.gridx = 0;
+            gbc_pnlAllRibbons.gridy = 1;
+            gbc_pnlAllRibbons.fill = GridBagConstraints.NONE;
+            gbc_pnlAllRibbons.anchor = GridBagConstraints.NORTHWEST;
+            gbc_pnlAllRibbons.insets = new Insets(-10, 10, 0, 5);
+            pnlPortrait.add(boxRibbons, gbc_pnlAllRibbons);
+        }
+
+        JPanel pnlAllAwards = new JPanel();
+        pnlAllAwards.setLayout(new BoxLayout(pnlAllAwards, BoxLayout.PAGE_AXIS));
+        pnlAllAwards.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlAwards.title")));
+
+        if (awardController.hasAwardsWithMedals()) {
+            JPanel pnlMedals = drawMedals();
+            pnlMedals.setName("pnlMedals");
+            pnlMedals.setLayout(new WrapLayout(FlowLayout.LEFT));
+            pnlAllAwards.add(pnlMedals);
+        }
+
+        if (awardController.hasAwardsWithMiscs()) {
+            JPanel pnlMiscAwards = drawMiscAwards();
+            pnlMiscAwards.setName("pnlMiscAwards");
+            pnlMiscAwards.setLayout(new WrapLayout(FlowLayout.LEFT));
+            pnlAllAwards.add(pnlMiscAwards);
+        }
+
+        if (awardController.hasAwardsWithMedals() || awardController.hasAwardsWithMiscs()) {
+            gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.fill = GridBagConstraints.BOTH;
+            gridBagConstraints.gridwidth = 2;
+            gridBagConstraints.insets = new Insets(0, 0, 10, 0);
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = gridY;
+            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+            add(pnlAllAwards, gridBagConstraints);
+            gridY++;
+        }
+        return gridY;
+    }
+
+    /**
+     * Retrieves a list of relevant special abilities for the person based on the current campaign options.
+     *
+     * <p>This method checks if abilities and/or implants are enabled in the campaign options and,
+     * for each enabled category, iterates over the person's corresponding options. For each option that is selected
+     * (boolean value is true), it retrieves the corresponding {@link SpecialAbility} and adds it to the result list if
+     * it is not already present.</p>
+     *
+     * @return a {@link List} of unique relevant {@link SpecialAbility} instances available to the person, depending on
+     *       campaign settings and the person's selected advantages or implants
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private List<SpecialAbility> getRelevantAbilities() {
+        List<SpecialAbility> relevantAbilities = new ArrayList<>();
+        if (campaignOptions.isUseAbilities() && (person.countOptions(LVL3_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(LVL3_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+
+        if (campaignOptions.isUseImplants() && (person.countOptions(MD_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(MD_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+        return relevantAbilities;
+    }
+
+    /**
+     * Returns a map of relevant skill attributes and their corresponding modifiers for the person.
+     *
+     * <p>This method iterates over all possible {@link SkillAttribute} values (excluding {@link SkillAttribute#NONE}),
+     * retrieves each attribute's score for the person, and computes the associated modifier using
+     * {@link Skill#getIndividualAttributeModifier(int)}. Only attributes with a non-zero modifier are included in the
+     * result map.</p>
+     *
+     * @return a {@link Map} mapping each relevant {@link SkillAttribute} to its computed modifier for the person
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private Map<SkillAttribute, Integer> getRelevantAttributes() {
+        Map<SkillAttribute, Integer> relevantAttributes = new HashMap<>();
+        for (SkillAttribute attribute : SkillAttribute.values()) {
+            if (attribute == SkillAttribute.NONE) {
+                continue;
+            }
+
+            int attributeScore = person.getAttributeScore(attribute);
+            // TODO enable once attribute modifier sare implemented
+            //            int modifier = getIndividualAttributeModifier(attributeScore);
+            int modifier = 0;
+            if (modifier != 0) {
+                relevantAttributes.put(attribute, modifier);
+            }
+        }
+        return relevantAttributes;
+    }
+
     private MouseListener getSwitchListener(JPanel current, JPanel switchTo) {
         return new MouseAdapter() {
             @Override
@@ -504,7 +712,7 @@ private Box drawRibbons() {
                 ribbon = ribbonAsImageIcon.getImage();
 
                 ribbonLabel.setIcon(new ImageIcon(ribbon));
-                ribbonLabel.setToolTipText(award.getTooltip(campaign.getCampaignOptions(), person));
+                ribbonLabel.setToolTipText(award.getTooltip(campaignOptions, person));
                 rowRibbonsBox.add(ribbonLabel, 0);
             } catch (Exception e) {
                 logger.error("", e);
@@ -519,7 +727,6 @@ private Box drawRibbons() {
             rowRibbonsBoxes.add(rowRibbonsBox);
         }
 
-        Collections.reverse(rowRibbonsBoxes);
         for (Box box : rowRibbonsBoxes) {
             boxRibbons.add(box);
         }
@@ -538,7 +745,7 @@ private Box drawRibbons() {
      */
     private int getAwardTierCount(Award award, int maximumTiers) {
         int numAwards = person.getAwardController().getNumberOfAwards(award);
-        int tierSize = campaign.getCampaignOptions().getAwardTierSize();
+        int tierSize = campaignOptions.getAwardTierSize();
 
         int divisionResult = numAwards / tierSize;
         int addition = (tierSize == 1) ? 0 : 1;
@@ -582,7 +789,7 @@ private JPanel drawMedals() {
                 medal = medalAsImageIcon.getImage();
 
                 medalLabel.setIcon(new ImageIcon(medal));
-                medalLabel.setToolTipText(award.getTooltip(campaign.getCampaignOptions(), person));
+                medalLabel.setToolTipText(award.getTooltip(campaignOptions, person));
                 pnlMedals.add(medalLabel);
             } catch (Exception e) {
                 logger.error("", e);
@@ -628,7 +835,7 @@ private JPanel drawMiscAwards() {
                 misc = miscAsImageIcon.getImage();
 
                 miscLabel.setIcon(new ImageIcon(misc));
-                miscLabel.setToolTipText(award.getTooltip(campaign.getCampaignOptions(), person));
+                miscLabel.setToolTipText(award.getTooltip(campaignOptions, person));
                 pnlMiscs.add(miscLabel);
             } catch (Exception e) {
                 logger.error("", e);
@@ -664,7 +871,7 @@ public JPanel setPortrait() {
         gbc_lblPortrait.gridy = 0;
         gbc_lblPortrait.fill = GridBagConstraints.NONE;
         gbc_lblPortrait.anchor = GridBagConstraints.NORTHWEST;
-        gbc_lblPortrait.insets = new Insets(0, 0, 0, 0);
+        gbc_lblPortrait.insets = new Insets(0, 0, 10, 0);
         pnlPortrait.add(lblPortrait, gbc_lblPortrait);
 
         return pnlPortrait;
@@ -686,7 +893,7 @@ public JPanel setPortrait() {
      */
     private ImageIcon getPortraitImageIcon() {
         Portrait portrait = person.getPortrait();
-        ImageIcon portraitImageIcon = portrait.getImageIcon(100);
+        ImageIcon portraitImageIcon = portrait.getImageIcon(175);
 
         PersonnelStatus status = person.getStatus();
         if (status.isDead()) {
@@ -727,24 +934,9 @@ private JPanel fillInfo() {
 
         int y = 0;
 
-        lblType.setName("lblType");
-        lblType.setText(String.format(resourceMap.getString("format.italic"), person.getRoleDesc()));
-        lblType.getAccessibleContext().setAccessibleName("Role: " + person.getRoleDesc());
-        GridBagConstraints gridBagConstraints = new GridBagConstraints();
-        gridBagConstraints.gridx = 0;
-        gridBagConstraints.gridy = y;
-        gridBagConstraints.gridwidth = 4;
-        gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.weighty = 0.0;
-        gridBagConstraints.insets = new Insets(0, 0, 5, 0);
-        gridBagConstraints.fill = GridBagConstraints.NONE;
-        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-        pnlInfo.add(lblType, gridBagConstraints);
-        y++;
-
         lblStatus1.setName("lblStatus1");
         lblStatus1.setText(resourceMap.getString("lblStatus1.text"));
-        gridBagConstraints = new GridBagConstraints();
+        GridBagConstraints gridBagConstraints = new GridBagConstraints();
         gridBagConstraints.gridx = 0;
         gridBagConstraints.gridy = y;
         gridBagConstraints.fill = GridBagConstraints.NONE;
@@ -765,7 +957,7 @@ private JPanel fillInfo() {
         pnlInfo.add(lblStatus2, gridBagConstraints);
         y++;
 
-        if (campaign.getCampaignOptions().isShowOriginFaction()) {
+        if (campaignOptions.isShowOriginFaction()) {
             lblOrigin1.setName("lblOrigin1");
             lblOrigin1.setText(resourceMap.getString("lblOrigin1.text"));
             gridBagConstraints = new GridBagConstraints();
@@ -994,7 +1186,7 @@ public void mouseClicked(MouseEvent e) {
         // We show the following if track total earnings is on for a free person or if
         // the
         // person has previously tracked total earnings
-        if (campaign.getCampaignOptions().isTrackTotalEarnings() &&
+        if (campaignOptions.isTrackTotalEarnings() &&
                   (person.getPrisonerStatus().isFree() || person.getTotalEarnings().isGreaterThan(Money.zero()))) {
             JLabel lblTotalEarnings1 = new JLabel(resourceMap.getString("lblTotalEarnings1.text"));
             lblTotalEarnings1.setName("lblTotalEarnings1");
@@ -1022,7 +1214,7 @@ public void mouseClicked(MouseEvent e) {
         // We show the following if track total xp earnings is on for a free person or
         // if the
         // person has previously tracked total xp earnings
-        if (campaign.getCampaignOptions().isTrackTotalXPEarnings() &&
+        if (campaignOptions.isTrackTotalXPEarnings() &&
                   (person.getPrisonerStatus().isFree() || (person.getTotalXPEarnings() != 0))) {
             JLabel lblTotalXPEarnings1 = new JLabel(resourceMap.getString("lblTotalXPEarnings1.text"));
             lblTotalXPEarnings1.setName("lblTotalXPEarnings1");
@@ -1239,7 +1431,7 @@ public void mouseClicked(MouseEvent e) {
             }
         }
 
-        if (campaign.getCampaignOptions().getFamilyDisplayLevel().displayParentsChildrenSiblings()) {
+        if (campaignOptions.getFamilyDisplayLevel().displayParentsChildrenSiblings()) {
             final List<Person> children = person.getGenealogy().getChildren();
             if (!children.isEmpty()) {
                 lblChildren1.setName("lblChildren1");
@@ -1279,7 +1471,7 @@ public void mouseClicked(MouseEvent e) {
 
             final List<Person> grandchildren = person.getGenealogy().getGrandchildren();
             if (!grandchildren.isEmpty() &&
-                      campaign.getCampaignOptions().getFamilyDisplayLevel().displayGrandparentsGrandchildren()) {
+                      campaignOptions.getFamilyDisplayLevel().displayGrandparentsGrandchildren()) {
                 lblGrandchildren1.setName("lblGrandchildren1");
                 lblGrandchildren1.setText(resourceMap.getString("lblGrandchildren1.text"));
                 gridBagConstraints = new GridBagConstraints();
@@ -1388,8 +1580,7 @@ public void mouseClicked(MouseEvent e) {
             }
 
             final List<Person> grandparents = person.getGenealogy().getGrandparents();
-            if (!grandparents.isEmpty() &&
-                      campaign.getCampaignOptions().getFamilyDisplayLevel().displayGrandparentsGrandchildren()) {
+            if (!grandparents.isEmpty() && campaignOptions.getFamilyDisplayLevel().displayGrandparentsGrandchildren()) {
                 lblGrandparents1.setName("lblGrandparents1");
                 lblGrandparents1.setText(resourceMap.getString("lblGrandparents1.text"));
                 gridBagConstraints = new GridBagConstraints();
@@ -1428,8 +1619,7 @@ public void mouseClicked(MouseEvent e) {
             }
 
             final List<Person> auntsAndUncles = person.getGenealogy().getsAuntsAndUncles();
-            if (!auntsAndUncles.isEmpty() &&
-                      campaign.getCampaignOptions().getFamilyDisplayLevel().isAuntsUnclesCousins()) {
+            if (!auntsAndUncles.isEmpty() && campaignOptions.getFamilyDisplayLevel().isAuntsUnclesCousins()) {
                 lblAuntsOrUncles1.setName("lblAuntsOrUncles1");
                 lblAuntsOrUncles1.setText(resourceMap.getString("lblAuntsOrUncles1.text"));
                 gridBagConstraints = new GridBagConstraints();
@@ -1469,7 +1659,7 @@ public void mouseClicked(MouseEvent e) {
             }
 
             final List<Person> cousins = person.getGenealogy().getCousins();
-            if (!cousins.isEmpty() && campaign.getCampaignOptions().getFamilyDisplayLevel().isAuntsUnclesCousins()) {
+            if (!cousins.isEmpty() && campaignOptions.getFamilyDisplayLevel().isAuntsUnclesCousins()) {
                 lblCousins1.setName("lblCousins1");
                 lblCousins1.setText(resourceMap.getString("lblCousins1.text"));
                 gridBagConstraints = new GridBagConstraints();
@@ -1528,522 +1718,450 @@ private static String getRelativeName(Person relative) {
         }
     }
 
-    private JPanel fillSkills() {
-        // skill panel
+    /**
+     * Creates and returns a JPanel displaying a sorted list of skills arranged in columns, with each skill's name and
+     * corresponding value shown side-by-side. The panel's title is determined by the provided resource key. Skills are
+     * distributed evenly across a fixed number of columns (default is three).
+     *
+     * @param relevantSkills the list of skill names to display; will be sorted alphabetically
+     * @param titleKey       the resource key for the panel's titled border
+     *
+     * @return a {@link JPanel} containing the skill names and values in a grid layout
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private JPanel fillSkills(List<String> relevantSkills, String titleKey) {
+        Collections.sort(relevantSkills);
         JPanel pnlSkills = new JPanel(new GridBagLayout());
-        pnlSkills.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlSkills.title")));
-
-        // abilities and implants
-        JLabel lblTough1 = new JLabel();
-        JLabel lblTough2 = new JLabel();
-        JLabel lblConnections1 = new JLabel();
-        JLabel lblConnections2 = new JLabel();
-        JLabel lblWealth1 = new JLabel();
-        JLabel lblWealth2 = new JLabel();
-        JLabel lblReputation1 = new JLabel();
-        JLabel lblReputation2 = new JLabel();
-        JLabel lblEdge1 = new JLabel();
-        JLabel lblEdge2 = new JLabel();
-        JLabel lblEdgeAvail1 = new JLabel();
-        JLabel lblEdgeAvail2 = new JLabel();
-
-        JLabel lblLoyalty1 = new JLabel();
-        JLabel lblLoyalty2 = new JLabel();
-
-        JLabel lblFatigue1 = new JLabel();
-        JLabel lblFatigue2 = new JLabel();
-
-        // education
-        JLabel lblEducationLevel1 = new JLabel();
-        JLabel lblEducationLevel2 = new JLabel();
-        JLabel lblEducationStage1 = new JLabel();
-        JLabel lblEducationStage2 = new JLabel();
-        JLabel lblEducationJourneyDays1 = new JLabel();
-        JLabel lblEducationJourneyDays2 = new JLabel();
-        JLabel lblEducationDays1 = new JLabel();
-        JLabel lblEducationDays2 = new JLabel();
+        pnlSkills.setBorder(BorderFactory.createTitledBorder(resourceMap.getString(titleKey)));
 
-        GridBagConstraints gridBagConstraints;
-
-        JLabel lblName;
-        JLabel lblValue;
+        Attributes attributes = person.getATOWAttributes();
+        PersonnelOptions options = person.getOptions();
+        int adjustedReputation = person.getAdjustedReputation(campaignOptions.isUseAgeEffects(),
+              campaign.isClanCampaign(),
+              campaign.getLocalDate(),
+              person.getRankLevel());
 
-        int firsty = 0;
-        int colBreak = Math.max((int) Math.ceil(person.getSkillNumber() / 2.0) + 1, 3);
-        int addition = 0;
-        double weight = 0.5;
-
-        int j = 0;
-        for (int i = 0; i < SkillType.getSkillList().length; i++) {
-            String skillName = SkillType.getSkillList()[i];
-            SkillType type = SkillType.getType(skillName);
-            if (person.hasSkill(skillName)) {
-                j++;
-                if (j == colBreak) {
-                    addition = 2;
-                    firsty = 0;
-                    weight = 1.0;
-                }
-                if (type.isRoleplaySkill()) {
-                    lblName = new JLabel(String.format(resourceMap.getString("format.itemHeader.roleplay"),
-                          skillName.replaceAll(Pattern.quote(RP_ONLY_TAG), "")));
-                } else {
-                    lblName = new JLabel(String.format(resourceMap.getString("format.itemHeader"), skillName));
-                }
-                int reputation = person.getAdjustedReputation(campaign.getCampaignOptions().isUseAgeEffects(),
-                      campaign.isClanCampaign(),
-                      campaign.getLocalDate(),
-                      person.getRankLevel());
-                lblValue = new JLabel(person.getSkill(skillName).toString(person.getOptions(), reputation));
-                lblName.setLabelFor(lblValue);
-                gridBagConstraints = new GridBagConstraints();
-                gridBagConstraints.gridx = addition;
-                gridBagConstraints.gridy = firsty;
-                gridBagConstraints.fill = GridBagConstraints.NONE;
-                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                pnlSkills.add(lblName, gridBagConstraints);
-                gridBagConstraints = new GridBagConstraints();
-                gridBagConstraints.gridx = 1 + addition;
-                gridBagConstraints.gridy = firsty;
-                gridBagConstraints.weightx = weight;
-                gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-                gridBagConstraints.fill = GridBagConstraints.NONE;
-                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                pnlSkills.add(lblValue, gridBagConstraints);
-                firsty++;
+        // Calculate how many rows per column for even distribution
+        double numColumns = 3.0;
+        int skillsPerColumn = (int) ceil(relevantSkills.size() / numColumns);
+
+        for (int i = 0; i < relevantSkills.size(); i++) {
+            int column = i / skillsPerColumn; // 0, 1, 2
+            int row = i % skillsPerColumn;
+            int gridX = column * 2; // Each column takes 2 grid positions: name + value
+
+            String skillName = relevantSkills.get(i);
+            Skill skill = person.getSkill(skillName);
+
+            JLabel lblName = new JLabel(String.format(resourceMap.getString("format.itemHeader"),
+                  skillName.replaceAll(Pattern.quote(RP_ONLY_TAG), "")));
+
+            // TODO enable once attribute modifier sare implemented
+            //            int attributeModifier = skill.getTotalAttributeModifier(attributes, skill.getType());
+            int attributeModifier = 0;
+            int spaModifier = skill.getSPAModifiers(options, adjustedReputation);
+            int ageModifier = skill.getAgingModifier();
+            int totalModifier = attributeModifier + spaModifier + ageModifier;
+
+            String color = "";
+            String icon = "";
+            if (totalModifier != 0) {
+                color = totalModifier < 0 ?
+                              MekHQ.getMHQOptions().getFontColorNegativeHexColor() :
+                              MekHQ.getMHQOptions().getFontColorPositiveHexColor();
+                icon = totalModifier < 0 ? "&#x25BC" : "&#x25B2";
             }
-        }
-
-        // reset firsty
-        firsty = colBreak;
-
-        if (campaign.getCampaignOptions().isUseAbilities() &&
-                  (person.countOptions(PersonnelOptions.LVL3_ADVANTAGES) > 0)) {
-            JLabel lblAbility1 = new JLabel(resourceMap.getString("lblAbility1.text"));
-            lblAbility1.setName("lblAbility1");
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblAbility1, gridBagConstraints);
-
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
 
-            for (Enumeration<IOption> i = person.getOptions(PersonnelOptions.LVL3_ADVANTAGES); i.hasMoreElements(); ) {
-                IOption option = i.nextElement();
-                if (option.booleanValue()) {
-                    JLabel lblAbility2 = new JLabel(Utilities.getOptionDisplayName(option));
-                    lblAbility2.setToolTipText(wordWrap(option.getDescription().replaceAll("\\n", "<br>")));
-                    lblAbility2.setName("lblAbility2");
-                    lblAbility2.getAccessibleContext()
-                          .getAccessibleRelationSet()
-                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblAbility1));
-                    gridBagConstraints.gridy = firsty++;
-                    pnlSkills.add(lblAbility2, gridBagConstraints);
-                }
+            String adjustment = "";
+            if (!color.isBlank()) {
+                adjustment = String.format(" %s%s%s", spanOpeningWithCustomColor(color), icon, CLOSING_SPAN_TAG);
             }
+
+            // TODO enable once attribute modifier sare implemented
+            //            JLabel lblValue = new JLabel(String.format("<html>%s%s</html>", skill.toString(options, attributes, adjustedReputation), adjustment));
+            JLabel lblValue = new JLabel(String.format("<html>%s%s</html>",
+                  skill.toString(options, adjustedReputation),
+                  adjustment));
+            lblName.setLabelFor(lblValue);
+            String tooltip = wordWrap(skill.getTooltip(options, attributes, adjustedReputation));
+            lblName.setToolTipText(tooltip);
+            lblValue.setToolTipText(tooltip);
+
+            // Name label constraints
+            GridBagConstraints nameConstraints = new GridBagConstraints();
+            nameConstraints.gridx = gridX;
+            nameConstraints.gridy = row;
+            nameConstraints.anchor = GridBagConstraints.NORTHWEST;
+
+            // Value label constraints
+            GridBagConstraints valueConstraints = new GridBagConstraints();
+            valueConstraints.gridx = gridX + 1;
+            valueConstraints.gridy = row;
+            valueConstraints.anchor = GridBagConstraints.NORTHWEST;
+            valueConstraints.insets = new Insets(0, 5, 0, 10);
+            valueConstraints.weightx = 1;
+
+            pnlSkills.add(lblName, nameConstraints);
+            pnlSkills.add(lblValue, valueConstraints);
         }
 
-        if (campaign.getCampaignOptions().isUseImplants() &&
-                  (person.countOptions(PersonnelOptions.MD_ADVANTAGES) > 0)) {
-            JLabel lblImplants1 = new JLabel(resourceMap.getString("lblImplants1.text"));
-            lblImplants1.setName("lblImplants1");
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblImplants1, gridBagConstraints);
+        return pnlSkills;
+    }
 
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
+    /**
+     * Creates and returns a JPanel displaying attribute modifiers arranged in columns, with each attribute's name and
+     * its corresponding modifier value shown side-by-side. The attributes are distributed evenly across a fixed number
+     * of columns (default is three). The panel's title is set using a localized resource key.
+     *
+     * @param relevantAttributes a map of {@link SkillAttribute} to their integer modifier values; each entry will be
+     *                           displayed as a name/value pair
+     *
+     * @return a {@link JPanel} containing the attribute names and values in a grid layout
+     */
+    private JPanel fillAttributeModifiers(Map<SkillAttribute, Integer> relevantAttributes) {
+        JPanel pnlAttributes = new JPanel(new GridBagLayout());
+        pnlAttributes.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlSkills.attributes")));
 
-            for (Enumeration<IOption> i = person.getOptions(PersonnelOptions.MD_ADVANTAGES); i.hasMoreElements(); ) {
-                IOption option = i.nextElement();
+        // Calculate how many rows per column for even distribution
+        double numColumns = 3.0;
+        int skillsPerColumn = (int) ceil(relevantAttributes.size() / numColumns);
 
-                if (option.booleanValue()) {
-                    JLabel lblImplants2 = new JLabel(Utilities.getOptionDisplayName(option));
-                    lblImplants2.setToolTipText(wordWrap(option.getDescription().replaceAll("\\n", "<br>")));
-                    lblImplants2.setName("lblImplants2");
-                    lblImplants2.getAccessibleContext()
-                          .getAccessibleRelationSet()
-                          .add(new AccessibleRelation(AccessibleRelation.LABELED_BY, lblImplants1));
-                    gridBagConstraints.gridy = firsty++;
-                    pnlSkills.add(lblImplants2, gridBagConstraints);
-                }
-            }
-        }
-
-        int edge = person.getAdjustedEdge();
-        if (campaign.getCampaignOptions().isUseEdge() && (edge != 0)) {
-            lblEdge1.setName("lblEdge1");
-            lblEdge1.setText(resourceMap.getString("lblEdge1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblEdge1, gridBagConstraints);
+        int i = 0;
+        for (SkillAttribute attribute : relevantAttributes.keySet()) {
+            int column = i / skillsPerColumn; // 0, 1, 2
+            int row = i % skillsPerColumn;
+            int gridX = column * 2; // Each column takes 2 grid positions: name + value
+
+            String attributeName = attribute.getLabel();
+            int attributeModifier = relevantAttributes.get(attribute);
+
+            JLabel lblName = new JLabel(String.format(resourceMap.getString("format.itemHeader"), attributeName));
+            JLabel lblValue = new JLabel((attributeModifier > 0 ? "+" : "") + attributeModifier);
+            lblName.setLabelFor(lblValue);
+
+            String tooltip = wordWrap(attribute.getDescription());
+            lblName.setToolTipText(tooltip);
+            lblValue.setToolTipText(tooltip);
+
+            // Name label constraints
+            GridBagConstraints nameConstraints = new GridBagConstraints();
+            nameConstraints.gridx = gridX;
+            nameConstraints.gridy = row;
+            nameConstraints.anchor = GridBagConstraints.NORTHWEST;
+
+            // Value label constraints
+            GridBagConstraints valueConstraints = new GridBagConstraints();
+            valueConstraints.gridx = gridX + 1;
+            valueConstraints.gridy = row;
+            valueConstraints.anchor = GridBagConstraints.NORTHWEST;
+            valueConstraints.insets = new Insets(0, 5, 0, 10);
+            valueConstraints.weightx = 1;
+
+            pnlAttributes.add(lblName, nameConstraints);
+            pnlAttributes.add(lblValue, valueConstraints);
 
-            lblEdge2.setName("lblEdge2");
-            lblEdge1.setLabelFor(lblEdge2);
-            lblEdge2.setText("" + person.getCurrentEdge() + '/' + person.getEdge());
-            lblEdge2.setToolTipText(person.getEdgeTooltip());
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 1;
-            gridBagConstraints.weightx = 0.5;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblEdge2, gridBagConstraints);
-            firsty++;
+            i++;
         }
 
-        int totalToughness = person.getToughness();
-        if ((campaign.getCampaignOptions().isUseToughness()) && (totalToughness != 0)) {
-            lblTough1.setName("lblTough1");
-            lblTough1.setText(resourceMap.getString("lblTough1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblTough1, gridBagConstraints);
+        return pnlAttributes;
+    }
 
-            lblTough2.setName("lblTough2");
-            lblTough2.setText((totalToughness >= 0 ? "+" : "") + totalToughness);
-            lblTough1.setLabelFor(lblTough2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblTough2, gridBagConstraints);
+    private JPanel fillAbilitiesAndImplants(List<SpecialAbility> relevantAbilities) {
+        JPanel pnlAbilitiesAndImplants = new JPanel(new GridBagLayout());
+        pnlAbilitiesAndImplants.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlSkills.abilities")));
 
-            firsty++;
-        }
+        // Calculate how many rows per column for even distribution
+        double numColumns = 3.0;
+        int skillsPerColumn = (int) ceil(relevantAbilities.size() / numColumns);
 
-        if (person.getConnections() > 0) {
-            lblConnections1.setName("lblConnections1");
-            lblConnections1.setText(resourceMap.getString("lblConnections1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblConnections1, gridBagConstraints);
+        for (int i = 0; i < relevantAbilities.size(); i++) {
+            int column = i / skillsPerColumn; // 0, 1
+            int row = i % skillsPerColumn;
 
-            lblConnections2.setName("lblConnections2");
-            lblConnections2.setText(person.getConnections() + "");
-            lblConnections1.setLabelFor(lblConnections2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblConnections2, gridBagConstraints);
+            SpecialAbility ability = relevantAbilities.get(i);
+            String name = ability.getDisplayName();
+            String description = ability.getDescription();
+            boolean isFlaw = ability.getCost() < -1; // -1 is used to designate an origin only SPA
 
-            firsty++;
+            String adjustment = "";
+            if (isFlaw) {
+                String color = MekHQ.getMHQOptions().getFontColorNegativeHexColor();
+                String icon = "&#x25BC;";
+                adjustment = String.format(" %s%s%s", spanOpeningWithCustomColor(color), icon, CLOSING_SPAN_TAG);
+            }
+
+            JLabel lblName = new JLabel(String.format("<html>%s%s</html>",
+                  name.replaceAll("\\s*\\([^)]*\\)", ""),
+                  adjustment));
+            String tooltip = wordWrap(description);
+            lblName.setToolTipText(tooltip);
+
+            // Name label constraints
+            GridBagConstraints nameConstraints = new GridBagConstraints();
+            nameConstraints.gridx = column;
+            nameConstraints.gridy = row;
+            nameConstraints.anchor = GridBagConstraints.NORTHWEST;
+            nameConstraints.insets = new Insets(0, 5, 0, 10);
+            nameConstraints.weightx = 1;
+
+            pnlAbilitiesAndImplants.add(lblName, nameConstraints);
         }
 
-        if (person.getWealth() != 0) {
-            lblWealth1.setName("lblWealth1");
-            lblWealth1.setText(resourceMap.getString("lblWealth1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblWealth1, gridBagConstraints);
+        return pnlAbilitiesAndImplants;
+    }
 
-            lblWealth2.setName("lblWealth2");
-            lblWealth2.setText(person.getWealth() + "");
-            lblWealth1.setLabelFor(lblWealth2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblWealth2, gridBagConstraints);
+    private JPanel fillOther() {
+        JPanel pnlOther = new JPanel(new GridBagLayout());
+        pnlOther.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlSkills.traits")));
+
+        JLabel lblConnections = null;
+        int connections = person.getConnections();
+        if (connections != 0) {
+            String connectionsLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblConnections.text"),
+                  connections,
+                  "");
+            lblConnections = new JLabel(connectionsLabel);
+            lblConnections.setToolTipText(wordWrap(resourceMap.getString("lblConnections.tooltip")));
+        }
 
-            firsty++;
+        JLabel lblWealth = null;
+        int wealth = person.getWealth();
+        if (wealth != 0) {
+            String wealthLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblWealth.text"),
+                  wealth,
+                  "");
+            lblWealth = new JLabel(wealthLabel);
+            lblWealth.setToolTipText(wordWrap(resourceMap.getString("lblWealth.tooltip")));
         }
 
-        int reputation = person.getAdjustedReputation(campaign.getCampaignOptions().isUseAgeEffects(),
+        JLabel lblReputation = null;
+        int baseReputation = person.getReputation();
+        int adjustedReputation = person.getAdjustedReputation(campaignOptions.isUseAgeEffects(),
               campaign.isClanCampaign(),
               campaign.getLocalDate(),
               person.getRankLevel());
-
-        if (reputation != 0) {
-            lblReputation1.setName("lblReputation1");
-            lblReputation1.setText(resourceMap.getString("lblReputation1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblReputation1, gridBagConstraints);
-
-            lblReputation2.setName("lblReputation2");
-            lblReputation2.setText(reputation + "");
-            lblReputation1.setLabelFor(lblReputation2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblReputation2, gridBagConstraints);
-
-            firsty++;
+        if (baseReputation != 0 || adjustedReputation != 0) {
+            String adjustment = getTraitAdjustmentIcon(baseReputation, adjustedReputation);
+            String reputationLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblReputation.text"),
+                  adjustedReputation,
+                  adjustment);
+            lblReputation = new JLabel(reputationLabel);
+            lblReputation.setToolTipText(wordWrap(String.format(resourceMap.getString("lblReputation.tooltip"),
+                  baseReputation,
+                  adjustedReputation)));
         }
 
-        int loyaltyModifier = person.getLoyaltyModifier(person.getLoyalty());
+        JLabel lblToughness = null;
+        int totalToughness = person.getToughness();
+        if ((campaignOptions.isUseToughness()) && (totalToughness != 0)) {
+            String toughnessLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblToughness.text"),
+                  totalToughness,
+                  "");
+            lblToughness = new JLabel(toughnessLabel);
+            lblToughness.setToolTipText(wordWrap(resourceMap.getString("lblToughness.tooltip")));
+        }
 
-        if (person.isCommander()) {
-            loyaltyModifier = person.getLoyaltyModifier(person.getLoyalty() + 2);
+        JLabel lblEdge = null;
+        int baseEdge = person.getEdge();
+        int adjustedEdge = person.getAdjustedEdge();
+        int currentEdge = person.getCurrentEdge();
+        if (campaignOptions.isUseEdge() && (baseEdge != 0 || adjustedEdge != 0)) {
+            String adjustment = getTraitAdjustmentIcon(baseEdge, adjustedEdge);
+            String edgeLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblEdge.text"),
+                  currentEdge + "/" + adjustedEdge,
+                  adjustment);
+            lblEdge = new JLabel(edgeLabel);
+            lblEdge.setToolTipText(wordWrap(resourceMap.getString("lblEdge.tooltip")));
         }
 
-        if ((campaign.getCampaignOptions().isUseLoyaltyModifiers()) &&
-                  (!campaign.getCampaignOptions().isUseHideLoyalty()) &&
+        JLabel lblLoyalty = null;
+        int loyaltyModifier = person.getLoyaltyModifier(person.getLoyalty());
+        if ((campaignOptions.isUseLoyaltyModifiers()) && (!campaignOptions.isUseHideLoyalty()) &&
                   (loyaltyModifier != 0)) {
-            lblLoyalty1.setName("lblLoyalty1");
-            lblLoyalty1.setText(resourceMap.getString("lblLoyalty1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblLoyalty1, gridBagConstraints);
-
-            lblLoyalty2.setName("lblLoyalty2");
-
-            lblLoyalty2.setText(loyaltyModifier + " (" + getLoyaltyName(loyaltyModifier) + ')');
-            lblLoyalty2.setLabelFor(lblLoyalty2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblLoyalty2, gridBagConstraints);
-
-            firsty++;
+            String loyaltyLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblLoyalty.text"),
+                  loyaltyModifier + " (" + getLoyaltyName(loyaltyModifier) + ')',
+                  "");
+            lblLoyalty = new JLabel(loyaltyLabel);
+            lblLoyalty.setToolTipText(wordWrap(resourceMap.getString("lblLoyalty.tooltip")));
         }
 
-        int fatigue = person.getFatigue();
-        if (campaign.getCampaignOptions().isUseFatigue() && fatigue > 0) {
-            lblFatigue1.setName("lblFatigue1");
-            lblFatigue1.setText(resourceMap.getString("lblFatigue1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblFatigue1, gridBagConstraints);
-
+        JLabel lblFatigue = null;
+        int baseFatigue = person.getFatigue();
+        int effectiveFatigue = getEffectiveFatigue(person.getFatigue(),
+              person.isClanPersonnel(),
+              person.getSkillLevel(campaign, false),
+              campaign.getFieldKitchenWithinCapacity());
+        if (campaignOptions.isUseFatigue() && (baseFatigue != 0 || effectiveFatigue != 0)) {
             StringBuilder fatigueDisplay = new StringBuilder("<html>");
-
-            int effectiveFatigue = getEffectiveFatigue(person.getFatigue(),
-                  person.isClanPersonnel(),
-                  person.getSkillLevel(campaign, false),
-                  campaign.getFieldKitchenWithinCapacity());
             int fatigueTurnoverModifier = MathUtility.clamp(((effectiveFatigue - 1) / 4) - 1, 0, 3);
-
-            if (effectiveFatigue != fatigue) {
-                fatigueDisplay.append("
<font color='gray'>")
-                      .append(fatigue)
+            if (effectiveFatigue != baseFatigue) {
+                fatigueDisplay.append("
<font color='gray'>").append(baseFatigue)
                       .append("</font>
 ")
                       .append(effectiveFatigue);
             } else {
                 fatigueDisplay.append("<font color='gray'>").append(effectiveFatigue).append("</font>");
             }
-
             if (fatigueTurnoverModifier > 0) {
                 fatigueDisplay.append(" (-").append(fatigueTurnoverModifier).append(')');
             }
-
             fatigueDisplay.append("</html>");
 
-            lblFatigue2.setName("lblFatigue2");
-            lblFatigue2.setText(fatigueDisplay.toString());
-            lblFatigue2.setLabelFor(lblFatigue2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblFatigue2, gridBagConstraints);
-
-            firsty++;
+            String adjustment = getTraitAdjustmentIcon(baseFatigue, effectiveFatigue);
+            String fatigueLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblFatigue.text"),
+                  fatigueDisplay,
+                  adjustment);
+            lblFatigue = new JLabel(fatigueLabel);
+            lblFatigue.setToolTipText(wordWrap(resourceMap.getString("lblFatigue.tooltip")));
         }
 
-        if (campaign.getCampaignOptions().isUseEducationModule()) {
-            lblEducationLevel1.setName("lblEducationLevel1");
-            lblEducationLevel1.setText(resourceMap.getString("lblEducationLevel1.text"));
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblEducationLevel1, gridBagConstraints);
-
-            lblEducationLevel2.setName("lblEducationLevel2");
-            lblEducationLevel2.setText(person.getEduHighestEducation().toString());
-            lblEducationLevel1.setLabelFor(lblEducationLevel2);
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 1;
-            gridBagConstraints.gridy = firsty;
-            gridBagConstraints.gridwidth = 3;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-            gridBagConstraints.fill = GridBagConstraints.NONE;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            pnlSkills.add(lblEducationLevel2, gridBagConstraints);
-
-            firsty++;
-
-            if (person.getEduEducationStage() != EducationStage.NONE) {
-                lblEducationStage1.setName("lblEducationStage1");
-                lblEducationStage1.setText(resourceMap.getString("lblEducationStage1.text"));
-                gridBagConstraints = new GridBagConstraints();
-                gridBagConstraints.gridx = 0;
-                gridBagConstraints.gridy = firsty;
-                gridBagConstraints.fill = GridBagConstraints.NONE;
-                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                pnlSkills.add(lblEducationStage1, gridBagConstraints);
-
-                lblEducationStage2.setName("lblEducationStage2");
-                lblEducationStage2.setText(person.getEduEducationStage().toString());
-                lblEducationStage2.setLabelFor(lblEducationStage2);
-                gridBagConstraints = new GridBagConstraints();
-                gridBagConstraints.gridx = 1;
-                gridBagConstraints.gridy = firsty;
-                gridBagConstraints.gridwidth = 3;
-                gridBagConstraints.weightx = 1.0;
-                gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-                gridBagConstraints.fill = GridBagConstraints.NONE;
-                gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                pnlSkills.add(lblEducationStage2, gridBagConstraints);
-
-                firsty++;
-
-                String educationText;
-
-                switch (person.getEduEducationStage()) {
-                    case EDUCATION:
-                        lblEducationDays1.setName("lblEducationDays1");
-                        lblEducationDays1.setText(resourceMap.getString("lblEducationDays1.text"));
-                        gridBagConstraints = new GridBagConstraints();
-                        gridBagConstraints.gridx = 0;
-                        gridBagConstraints.gridy = firsty;
-                        gridBagConstraints.fill = GridBagConstraints.NONE;
-                        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                        pnlSkills.add(lblEducationDays1, gridBagConstraints);
-
-                        Academy academy = EducationController.getAcademy(person.getEduAcademySet(),
-                              person.getEduAcademyNameInSet());
-
-                        if (academy == null) {
-                            logger.debug("Found null academy for {} skipping", person.getFullTitle());
+        JLabel lblHighestEducation = null;
+        JLabel lblEducationStage = null;
+        if (campaignOptions.isUseEducationModule()) {
+            EducationLevel highestEducation = person.getEduHighestEducation();
+            String highestEducationLabel = String.format(resourceMap.getString("format.traitValue"),
+                  resourceMap.getString("lblHighestEducation.text"),
+                  highestEducation,
+                  "");
+            lblHighestEducation = new JLabel(highestEducationLabel);
+            lblHighestEducation.setToolTipText(wordWrap(highestEducation.getToolTipText()));
+
+            EducationStage educationStage = person.getEduEducationStage();
+            Academy academy = EducationController.getAcademy(person.getEduAcademySet(),
+                  person.getEduAcademyNameInSet());
+
+            if (academy != null && educationStage != EducationStage.NONE) {
+                String educationLabel;
+                String educationValue;
+                switch (educationStage) {
+                    case EDUCATION -> {
+                        educationLabel = resourceMap.getString("lblEducationStage.educationTime");
+
+                        if (academy.isPrepSchool()) {
+                            educationValue = String.format(resourceMap.getString("lblEducationDurationAge.text"),
+                                  academy.getAgeMax());
                         } else {
-                            lblEducationDays2.setName("lblEducationDays2");
-                            if (academy.isPrepSchool()) {
-                                educationText = String.format(resourceMap.getString("lblEducationDurationAge.text"),
-                                      academy.getAgeMax());
-                            } else {
-                                educationText = String.format(resourceMap.getString("lblEducationDurationDays.text"),
-                                      person.getEduEducationTime());
-                            }
-
-                            lblEducationDays2.setName("lblEducationDays2");
-                            lblEducationDays2.setText(educationText);
-                            lblEducationDays2.setLabelFor(lblEducationDays2);
-                            gridBagConstraints = new GridBagConstraints();
-                            gridBagConstraints.gridx = 1;
-                            gridBagConstraints.gridy = firsty;
-                            gridBagConstraints.gridwidth = 3;
-                            gridBagConstraints.weightx = 1.0;
-                            gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-                            gridBagConstraints.fill = GridBagConstraints.NONE;
-                            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                            pnlSkills.add(lblEducationDays2, gridBagConstraints);
+                            educationValue = String.format(resourceMap.getString("lblEducationDurationDays.text"),
+                                  person.getEduEducationTime());
                         }
+                    }
+                    case JOURNEY_TO_CAMPUS, JOURNEY_FROM_CAMPUS -> {
+                        educationLabel = resourceMap.getString("lblEducationStage.journeyTime");
 
-                        break;
-                    case JOURNEY_TO_CAMPUS:
-                    case JOURNEY_FROM_CAMPUS:
-                        lblEducationJourneyDays1.setName("lblEducationJourneyDays1");
-                        lblEducationJourneyDays1.setText(resourceMap.getString("lblEducationJourneyDays1.text"));
-                        gridBagConstraints = new GridBagConstraints();
-                        gridBagConstraints.gridx = 0;
-                        gridBagConstraints.gridy = firsty;
-                        gridBagConstraints.fill = GridBagConstraints.NONE;
-                        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                        pnlSkills.add(lblEducationJourneyDays1, gridBagConstraints);
-
-                        if (person.getEduEducationStage() == EducationStage.JOURNEY_TO_CAMPUS) {
-                            educationText = String.format(resourceMap.getString("lblEducationTravelTo.text"),
+                        if (educationStage.isJourneyToCampus()) {
+                            educationValue = String.format(resourceMap.getString("lblEducationTravelTo.text"),
                                   person.getEduDaysOfTravel(),
                                   person.getEduJourneyTime(),
                                   campaign.getSystemById(person.getEduAcademySystem())
                                         .getName(campaign.getLocalDate()));
                         } else {
-                            educationText = String.format(resourceMap.getString("lblEducationTravelFrom.text"),
+                            educationValue = String.format(resourceMap.getString("lblEducationTravelFrom.text"),
                                   person.getEduDaysOfTravel(),
                                   person.getEduJourneyTime(),
                                   campaign.getSystemById(person.getEduAcademySystem())
                                         .getName(campaign.getLocalDate()));
 
                         }
-
-                        lblEducationJourneyDays2.setName("lblEducationJourneyDays2");
-                        lblEducationJourneyDays2.setText(educationText);
-                        lblEducationJourneyDays2.setLabelFor(lblEducationJourneyDays2);
-                        gridBagConstraints = new GridBagConstraints();
-                        gridBagConstraints.gridx = 1;
-                        gridBagConstraints.gridy = firsty;
-                        gridBagConstraints.gridwidth = 3;
-                        gridBagConstraints.weightx = 1.0;
-                        gridBagConstraints.insets = new Insets(0, 10, 0, 0);
-                        gridBagConstraints.fill = GridBagConstraints.NONE;
-                        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-                        pnlSkills.add(lblEducationJourneyDays2, gridBagConstraints);
-                        break;
-                    case GRADUATING:
-                    case DROPPING_OUT:
-                    case NONE:
-
-                        break;
+                    }
+                    default -> {
+                        educationLabel = educationStage.toString();
+                        educationValue = "-";
+                    }
                 }
+
+                String educationStageLabel = String.format(resourceMap.getString("format.traitValue"),
+                      educationLabel,
+                      educationValue,
+                      "");
+                lblEducationStage = new JLabel(educationStageLabel);
+                lblEducationStage.setToolTipText(wordWrap(educationStage.getToolTipText()));
             }
         }
 
-        return pnlSkills;
+        List<JLabel> components = new ArrayList<>();
+        if (lblConnections != null) {
+            components.add(lblConnections);
+        }
+        if (lblWealth != null) {
+            components.add(lblWealth);
+        }
+        if (lblReputation != null) {
+            components.add(lblReputation);
+        }
+        if (lblToughness != null) {
+            components.add(lblToughness);
+        }
+        if (lblEdge != null) {
+            components.add(lblEdge);
+        }
+        if (lblLoyalty != null) {
+            components.add(lblLoyalty);
+        }
+        if (lblFatigue != null) {
+            components.add(lblFatigue);
+        }
+        if (lblHighestEducation != null) {
+            components.add(lblHighestEducation);
+        }
+        if (lblEducationStage != null) {
+            components.add(lblEducationStage);
+        }
+
+        GridBagConstraints gbc = new GridBagConstraints();
+        gbc.anchor = GridBagConstraints.WEST;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        gbc.weightx = 1.0;
+
+        int total = components.size();
+        int numRows = (int) Math.ceil(total / 2.0);
+
+        for (int i = 0; i < total; i++) {
+            int col = i / numRows;
+            int row = i % numRows;
+
+            gbc.gridx = col;
+            gbc.gridy = row;
+            pnlOther.add(components.get(i), gbc);
+        }
+
+        return pnlOther;
+    }
+
+    /**
+     * Returns an HTML-formatted icon indicating whether the adjusted trait value is higher or lower than the base
+     * value.
+     *
+     * <p>If the adjusted value is lower than the base value, a downward arrow is returned,
+     * colored based on the configured negative color. If the adjusted value is higher, an upward arrow is returned,
+     * colored according to the configured positive color. When there is no adjustment (base and adjusted values are
+     * equal), an empty string is returned.</p>
+     *
+     * @param baseValue     the original or unmodified value of the trait
+     * @param adjustedValue the value of the trait after adjustment
+     *
+     * @return an HTML string containing a colored up or down arrow, or an empty string if there is no adjustment
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private static String getTraitAdjustmentIcon(int baseValue, int adjustedValue) {
+        String adjustment = "";
+        if (baseValue > adjustedValue) {
+            String color = MekHQ.getMHQOptions().getFontColorNegativeHexColor();
+            adjustment = String.format(" %s%s%s", spanOpeningWithCustomColor(color), "&#x25BC", CLOSING_SPAN_TAG);
+        } else if (baseValue < adjustedValue) {
+            String color = MekHQ.getMHQOptions().getFontColorPositiveHexColor();
+            adjustment = String.format(" %s%s%s", spanOpeningWithCustomColor(color), "&#x25B2", CLOSING_SPAN_TAG);
+        }
+        return adjustment;
     }
 
     /**
@@ -2170,27 +2288,6 @@ private JPanel fillInjuries() {
         JPanel pnlInjuries = new JPanel(new BorderLayout());
         pnlInjuries.setBorder(BorderFactory.createTitledBorder(resourceMap.getString("pnlInjuries.title")));
 
-        JButton medicalButton = new JButton(new ImageIcon("data/images/misc/medical.png"));
-        medicalButton.getAccessibleContext().setAccessibleName(resourceMap.getString("btnMedical.tooltip"));
-        medicalButton.addActionListener(event -> {
-            MedicalViewDialog medDialog = new MedicalViewDialog(SwingUtilities.getWindowAncestor(this),
-                  campaign,
-                  person);
-            medDialog.setModalityType(ModalityType.APPLICATION_MODAL);
-            medDialog.setVisible(true);
-            removeAll();
-            repaint();
-            revalidate();
-            initComponents();
-            revalidate();
-            MekHQ.triggerEvent(new PersonChangedEvent(person));
-        });
-        medicalButton.setMaximumSize(new Dimension(32, 32));
-        medicalButton.setMargin(new Insets(0, 0, 0, 0));
-        medicalButton.setToolTipText(resourceMap.getString("btnMedical.tooltip"));
-        medicalButton.setAlignmentY(Component.TOP_ALIGNMENT);
-        pnlInjuries.add(medicalButton, BorderLayout.LINE_START);
-
         JPanel pnlInjuryDetails = new JPanel(new GridBagLayout());
         pnlInjuryDetails.getAccessibleContext().setAccessibleName("Injury Details for " + person.getFullName());
         pnlInjuryDetails.setAlignmentY(Component.TOP_ALIGNMENT);


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-07T00:20:37Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:22:21Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:22:43Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:22:52Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:23:01Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:23:09Z
user: login: Scoppio
body: approved, but there are things to consider
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:26:08Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:34:35Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:35:54Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:36:23Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:38:12Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:40:18Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:42:34Z
user: login: IllianiBird
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:48:30Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-07T02:54:59Z

Review comments:
diff_hunk: @@ -516,6 +523,64 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: github-advanced-security[bot]
body: ## Useless parameter

The parameter 'attributes' is never used.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9339)
created_at: 2025-05-07T00:20:37Z
author_association: NONE
diff_hunk: @@ -516,6 +523,64 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? "+" : "") + firstLinkedAttributeModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: github-advanced-security[bot]
body: ## Useless comparison test

Test is always false.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9335)
created_at: 2025-05-07T00:20:37Z
author_association: NONE
diff_hunk: @@ -516,6 +523,64 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? "+" : "") + firstLinkedAttributeModifier));
+
+        SkillAttribute secondLinkedAttribute = type.getSecondAttribute();
+        if (secondLinkedAttribute != SkillAttribute.NONE) {
+            // TODO enable once attribute modifier sare implemented
+            //            int secondLinkedAttributeModifier = attributes.getAttributeModifier(secondLinkedAttribute);
+            int secondLinkedAttributeModifier = 0;
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.linkedAttribute",
+                  secondLinkedAttribute.getLabel(),
+                  (secondLinkedAttributeModifier > 0 ? "+" : "") + secondLinkedAttributeModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: github-advanced-security[bot]
body: ## Useless comparison test

Test is always false.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9336)
created_at: 2025-05-07T00:20:37Z
author_association: NONE
diff_hunk: @@ -447,6 +559,79 @@
         add(Box.createGlue(), gridBagConstraints);
     }
 
+    /**
+     * Retrieves a list of relevant special abilities for the person based on the current campaign options.
+     *
+     * <p>This method checks if abilities and/or implants are enabled in the campaign options and,
+     * for each enabled category, iterates over the person's corresponding options. For each option that is selected
+     * (boolean value is true), it retrieves the corresponding {@link SpecialAbility} and adds it to the result list if
+     * it is not already present.</p>
+     *
+     * @return a {@link List} of unique relevant {@link SpecialAbility} instances available to the person, depending on
+     *       campaign settings and the person's selected advantages or implants
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private List<SpecialAbility> getRelevantAbilities() {
+        List<SpecialAbility> relevantAbilities = new ArrayList<>();
+        if (campaignOptions.isUseAbilities() && (person.countOptions(LVL3_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(LVL3_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+
+        if (campaignOptions.isUseImplants() && (person.countOptions(MD_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(MD_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+        return relevantAbilities;
+    }
+
+    /**
+     * Returns a map of relevant skill attributes and their corresponding modifiers for the person.
+     *
+     * <p>This method iterates over all possible {@link SkillAttribute} values (excluding {@link SkillAttribute#NONE}),
+     * retrieves each attribute's score for the person, and computes the associated modifier using
+     * {@link Skill#getIndividualAttributeModifier(int)}. Only attributes with a non-zero modifier are included in the
+     * result map.</p>
+     *
+     * @return a {@link Map} mapping each relevant {@link SkillAttribute} to its computed modifier for the person
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private Map<SkillAttribute, Integer> getRelevantAttributes() {
+        Map<SkillAttribute, Integer> relevantAttributes = new HashMap<>();
+        for (SkillAttribute attribute : SkillAttribute.values()) {
+            if (attribute == SkillAttribute.NONE) {
+                continue;
+            }
+
+            int attributeScore = person.getAttributeScore(attribute);
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: github-advanced-security[bot]
body: ## Unread local variable

Variable 'int attributeScore' is never read.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9338)
created_at: 2025-05-07T00:20:37Z
author_association: NONE
diff_hunk: @@ -447,6 +559,79 @@
         add(Box.createGlue(), gridBagConstraints);
     }
 
+    /**
+     * Retrieves a list of relevant special abilities for the person based on the current campaign options.
+     *
+     * <p>This method checks if abilities and/or implants are enabled in the campaign options and,
+     * for each enabled category, iterates over the person's corresponding options. For each option that is selected
+     * (boolean value is true), it retrieves the corresponding {@link SpecialAbility} and adds it to the result list if
+     * it is not already present.</p>
+     *
+     * @return a {@link List} of unique relevant {@link SpecialAbility} instances available to the person, depending on
+     *       campaign settings and the person's selected advantages or implants
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private List<SpecialAbility> getRelevantAbilities() {
+        List<SpecialAbility> relevantAbilities = new ArrayList<>();
+        if (campaignOptions.isUseAbilities() && (person.countOptions(LVL3_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(LVL3_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+
+        if (campaignOptions.isUseImplants() && (person.countOptions(MD_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(MD_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+        return relevantAbilities;
+    }
+
+    /**
+     * Returns a map of relevant skill attributes and their corresponding modifiers for the person.
+     *
+     * <p>This method iterates over all possible {@link SkillAttribute} values (excluding {@link SkillAttribute#NONE}),
+     * retrieves each attribute's score for the person, and computes the associated modifier using
+     * {@link Skill#getIndividualAttributeModifier(int)}. Only attributes with a non-zero modifier are included in the
+     * result map.</p>
+     *
+     * @return a {@link Map} mapping each relevant {@link SkillAttribute} to its computed modifier for the person
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private Map<SkillAttribute, Integer> getRelevantAttributes() {
+        Map<SkillAttribute, Integer> relevantAttributes = new HashMap<>();
+        for (SkillAttribute attribute : SkillAttribute.values()) {
+            if (attribute == SkillAttribute.NONE) {
+                continue;
+            }
+
+            int attributeScore = person.getAttributeScore(attribute);
+            // TODO enable once attribute modifier sare implemented
+            //            int modifier = getIndividualAttributeModifier(attributeScore);
+            int modifier = 0;
+            if (modifier != 0) {
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: github-advanced-security[bot]
body: ## Useless comparison test

Test is always false.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9337)
created_at: 2025-05-07T00:20:37Z
author_association: NONE
diff_hunk: @@ -516,6 +523,64 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: IllianiBird
body: This is prep for when attribute modifiers are implemented so I don't need to deprecate a bunch of stuff
created_at: 2025-05-07T00:22:21Z
author_association: COLLABORATOR
diff_hunk: @@ -516,6 +523,64 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? "+" : "") + firstLinkedAttributeModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: IllianiBird
body: This is prep for when attribute modifiers are active (in 50.07).
created_at: 2025-05-07T00:22:43Z
author_association: COLLABORATOR
diff_hunk: @@ -516,6 +523,64 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? "+" : "") + firstLinkedAttributeModifier));
+
+        SkillAttribute secondLinkedAttribute = type.getSecondAttribute();
+        if (secondLinkedAttribute != SkillAttribute.NONE) {
+            // TODO enable once attribute modifier sare implemented
+            //            int secondLinkedAttributeModifier = attributes.getAttributeModifier(secondLinkedAttribute);
+            int secondLinkedAttributeModifier = 0;
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.linkedAttribute",
+                  secondLinkedAttribute.getLabel(),
+                  (secondLinkedAttributeModifier > 0 ? "+" : "") + secondLinkedAttributeModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: IllianiBird
body: This is prep for when attribute modifiers are active (in 50.07).
created_at: 2025-05-07T00:22:52Z
author_association: COLLABORATOR
diff_hunk: @@ -447,6 +559,79 @@
         add(Box.createGlue(), gridBagConstraints);
     }
 
+    /**
+     * Retrieves a list of relevant special abilities for the person based on the current campaign options.
+     *
+     * <p>This method checks if abilities and/or implants are enabled in the campaign options and,
+     * for each enabled category, iterates over the person's corresponding options. For each option that is selected
+     * (boolean value is true), it retrieves the corresponding {@link SpecialAbility} and adds it to the result list if
+     * it is not already present.</p>
+     *
+     * @return a {@link List} of unique relevant {@link SpecialAbility} instances available to the person, depending on
+     *       campaign settings and the person's selected advantages or implants
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private List<SpecialAbility> getRelevantAbilities() {
+        List<SpecialAbility> relevantAbilities = new ArrayList<>();
+        if (campaignOptions.isUseAbilities() && (person.countOptions(LVL3_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(LVL3_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+
+        if (campaignOptions.isUseImplants() && (person.countOptions(MD_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(MD_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+        return relevantAbilities;
+    }
+
+    /**
+     * Returns a map of relevant skill attributes and their corresponding modifiers for the person.
+     *
+     * <p>This method iterates over all possible {@link SkillAttribute} values (excluding {@link SkillAttribute#NONE}),
+     * retrieves each attribute's score for the person, and computes the associated modifier using
+     * {@link Skill#getIndividualAttributeModifier(int)}. Only attributes with a non-zero modifier are included in the
+     * result map.</p>
+     *
+     * @return a {@link Map} mapping each relevant {@link SkillAttribute} to its computed modifier for the person
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private Map<SkillAttribute, Integer> getRelevantAttributes() {
+        Map<SkillAttribute, Integer> relevantAttributes = new HashMap<>();
+        for (SkillAttribute attribute : SkillAttribute.values()) {
+            if (attribute == SkillAttribute.NONE) {
+                continue;
+            }
+
+            int attributeScore = person.getAttributeScore(attribute);
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: IllianiBird
body: This is prep for when attribute modifiers are active (in 50.07).
created_at: 2025-05-07T00:23:01Z
author_association: COLLABORATOR
diff_hunk: @@ -447,6 +559,79 @@
         add(Box.createGlue(), gridBagConstraints);
     }
 
+    /**
+     * Retrieves a list of relevant special abilities for the person based on the current campaign options.
+     *
+     * <p>This method checks if abilities and/or implants are enabled in the campaign options and,
+     * for each enabled category, iterates over the person's corresponding options. For each option that is selected
+     * (boolean value is true), it retrieves the corresponding {@link SpecialAbility} and adds it to the result list if
+     * it is not already present.</p>
+     *
+     * @return a {@link List} of unique relevant {@link SpecialAbility} instances available to the person, depending on
+     *       campaign settings and the person's selected advantages or implants
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private List<SpecialAbility> getRelevantAbilities() {
+        List<SpecialAbility> relevantAbilities = new ArrayList<>();
+        if (campaignOptions.isUseAbilities() && (person.countOptions(LVL3_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(LVL3_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+
+        if (campaignOptions.isUseImplants() && (person.countOptions(MD_ADVANTAGES) > 0)) {
+            for (Enumeration<IOption> i = person.getOptions(MD_ADVANTAGES); i.hasMoreElements(); ) {
+                IOption option = i.nextElement();
+                if (option.booleanValue()) {
+                    SpecialAbility ability = getOption(option.getName());
+                    if (!relevantAbilities.contains(ability)) {
+                        relevantAbilities.add(ability);
+                    }
+                }
+            }
+        }
+        return relevantAbilities;
+    }
+
+    /**
+     * Returns a map of relevant skill attributes and their corresponding modifiers for the person.
+     *
+     * <p>This method iterates over all possible {@link SkillAttribute} values (excluding {@link SkillAttribute#NONE}),
+     * retrieves each attribute's score for the person, and computes the associated modifier using
+     * {@link Skill#getIndividualAttributeModifier(int)}. Only attributes with a non-zero modifier are included in the
+     * result map.</p>
+     *
+     * @return a {@link Map} mapping each relevant {@link SkillAttribute} to its computed modifier for the person
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private Map<SkillAttribute, Integer> getRelevantAttributes() {
+        Map<SkillAttribute, Integer> relevantAttributes = new HashMap<>();
+        for (SkillAttribute attribute : SkillAttribute.values()) {
+            if (attribute == SkillAttribute.NONE) {
+                continue;
+            }
+
+            int attributeScore = person.getAttributeScore(attribute);
+            // TODO enable once attribute modifier sare implemented
+            //            int modifier = getIndividualAttributeModifier(attributeScore);
+            int modifier = 0;
+            if (modifier != 0) {
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: IllianiBird
body: This is prep for when attribute modifiers are active (in 50.07).
created_at: 2025-05-07T00:23:09Z
author_association: COLLABORATOR
diff_hunk: @@ -92,6 +94,7 @@ public class Skill {
     public static int COUNT_UP_MAX_VALUE = 10;
     public static int COUNT_DOWN_MIN_VALUE = 0;
 
+    private static final String RESOURCE_BUNDLE = "mekhq.resources." + Skill.class.getSimpleName();
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: Scoppio
body: I suggest against doing this. If someone renames this class then this wont be pointing to the correct file anymore.
created_at: 2025-05-07T01:50:42Z
author_association: COLLABORATOR
diff_hunk: @@ -516,6 +523,64 @@ public String toString(PersonnelOptions options, int reputation) {
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: Scoppio
body: the symbols should also be in the i18n, Iknow this is fringe, but I always remember the problem with quotation marks, latin script uses "those", and they are usually "open - close", french uses a special double chevron << for important stuff >>, other languages use simple quotation mark, etc.
created_at: 2025-05-07T01:53:00Z
author_association: COLLABORATOR
diff_hunk: @@ -90,6 +91,7 @@
  * @author Jay Lawson (jaylawson39 at yahoo.com)
  */
 public class SkillType {
+    private static final String RESOURCE_BUNDLE = "mekhq.resources." + SkillType.class.getSimpleName();
path: MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java
user: login: Scoppio
body: same issue as before, this adds a level of indirection that is not necessary.
created_at: 2025-05-07T01:53:58Z
author_association: COLLABORATOR
diff_hunk: @@ -441,6 +470,44 @@ public String getName() {
         return name;
     }
 
+    /**
+     * Generates a resource bundle key derived from the {@code name} field by removing all occurrences of '/', '-', and
+     * whitespace characters.
+     *
+     * @return a normalized resource bundle key string with specific characters removed from {@code name}
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    private String getResourceBundleKey() {
+        String key = name;
+        key = key.replace(RP_ONLY_TAG, "");
+        key = key.replace("/", "");
+        key = key.replace("-", "");
+        key = key.replace(" ", "");
+        return key;
+    }
+
+    public String getFlavorText(boolean includeHtmlTags, boolean includeAttributes) {
+        String key = getResourceBundleKey();
+        String rawFlavorText = getTextAt(RESOURCE_BUNDLE, key + ".flavorText");
+
+        String htmlOpenTag = includeHtmlTags ? "<html>" : "";
+        String htmlCloseTag = includeHtmlTags ? "</html>" : "";
+
+        if (!includeAttributes) {
+            return htmlOpenTag + rawFlavorText + htmlCloseTag;
+        }
+
+        String flavorText = htmlOpenTag + rawFlavorText + "<br>(" + firstAttribute.getLabel();
+
+        if (secondAttribute != NONE) {
+            flavorText += ", " + secondAttribute.getLabel() + ')' + htmlCloseTag;
+        }
+
+        return flavorText;
+    }
path: MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java
user: login: Scoppio
body: There are better ways to do this... I think we should try to find some way to do this that can be easily replicated around the project bc its very commonly a problem we have to deal with. Dont worry about this piece of code atm, its fine.
created_at: 2025-05-07T01:55:47Z
author_association: COLLABORATOR
diff_hunk: @@ -322,7 +324,7 @@ private void changePersonnelView() {
                                             PersonnelTabView.GENERAL :
                                             choicePersonView.getSelectedItem();
         final XTableColumnModel columnModel = (XTableColumnModel) getPersonnelTable().getColumnModel();
-        getPersonnelTable().setRowHeight(UIUtil.scaleForGUI(15));
path: MekHQ/src/mekhq/gui/PersonnelTab.java
user: login: Scoppio
body: I suggest against doing static imports too liberally, as it can very quickly start to hide complexities and dependencies between modules
created_at: 2025-05-07T01:57:02Z
author_association: COLLABORATOR
diff_hunk: @@ -56,6 +56,7 @@
 import static mekhq.campaign.personnel.skills.Attributes.ATTRIBUTE_IMPROVEMENT_COST;
 import static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;
 import static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;
+import static mekhq.campaign.personnel.skills.SkillType.S_ARTILLERY;
path: MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java
user: login: Scoppio
body: same here, it makes much more sense to use SkillType.S_ARTILLERY in the body of the code., this also remove all the multi... OH.... This is not an enum... Ignore me here.
created_at: 2025-05-07T01:58:06Z
author_association: COLLABORATOR
diff_hunk: @@ -132,96 +165,58 @@ private void initComponents() {
         gbc_pnlPortrait.gridy = 0;
         gbc_pnlPortrait.fill = GridBagConstraints.NONE;
         gbc_pnlPortrait.anchor = GridBagConstraints.NORTHWEST;
-        gbc_pnlPortrait.insets = new Insets(10, 10, 0, 0);
+        gbc_pnlPortrait.insets = new Insets(10, 0, 0, 0);
         add(pnlPortrait, gbc_pnlPortrait);
 
         JPanel pnlInfo = fillInfo();
         GridBagConstraints gridBagConstraints = new GridBagConstraints();
         gridBagConstraints.gridx = 1;
         gridBagConstraints.gridy = 0;
         gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+        gridBagConstraints.insets = new Insets(0, 0, 10, 0);
         gridBagConstraints.fill = GridBagConstraints.BOTH;
         gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
         add(pnlInfo, gridBagConstraints);
 
         int gridy = 1;
 
-        JPanel pnlSkills = fillSkills();
-        gridBagConstraints = new GridBagConstraints();
-        gridBagConstraints.gridx = 0;
-        gridBagConstraints.gridy = gridy;
-        gridBagConstraints.gridwidth = 2;
-        gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-        gridBagConstraints.fill = GridBagConstraints.BOTH;
-        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-        add(pnlSkills, gridBagConstraints);
-        gridy++;
-
-        if (campaign.getCampaignOptions().isUseAdvancedMedical()) {
-            JPanel pnlInjuries = fillInjuries();
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
-            gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-            gridBagConstraints.fill = GridBagConstraints.BOTH;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlInjuries, gridBagConstraints);
-            gridy++;
-        }
-
-        if (!person.getGenealogy().isEmpty()) {
-            JPanel pnlFamily = fillFamily();
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
-            gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-            gridBagConstraints.fill = GridBagConstraints.BOTH;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlFamily, gridBagConstraints);
-            gridy++;
-        }
-
-        if (person.getAwardController().hasAwards()) {
-            if (person.getAwardController().hasAwardsWithRibbons()) {
+        PersonAwardController awardController = person.getAwardController();
+        if (awardController.hasAwards()) {
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: Scoppio
body: I know it is better now, but every "big component" should be a function call, so it is easier to find and fix/change things.
created_at: 2025-05-07T02:04:42Z
author_association: COLLABORATOR
diff_hunk: @@ -516,6 +523,64 @@ public String toString(PersonnelOptions options, int reputation) {
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: IllianiBird
body: That is very tedious but I'll see it done.
created_at: 2025-05-07T02:34:35Z
author_association: COLLABORATOR
diff_hunk: @@ -92,6 +94,7 @@ public class Skill {
     public static int COUNT_UP_MAX_VALUE = 10;
     public static int COUNT_DOWN_MIN_VALUE = 0;
 
+    private static final String RESOURCE_BUNDLE = "mekhq.resources." + Skill.class.getSimpleName();
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: IllianiBird
body: Great point. I'll have it done
created_at: 2025-05-07T02:35:54Z
author_association: COLLABORATOR
diff_hunk: @@ -90,6 +91,7 @@
  * @author Jay Lawson (jaylawson39 at yahoo.com)
  */
 public class SkillType {
+    private static final String RESOURCE_BUNDLE = "mekhq.resources." + SkillType.class.getSimpleName();
path: MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java
user: login: IllianiBird
body: Resolved in upcoming commit
created_at: 2025-05-07T02:36:23Z
author_association: COLLABORATOR
diff_hunk: @@ -322,7 +324,7 @@ private void changePersonnelView() {
                                             PersonnelTabView.GENERAL :
                                             choicePersonView.getSelectedItem();
         final XTableColumnModel columnModel = (XTableColumnModel) getPersonnelTable().getColumnModel();
-        getPersonnelTable().setRowHeight(UIUtil.scaleForGUI(15));
path: MekHQ/src/mekhq/gui/PersonnelTab.java
user: login: IllianiBird
body: I see your point, I hadn't considered that
created_at: 2025-05-07T02:38:12Z
author_association: COLLABORATOR
diff_hunk: @@ -56,6 +56,7 @@
 import static mekhq.campaign.personnel.skills.Attributes.ATTRIBUTE_IMPROVEMENT_COST;
 import static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;
 import static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;
+import static mekhq.campaign.personnel.skills.SkillType.S_ARTILLERY;
path: MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java
user: login: IllianiBird
body: SkillType not using an enum has plagued me for 2 dev cycles. In 50.07 I am absolutely fixing that.
created_at: 2025-05-07T02:40:18Z
author_association: COLLABORATOR
diff_hunk: @@ -132,96 +165,58 @@ private void initComponents() {
         gbc_pnlPortrait.gridy = 0;
         gbc_pnlPortrait.fill = GridBagConstraints.NONE;
         gbc_pnlPortrait.anchor = GridBagConstraints.NORTHWEST;
-        gbc_pnlPortrait.insets = new Insets(10, 10, 0, 0);
+        gbc_pnlPortrait.insets = new Insets(10, 0, 0, 0);
         add(pnlPortrait, gbc_pnlPortrait);
 
         JPanel pnlInfo = fillInfo();
         GridBagConstraints gridBagConstraints = new GridBagConstraints();
         gridBagConstraints.gridx = 1;
         gridBagConstraints.gridy = 0;
         gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+        gridBagConstraints.insets = new Insets(0, 0, 10, 0);
         gridBagConstraints.fill = GridBagConstraints.BOTH;
         gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
         add(pnlInfo, gridBagConstraints);
 
         int gridy = 1;
 
-        JPanel pnlSkills = fillSkills();
-        gridBagConstraints = new GridBagConstraints();
-        gridBagConstraints.gridx = 0;
-        gridBagConstraints.gridy = gridy;
-        gridBagConstraints.gridwidth = 2;
-        gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-        gridBagConstraints.fill = GridBagConstraints.BOTH;
-        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-        add(pnlSkills, gridBagConstraints);
-        gridy++;
-
-        if (campaign.getCampaignOptions().isUseAdvancedMedical()) {
-            JPanel pnlInjuries = fillInjuries();
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
-            gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-            gridBagConstraints.fill = GridBagConstraints.BOTH;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlInjuries, gridBagConstraints);
-            gridy++;
-        }
-
-        if (!person.getGenealogy().isEmpty()) {
-            JPanel pnlFamily = fillFamily();
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
-            gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-            gridBagConstraints.fill = GridBagConstraints.BOTH;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlFamily, gridBagConstraints);
-            gridy++;
-        }
-
-        if (person.getAwardController().hasAwards()) {
-            if (person.getAwardController().hasAwardsWithRibbons()) {
+        PersonAwardController awardController = person.getAwardController();
+        if (awardController.hasAwards()) {
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: IllianiBird
body: It's better, but I still don't like it. There is a lot of boilerplate. Lemme have a think. I know I can handle this whole method better, just need to figure out how...
created_at: 2025-05-07T02:42:34Z
author_association: COLLABORATOR
diff_hunk: @@ -132,96 +165,58 @@ private void initComponents() {
         gbc_pnlPortrait.gridy = 0;
         gbc_pnlPortrait.fill = GridBagConstraints.NONE;
         gbc_pnlPortrait.anchor = GridBagConstraints.NORTHWEST;
-        gbc_pnlPortrait.insets = new Insets(10, 10, 0, 0);
+        gbc_pnlPortrait.insets = new Insets(10, 0, 0, 0);
         add(pnlPortrait, gbc_pnlPortrait);
 
         JPanel pnlInfo = fillInfo();
         GridBagConstraints gridBagConstraints = new GridBagConstraints();
         gridBagConstraints.gridx = 1;
         gridBagConstraints.gridy = 0;
         gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+        gridBagConstraints.insets = new Insets(0, 0, 10, 0);
         gridBagConstraints.fill = GridBagConstraints.BOTH;
         gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
         add(pnlInfo, gridBagConstraints);
 
         int gridy = 1;
 
-        JPanel pnlSkills = fillSkills();
-        gridBagConstraints = new GridBagConstraints();
-        gridBagConstraints.gridx = 0;
-        gridBagConstraints.gridy = gridy;
-        gridBagConstraints.gridwidth = 2;
-        gridBagConstraints.weightx = 1.0;
-        gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-        gridBagConstraints.fill = GridBagConstraints.BOTH;
-        gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-        add(pnlSkills, gridBagConstraints);
-        gridy++;
-
-        if (campaign.getCampaignOptions().isUseAdvancedMedical()) {
-            JPanel pnlInjuries = fillInjuries();
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
-            gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-            gridBagConstraints.fill = GridBagConstraints.BOTH;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlInjuries, gridBagConstraints);
-            gridy++;
-        }
-
-        if (!person.getGenealogy().isEmpty()) {
-            JPanel pnlFamily = fillFamily();
-            gridBagConstraints = new GridBagConstraints();
-            gridBagConstraints.gridx = 0;
-            gridBagConstraints.gridy = gridy;
-            gridBagConstraints.gridwidth = 2;
-            gridBagConstraints.weightx = 1.0;
-            gridBagConstraints.insets = new Insets(5, 5, 5, 5);
-            gridBagConstraints.fill = GridBagConstraints.BOTH;
-            gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
-            add(pnlFamily, gridBagConstraints);
-            gridy++;
-        }
-
-        if (person.getAwardController().hasAwards()) {
-            if (person.getAwardController().hasAwardsWithRibbons()) {
+        PersonAwardController awardController = person.getAwardController();
+        if (awardController.hasAwards()) {
path: MekHQ/src/mekhq/gui/view/PersonViewPanel.java
user: login: IllianiBird
body: I decided to just go ahead and split it out. I still feel there is a better way to handle this whole method, but I'm at a point where I can't see it and moving it out resolves the problem without me inventing new issues.
created_at: 2025-05-07T02:48:30Z
author_association: COLLABORATOR
diff_hunk: @@ -516,6 +524,65 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        String additionSymbol = getTextAt(RESOURCE_BUNDLE, "tooltip.format.addition");
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? additionSymbol : "") + firstLinkedAttributeModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: github-advanced-security[bot]
body: ## Useless comparison test

Test is always false.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9340)
created_at: 2025-05-07T02:54:58Z
author_association: NONE
diff_hunk: @@ -516,6 +524,65 @@
         }
     }
 
+    /**
+     * Creates an HTML-formatted tooltip string for the skill, incorporating flavor text, aging modifiers, SPA
+     * modifiers, and linked attribute modifiers. The content is constructed using resource bundle formatting and the
+     * provided personnel options, attribute values, and reputation adjustment.
+     *
+     * @param options            the personnel options affecting SPA calculation
+     * @param attributes         the set of attributes used to determine modifier values
+     * @param adjustedReputation the reputation value impacting the tooltip details
+     *
+     * @return an HTML-formatted string representing the generated skill tooltip
+     *
+     * @author Illiani
+     * @since 0.50.06
+     */
+    public String getTooltip(PersonnelOptions options, Attributes attributes, int adjustedReputation) {
+        StringBuilder tooltip = new StringBuilder();
+
+        String flavorText = getType().getFlavorText(false, false);
+        if (!flavorText.isBlank()) {
+            tooltip.append(flavorText).append("<br><br>");
+        }
+
+        if (agingModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.aging",
+                  (agingModifier > 0 ? "+" : "") + agingModifier));
+        }
+
+        int spaModifier = getSPAModifiers(options, adjustedReputation);
+        if (spaModifier != 0) {
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.spa",
+                  (spaModifier > 0 ? "+" : "") + spaModifier));
+        }
+
+        SkillAttribute firstLinkedAttribute = type.getFirstAttribute();
+        // TODO enable once attribute modifier sare implemented
+        //        int firstLinkedAttributeModifier = attributes.getAttributeModifier(firstLinkedAttribute);
+        String additionSymbol = getTextAt(RESOURCE_BUNDLE, "tooltip.format.addition");
+        int firstLinkedAttributeModifier = 0;
+        tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+              "tooltip.format.linkedAttribute",
+              firstLinkedAttribute.getLabel(),
+              (firstLinkedAttributeModifier > 0 ? additionSymbol : "") + firstLinkedAttributeModifier));
+
+        SkillAttribute secondLinkedAttribute = type.getSecondAttribute();
+        if (secondLinkedAttribute != SkillAttribute.NONE) {
+            // TODO enable once attribute modifier sare implemented
+            //            int secondLinkedAttributeModifier = attributes.getAttributeModifier(secondLinkedAttribute);
+            int secondLinkedAttributeModifier = 0;
+            tooltip.append(getFormattedTextAt(RESOURCE_BUNDLE,
+                  "tooltip.format.linkedAttribute",
+                  secondLinkedAttribute.getLabel(),
+                  (secondLinkedAttributeModifier > 0 ? additionSymbol : "") + secondLinkedAttributeModifier));
path: MekHQ/src/mekhq/campaign/personnel/skills/Skill.java
user: login: github-advanced-security[bot]
body: ## Useless comparison test

Test is always false.

[Show more details](https://github.com/MegaMek/mekhq/security/code-scanning/9341)
created_at: 2025-05-07T02:54:58Z
author_association: NONE

PR comments:
user: login: codecov[bot]
created_at: 2025-05-07T00:22:32Z
body: ## [Codecov](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) Report
Attention: Patch coverage is `0%` with `548 lines` in your changes missing coverage. Please review.
> Project coverage is 11.68%. Comparing base [(`ee51c3d`)](https://app.codecov.io/gh/MegaMek/mekhq/commit/ee51c3d19a6971c43fd59bee1c091746143308c9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) to head [(`4edd6fa`)](https://app.codecov.io/gh/MegaMek/mekhq/commit/4edd6fa078aac98f2b73e266adf217246c88706e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek).
> Report is 15 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) | Patch % | Lines |
|---|---|---|
| [MekHQ/src/mekhq/gui/view/PersonViewPanel.java](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&filepath=MekHQ%2Fsrc%2Fmekhq%2Fgui%2Fview%2FPersonViewPanel.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek#diff-TWVrSFEvc3JjL21la2hxL2d1aS92aWV3L1BlcnNvblZpZXdQYW5lbC5qYXZh) | 0.00% | [463 Missing :warning: ](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) |
| [...kHQ/src/mekhq/campaign/personnel/skills/Skill.java](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&filepath=MekHQ%2Fsrc%2Fmekhq%2Fcampaign%2Fpersonnel%2Fskills%2FSkill.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek#diff-TWVrSFEvc3JjL21la2hxL2NhbXBhaWduL3BlcnNvbm5lbC9za2lsbHMvU2tpbGwuamF2YQ==) | 0.00% | [25 Missing :warning: ](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) |
| [...src/mekhq/campaign/personnel/skills/SkillType.java](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&filepath=MekHQ%2Fsrc%2Fmekhq%2Fcampaign%2Fpersonnel%2Fskills%2FSkillType.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek#diff-TWVrSFEvc3JjL21la2hxL2NhbXBhaWduL3BlcnNvbm5lbC9za2lsbHMvU2tpbGxUeXBlLmphdmE=) | 0.00% | [24 Missing :warning: ](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) |
| [.../mekhq/gui/adapter/PersonnelTableMouseAdapter.java](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&filepath=MekHQ%2Fsrc%2Fmekhq%2Fgui%2Fadapter%2FPersonnelTableMouseAdapter.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek#diff-TWVrSFEvc3JjL21la2hxL2d1aS9hZGFwdGVyL1BlcnNvbm5lbFRhYmxlTW91c2VBZGFwdGVyLmphdmE=) | 0.00% | [23 Missing :warning: ](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) |
| [MekHQ/src/mekhq/campaign/personnel/Person.java](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&filepath=MekHQ%2Fsrc%2Fmekhq%2Fcampaign%2Fpersonnel%2FPerson.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek#diff-TWVrSFEvc3JjL21la2hxL2NhbXBhaWduL3BlcnNvbm5lbC9QZXJzb24uamF2YQ==) | 0.00% | [11 Missing :warning: ](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) |
| [MekHQ/src/mekhq/gui/PersonnelTab.java](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&filepath=MekHQ%2Fsrc%2Fmekhq%2Fgui%2FPersonnelTab.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek#diff-TWVrSFEvc3JjL21la2hxL2d1aS9QZXJzb25uZWxUYWIuamF2YQ==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##             master    #6894      +/-   ##
============================================
- Coverage     11.69%   11.68%   -0.01%     
+ Complexity     6657     6651       -6     
============================================
  Files          1100     1100              
  Lines        141324   141324              
  Branches      21848    21885      +37     
============================================
- Hits          16526    16515      -11     
- Misses       123098   123112      +14     
+ Partials       1700     1697       -3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/MegaMek/mekhq/pull/6894?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MegaMek).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: IllianiBird
created_at: 2025-05-07T01:10:42Z
body: Removed 'For New Dev Cycle' tag. Updated to be suitable for 50.06
author_association: COLLABORATOR
user: login: Scoppio
created_at: 2025-05-07T01:49:08Z
body: I am approving, but I dont know what the green and red arrows are supposed to mean
author_association: COLLABORATOR
user: login: IllianiBird
created_at: 2025-05-07T01:55:21Z
body: They mean that the skill is being buffed or debuffed by something. The tooltip explains what the actual modifier is. It's largely irrelevant (and will be mostly unseen) in 50.06, but will be very important in 50.07 when attribute score modify every skill
author_association: COLLABORATOR
user: login: IllianiBird
created_at: 2025-05-07T02:48:45Z
body: - Updated to account for Luana's feedback
author_association: COLLABORATOR